@cocorograph/hub-agent 0.7.104 → 0.7.105

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.104",
3
+ "version": "0.7.105",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの tmux/pty を中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -334,6 +334,10 @@ export class CodexStreamBridge extends EventEmitter {
334
334
  /** @type {Map<string, object>} stream_id -> session record */
335
335
  this.sessions = new Map()
336
336
  this._accountRateLimitsByProfile = new Map()
337
+ // Phase 3 (Read-plane): listThreads の single-flight。key(`cwd::limit`) -> in-flight Promise。
338
+ // codex.sessions.request を直列キューから外したため、同一 cwd への同時リクエストが
339
+ // app-server の同時 spawn (storm) を起こさないよう 1 本に畳む。
340
+ this._listThreadsInFlight = new Map()
337
341
  }
338
342
 
339
343
  /**
@@ -762,6 +766,19 @@ export class CodexStreamBridge extends EventEmitter {
762
766
  * @returns {Promise<Array<{thread_id: string, cwd: string, preview: string, updated_at_ms: number}>>}
763
767
  */
764
768
  async listThreads({ cwd, limit } = {}) {
769
+ // Phase 3 (Read-plane): single-flight。同一 (cwd, limit) への同時リクエストを 1 本の
770
+ // in-flight Promise に畳み、app-server の同時 spawn (storm) を防ぐ。異なる cwd は個別に走る。
771
+ const key = `${cwd || ""}::${limit ?? ""}`
772
+ const existing = this._listThreadsInFlight.get(key)
773
+ if (existing) return existing
774
+ const p = this._doListThreads({ cwd, limit }).finally(() => {
775
+ this._listThreadsInFlight.delete(key)
776
+ })
777
+ this._listThreadsInFlight.set(key, p)
778
+ return p
779
+ }
780
+
781
+ async _doListThreads({ cwd, limit } = {}) {
765
782
  const client = this._clientFactory({ cwd, codexCmd: this.codexCmd, logger: this.logger })
766
783
  client.start()
767
784
  try {
package/src/main.mjs CHANGED
@@ -342,6 +342,33 @@ export function isFastPathMessage(type) {
342
342
  )
343
343
  }
344
344
 
345
+ /**
346
+ * Phase 3 (Read-plane 2026-07-24): タイムライン/セッション一覧の「読み取り専用 RPC」を判定する。
347
+ * これらは disk (jsonl) / codex rollout を読んで自前の response を送るだけで pane・入力・bridge の
348
+ * 状態を一切変更しない (ハンドラ本体で確認済み: claude.history.request=fetchSessionHistory /
349
+ * claude.sessions.request=listSessions / codex.sessions.request=listThreads、いずれも純粋読み取り)。
350
+ * メッセージ自身が cwd/session_id/stream_id を持ち、frontend も stream_id/cwd 照合で stale 応答を
351
+ * 捨てるため、WS 受信順に対する順序依存が無い。
352
+ *
353
+ * これを直列 dispatchChain から外す理由 (view/spawn 分離の核): 直列キューは「WS 受信順 = pane 反映順」
354
+ * を守るためのものだが、その代償として前方の重い処理 (codex.sessions.request/codex.attach の
355
+ * app-server spawn = 数秒、claude.sessions.request の全ファイル読み) が後続の claude.attach を
356
+ * head-of-line blocking し、attach→ready を数十秒遅延させていた (本番計測 cold p95 40〜54s)。
357
+ * claude/codex が同一 dispatchChain を共有するため codex の spawn が claude の切替まで巻き込む。
358
+ * 読み取り RPC を並行レーンへ逃がすことで、タイムライン閲覧が live 接続 (spawn) の完了を待たなくなる。
359
+ *
360
+ * ⚠️ codex.sessions.request は listThreads が app-server を spawn するため、off-queue 化の副作用で
361
+ * 同一 cwd への同時リクエストが spawn storm を起こしうる。CodexStreamBridge.listThreads 側に
362
+ * single-flight ガードを併設して同時 spawn を 1 本に畳む (B で rollout disk 走査へ置換すれば spawn 自体が消える)。
363
+ */
364
+ export function isReadOnlyRpc(type) {
365
+ return (
366
+ type === "claude.history.request" ||
367
+ type === "claude.sessions.request" ||
368
+ type === "codex.sessions.request"
369
+ )
370
+ }
371
+
345
372
  /**
346
373
  * active プロファイルの configDir を process.env.CLAUDE_CONFIG_DIR へ反映する。
347
374
  * 既定 (`~/.claude`) のときは変数を消して従来挙動 (Claude Code の既定パス) に戻す。
@@ -1122,6 +1149,20 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
1122
1149
  })
1123
1150
  return
1124
1151
  }
1152
+ // Phase 3 (Read-plane): 読み取り専用 RPC (history/sessions) も直列キューから外して並行処理する。
1153
+ // 直列キュー前方の重い処理 (codex app-server spawn 等) が claude.attach→ready を HoL blocking
1154
+ // するのを防ぐ (view/spawn 分離)。純粋読み取りで順序依存が無いことは isReadOnlyRpc の説明参照。
1155
+ if (isReadOnlyRpc(msg?.type)) {
1156
+ Promise.resolve(
1157
+ dispatch(msg, { ...ctx, client, ptyBridge, claudeBridge, codexBridge, codexMcpControl, uploadManager }),
1158
+ ).catch((err) => {
1159
+ logger.error(
1160
+ { err: err.message, type: msg?.type },
1161
+ "dispatch threw (bypassed read-only RPC lane)",
1162
+ )
1163
+ })
1164
+ return
1165
+ }
1125
1166
  dispatchChain = dispatchChain
1126
1167
  .then(() => dispatch(msg, { ...ctx, client, ptyBridge, claudeBridge, codexBridge, codexMcpControl, uploadManager }))
1127
1168
  .catch((err) => {