@cocorograph/hub-agent 0.8.73 → 0.8.75

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/README.md CHANGED
@@ -105,7 +105,7 @@ hub-agent/
105
105
  │ ├── ws-client.mjs # outbound WSS + reconnect with jitter
106
106
  │ ├── claude-stream-bridge.mjs # Claude Agent SDK チャット中継
107
107
  │ ├── codex-stream-bridge.mjs # Codex app-server チャット中継
108
- │ ├── tmux.mjs # tmux exec/list/create/kill
108
+ │ ├── tmux.mjs # tmux list/create/kill
109
109
  │ ├── state.mjs # session status / context_pct 検知
110
110
  │ ├── skills.mjs # ~/.claude/skills 集計
111
111
  │ ├── usage.mjs # Claude Code 使用量集計
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocorograph/hub-agent",
3
- "version": "0.8.73",
3
+ "version": "0.8.75",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの Claude / Codex チャットと tmux セッションを中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -58,11 +58,11 @@ const SETTLE_MAX_MS = 25_000
58
58
  * @param {{status?: string, error?: string|null}|undefined} startup
59
59
  * @returns {{name: string, status: string, serverInfo?: object, error?: string, tools: {name: string}[]}}
60
60
  */
61
- export function normalizeCodexMcpServer(entry, startup) {
61
+ export function normalizeCodexMcpServer(entry, startup, runtime) {
62
62
  const name = entry?.name
63
63
  const authStatus = entry?.authStatus
64
- const startupState = startup?.status
65
- const startupError = startup?.error || undefined
64
+ const startupState = runtime?.status || startup?.status
65
+ const startupError = (runtime ? runtime.error : startup?.error) || undefined
66
66
 
67
67
  // tools は {toolName: {name, description, inputSchema...}} のマップで来る。
68
68
  // inputSchema を載せると WS が 1MB 超になるため、名前だけに落とす。
@@ -74,7 +74,9 @@ export function normalizeCodexMcpServer(entry, startup) {
74
74
  } else if (startupState === "starting") {
75
75
  status = "pending"
76
76
  } else if (startupState === "failed" || startupState === "cancelled") {
77
- status = isAuthFailure(startupError, authStatus) ? "needs-auth" : "failed"
77
+ status = (runtime || startup)?.failureReason === "reauthenticationRequired" || isAuthFailure(startupError, authStatus) ? "needs-auth" : "failed"
78
+ } else if (startupState) {
79
+ status = "pending"
78
80
  } else if (authStatus === "notLoggedIn") {
79
81
  // 通知が来ていない (thread 起動前 / 取りこぼし) が、未ログインは確実に再認証が要る。
80
82
  status = "needs-auth"
@@ -88,6 +90,7 @@ export function normalizeCodexMcpServer(entry, startup) {
88
90
  return {
89
91
  name,
90
92
  status,
93
+ canAuthenticate: authStatus === "oAuth" || authStatus === "notLoggedIn",
91
94
  ...(entry?.serverInfo ? { serverInfo: entry.serverInfo } : {}),
92
95
  ...(startupError ? { error: truncateError(startupError) } : {}),
93
96
  tools: toolNames.map((n) => ({ name: n })),
@@ -95,22 +98,47 @@ export function normalizeCodexMcpServer(entry, startup) {
95
98
  }
96
99
 
97
100
  /** 起動失敗が「認証が要る」由来か。OAuth フロー (再認証ボタン) に倒してよいかの判定。 */
98
- function isAuthFailure(error, authStatus) {
101
+ export function isAuthFailure(error, authStatus) {
99
102
  if (authStatus === "notLoggedIn") return true
100
103
  if (!error) return false
101
104
  // 実測のエラー文言:
102
105
  // "MCP client for `hub` failed to start: ... Auth error: OAuth authorization required,
103
106
  // when send initialize request"
104
- return /oauth|authorization required|unauthorized|401/i.test(error)
107
+ return /oauth|auth(?:entication|orization)? required|unauthorized|401/i.test(error)
105
108
  }
106
109
 
107
110
  /** エラー文言は UI の 1 行に収まらないため要約する (全文は agent のログに残る)。 */
108
111
  function truncateError(error) {
109
112
  const s = String(error)
110
- if (/oauth|authorization required/i.test(s)) return "OAuth 認証が必要です"
113
+ if (/oauth|auth(?:entication|orization)? required/i.test(s)) return "OAuth 認証が必要です"
111
114
  return s.length > 160 ? `${s.slice(0, 157)}…` : s
112
115
  }
113
116
 
117
+ /** Both control and conversation clients use the same bounded, validated inventory pagination. */
118
+ export async function listCodexMcpServers(client, { threadId, startup = new Map(), runtime = new Map() } = {}) {
119
+ const servers = []
120
+ const seenCursors = new Set()
121
+ let cursor
122
+ do {
123
+ const res = await client.request("mcpServerStatus/list", {
124
+ detail: "toolsAndAuthOnly",
125
+ ...(threadId ? { threadId } : {}),
126
+ ...(cursor ? { cursor } : {}),
127
+ })
128
+ if (!Array.isArray(res?.data) || res.data.some((entry) => typeof entry?.name !== "string" || !entry.name)) {
129
+ throw new Error("codex_mcp_status_invalid")
130
+ }
131
+ servers.push(...res.data.map((entry) => normalizeCodexMcpServer(entry, startup.get(entry.name), runtime.get(entry.name))))
132
+ cursor = res.nextCursor
133
+ if (cursor != null && (typeof cursor !== "string" || !cursor || seenCursors.has(cursor))) {
134
+ throw new Error("codex_mcp_status_cursor_invalid")
135
+ }
136
+ if (cursor) seenCursors.add(cursor)
137
+ if (seenCursors.size > 100) throw new Error("codex_mcp_status_page_limit")
138
+ } while (cursor)
139
+ return servers
140
+ }
141
+
114
142
  export class CodexMcpControl {
115
143
  /**
116
144
  * @param {{logger?: object, codexCmd?: string, cwd?: string, idleMs?: number,
@@ -185,7 +213,7 @@ export class CodexMcpControl {
185
213
  if (msg?.method === "mcpServer/startupStatus/updated") {
186
214
  const p = msg.params || {}
187
215
  if (p.name) {
188
- this.startup.set(p.name, { status: p.status, error: p.error ?? null })
216
+ this.startup.set(p.name, { status: p.status, error: p.error ?? null, failureReason: p.failureReason })
189
217
  }
190
218
  return
191
219
  }
@@ -243,13 +271,9 @@ export class CodexMcpControl {
243
271
  const client = await this._ensure()
244
272
  await this._waitForStartupSettle()
245
273
 
246
- const res = await client.request("mcpServerStatus/list", {
247
- // full だと resources / resourceTemplates まで載って更に膨らむ。
248
- detail: "toolsAndAuthOnly",
249
- })
250
- const rows = res?.data ?? []
274
+ const servers = await listCodexMcpServers(client, { startup: this.startup })
251
275
  this._touch()
252
- return rows.map((entry) => normalizeCodexMcpServer(entry, this.startup.get(entry?.name)))
276
+ return servers
253
277
  }
254
278
 
255
279
  /**
@@ -48,6 +48,7 @@ import { recordInjectSourcePersistent } from "./inject-source-store.mjs"
48
48
 
49
49
  import { AsyncTtlCache } from "./async-ttl-cache.mjs"
50
50
  import { CodexAppServerClient } from "./codex-appserver-client.mjs"
51
+ import { listCodexMcpServers } from "./codex-mcp-control.mjs"
51
52
  import { addCodexSessionTitles, operateCodexSession, validateCodexSessionOperation } from "./codex-session-operations.mjs"
52
53
  import { defaultCodexDir } from "./codex-profiles.mjs"
53
54
  import {
@@ -2104,6 +2105,86 @@ export class CodexStreamBridge extends EventEmitter {
2104
2105
  return Array.from(new Set(this.sessions.values())).filter((s) => !s.dead)
2105
2106
  }
2106
2107
 
2108
+ _mcpSession(stream_id) {
2109
+ if (typeof stream_id !== "string" || !stream_id) throw new Error("codex_mcp_stream_required")
2110
+ const session = this.sessions.get(stream_id)
2111
+ if (!session || session.dead) throw new Error("codex_mcp_session_missing")
2112
+ if (!session.threadId || !session.client || session.client.closed) throw new Error("codex_mcp_session_unavailable")
2113
+ return session
2114
+ }
2115
+
2116
+ async mcpStatus({ stream_id } = {}) {
2117
+ const session = this._mcpSession(stream_id)
2118
+ const servers = await listCodexMcpServers(session.client, {
2119
+ threadId: session.threadId,
2120
+ startup: session.mcpStartupByThread?.get(session.threadId),
2121
+ runtime: session.mcpRuntimeByThread?.get(session.threadId),
2122
+ })
2123
+ // A detach/reattach during the RPC must not return another conversation's state.
2124
+ if (this._mcpSession(stream_id) !== session) throw new Error("codex_mcp_session_changed")
2125
+ return servers
2126
+ }
2127
+
2128
+ async mcpReconnect(serverName, { stream_id } = {}) {
2129
+ const session = this._mcpSession(stream_id)
2130
+ await session.client.request("config/mcpServer/reload", null)
2131
+ if (this._mcpSession(stream_id) !== session) throw new Error("codex_mcp_session_changed")
2132
+ // The RPC queues a refresh of loaded threads; acceptance is not proof of recovery.
2133
+ return { targeted: 1, refreshQueued: true }
2134
+ }
2135
+
2136
+ async mcpAuthenticate(serverName, { stream_id } = {}) {
2137
+ if (typeof serverName !== "string" || !serverName.trim()) throw new Error("codex_mcp_server_required")
2138
+ const session = this._mcpSession(stream_id)
2139
+ const result = await session.client.request("mcpServer/oauth/login", { name: serverName, threadId: session.threadId })
2140
+ if (this._mcpSession(stream_id) !== session) throw new Error("codex_mcp_session_changed")
2141
+ if (typeof result?.authorizationUrl !== "string" || !result.authorizationUrl) throw new Error("codex_mcp_oauth_response_invalid")
2142
+ return { authorizationUrl: result.authorizationUrl, callbackExpected: false, requiresUserAction: true }
2143
+ }
2144
+
2145
+ _observeMcpNotification(session, msg) {
2146
+ const p = msg?.params
2147
+ const threadId = p?.threadId
2148
+ // Startup may arrive before thread/start resolves. Key by its explicit thread id.
2149
+ // OAuth completions without a thread id cannot establish which conversation recovered.
2150
+ if (typeof threadId !== "string" || !threadId) return
2151
+ if (msg.method === "mcpServer/oauthLogin/completed" && threadId === session.threadId && p.success === true && typeof p.name === "string" && p.name) {
2152
+ session.mcpRuntimeByThread ||= new Map()
2153
+ if (!session.mcpRuntimeByThread.has(threadId)) session.mcpRuntimeByThread.set(threadId, new Map())
2154
+ session.mcpRuntimeByThread.get(threadId).set(p.name, { status: "starting", error: null })
2155
+ return
2156
+ }
2157
+ if (msg.method === "mcpServer/startupStatus/updated" && typeof p.name === "string" && p.name) {
2158
+ session.mcpStartupByThread ||= new Map()
2159
+ if (!session.mcpStartupByThread.has(threadId)) session.mcpStartupByThread.set(threadId, new Map())
2160
+ session.mcpStartupByThread.get(threadId).set(p.name, { status: p.status, error: p.error ?? null, failureReason: p.failureReason })
2161
+ const runtime = session.mcpRuntimeByThread?.get(threadId)
2162
+ if ((p.status === "failed" || p.status === "cancelled") && runtime?.get(p.name)?.status === "starting") {
2163
+ runtime.delete(p.name)
2164
+ }
2165
+ }
2166
+ const item = p.item
2167
+ if (msg.method !== "item/completed" || item?.type !== "mcpToolCall" || typeof item.server !== "string" || !item.server) return
2168
+ session.mcpRuntimeByThread ||= new Map()
2169
+ if (!session.mcpRuntimeByThread.has(threadId)) session.mcpRuntimeByThread.set(threadId, new Map())
2170
+ const runtime = session.mcpRuntimeByThread.get(threadId)
2171
+ const errorText = typeof item.error === "string" ? item.error : item.error?.message
2172
+ const resultText = Array.isArray(item.result?.content)
2173
+ ? item.result.content.filter((part) => part?.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n") : ""
2174
+ const failed = item.status === "failed" || item.error || item.result?.isError === true
2175
+ // Tool failures include ordinary business errors; only explicit auth requirements affect
2176
+ // server health. Mentioning OAuth in a response is not itself an authentication failure.
2177
+ const requiresAuthentication = (text) => typeof text === "string" && /\bauth(?:entication|orization)?\s+required\b/i.test(text)
2178
+ if (failed && (requiresAuthentication(errorText) || requiresAuthentication(resultText))) {
2179
+ runtime.set(item.server, { status: "failed", error: "Authentication required" })
2180
+ } else if (!failed && item.status === "completed" && Array.isArray(item.result?.content)) {
2181
+ runtime.delete(item.server)
2182
+ session.mcpStartupByThread ||= new Map()
2183
+ if (!session.mcpStartupByThread.has(threadId)) session.mcpStartupByThread.set(threadId, new Map())
2184
+ session.mcpStartupByThread.get(threadId).set(item.server, { status: "ready", error: null })
2185
+ }
2186
+ }
2187
+
2107
2188
  _recordAcceptedInput(session, inputId) {
2108
2189
  if (!session || typeof inputId !== "string" || !inputId) return
2109
2190
  const cancelledIds = session.cancelledInputIds || (session.cancelledInputIds = [])
@@ -3211,6 +3292,7 @@ export class CodexStreamBridge extends EventEmitter {
3211
3292
 
3212
3293
  _onNotification(session, msg) {
3213
3294
  const { stream_id } = session
3295
+ this._observeMcpNotification(session, msg)
3214
3296
  // app-server からの通知 = ターンが進行している証跡。これで touch することで、
3215
3297
  // 正常に走っているターンは reaper に回収されず、通知が途絶えたスタックターンだけが
3216
3298
  // 放棄セッションとして回収される。
package/src/hooks.mjs CHANGED
@@ -17,7 +17,6 @@
17
17
  export const HOOK_NAMES = Object.freeze([
18
18
  "onAgentStart",
19
19
  "onAgentStop",
20
- "interceptTmuxExec",
21
20
  "interceptSessionCreate",
22
21
  "interceptSessionKill",
23
22
  "filterSessions",
@@ -25,26 +24,12 @@ export const HOOK_NAMES = Object.freeze([
25
24
  "transformStatusDetection",
26
25
  ])
27
26
 
28
- /**
29
- * @typedef {Object} TmuxExecArgs
30
- * @property {string[]} args
31
- * @property {Record<string, string>} env
32
- */
33
-
34
- /**
35
- * @typedef {Object} TmuxExecDecision
36
- * @property {string} command
37
- * @property {string[]} args
38
- * @property {Record<string, string>} [env]
39
- */
40
-
41
27
  /**
42
28
  * @typedef {Object} Plugin
43
29
  * @property {string} name
44
30
  * @property {{
45
31
  * onAgentStart?: (ctx: HookContext) => Promise<void>,
46
32
  * onAgentStop?: (ctx: HookContext) => Promise<void>,
47
- * interceptTmuxExec?: (ctx: HookContext & TmuxExecArgs) => Promise<TmuxExecDecision | null>,
48
33
  * }} hooks
49
34
  */
50
35
 
package/src/main.mjs CHANGED
@@ -5,7 +5,7 @@
5
5
  * - plugin discovery + onAgentStart broadcast
6
6
  * - WS メッセージのディスパッチ:
7
7
  * - claude.* / codex.* → ClaudeStreamBridge / CodexStreamBridge (チャット)
8
- * - tmux.* → tmux module (exec / セッション一覧・作成・削除)
8
+ * - tmux.* → tmux module (セッション一覧・作成・削除)
9
9
  *
10
10
  * 仕様書: ナレッジ/インフラ/cockpit-hub-hosted-integration-spec (id=6080)
11
11
  */
@@ -43,7 +43,7 @@ import {
43
43
  resolveSyncTargets,
44
44
  syncBundleTargets,
45
45
  } from "./hub-bundle.mjs"
46
- import { loadPlugins, runHookBroadcast, runHookChain } from "./plugin-loader.mjs"
46
+ import { loadPlugins, runHookBroadcast } from "./plugin-loader.mjs"
47
47
  import { WsClient } from "./ws-client.mjs"
48
48
  import { DispatchScheduler } from "./dispatch-scheduler.mjs"
49
49
  import { AsyncTtlCache } from "./async-ttl-cache.mjs"
@@ -104,7 +104,6 @@ import {
104
104
  buildWorktreeIndex,
105
105
  createSession as createTmuxSession,
106
106
  createWorktreeDir,
107
- execTmux,
108
107
  killManySessions,
109
108
  killSession as killTmuxSession,
110
109
  listSessions as listTmuxSessions,
@@ -387,10 +386,10 @@ export function withDeadline(promise, timeoutMs, onTimeout) {
387
386
  /**
388
387
  * B7: 直列 dispatchChain をバイパスして即時処理してよい低レイテンシ経路かを判定する。
389
388
  * - claude.interrupt / codex.interrupt (2026-07-02): 停止指示。前段に重い dispatch
390
- * (tmux.exec / tmux セッション操作等) が滞留していると停止が遅延するため直列キューを
389
+ * (tmux セッション操作等) が滞留していると停止が遅延するため直列キューを
391
390
  * バイパスする。中断は「現ターンを止める」冪等な制御で、入力系のような順序依存が無い
392
391
  * (むしろキュー内の後続入力より先に届くべき)。
393
- * 入力系 (claude.input)・他の制御系 (permission / tmux.exec / tmux セッション操作等) は
392
+ * 入力系 (claude.input)・他の制御系 (permission / tmux セッション操作等) は
394
393
  * WS 受信順 = pane 反映順を守るため false (= 直列キューに残す)。
395
394
  */
396
395
  export function isFastPathMessage(type) {
@@ -3330,65 +3329,6 @@ export function handleClaudeChatInput(msg, ctx) {
3330
3329
  return p
3331
3330
  }
3332
3331
 
3333
- /**
3334
- * `tmux.exec` を処理し、`tmux.result` を返す。
3335
- *
3336
- * 公式 Hub の送信入口は撤去済みだが、公開 0.x package の root handler / deep import、
3337
- * plugin hook、旧 Hub との互換入口として handler・dispatch・execTmux を維持する。
3338
- *
3339
- * plugin の `interceptTmuxExec` hook が `{ command, args, env }` を返したときは、その
3340
- * command を代わりに spawn する (例: remote-ops プラグインで ssh 越しに実行)。hook が
3341
- * 何も返さなければ既定のローカル `execTmux` へ落ちる。どちらの経路でも
3342
- * `request_id` / `stdout` / `stderr` / `exit_code` / `plugin` の 5 フィールドを揃えて返す。
3343
- *
3344
- * dispatch から呼ぶ用のヘルパーだが、テストから直接叩けるよう export している
3345
- * (handleStreamsSyncResponse / handleClaudeChatInput と同じ扱い)。
3346
- *
3347
- * @param {{request_id?:string, args?:string[]}} msg
3348
- * @param {{plugins:any[], logger:object, client:{send:Function}}} ctx
3349
- * @param {{execTmux?:Function}} [deps] 既定実装の差し替え口 (テスト専用)。
3350
- */
3351
- export async function handleTmuxExec(msg, ctx, deps = {}) {
3352
- const execTmuxImpl = deps.execTmux || execTmux
3353
- const args = Array.isArray(msg.args) ? msg.args : []
3354
- const hookResult = await runHookChain(ctx.plugins, "interceptTmuxExec", {
3355
- logger: ctx.logger,
3356
- args,
3357
- env: process.env,
3358
- })
3359
- let result
3360
- if (hookResult?.result?.command) {
3361
- // plugin が独自 command で差し替え (例: remote-ops プラグインで ssh 越し)
3362
- const { command, args: spawnArgs, env } = hookResult.result
3363
- try {
3364
- const { execFile } = await import("node:child_process")
3365
- const { promisify } = await import("node:util")
3366
- const execFileP = promisify(execFile)
3367
- const { stdout, stderr } = await execFileP(command, spawnArgs, {
3368
- env: env || process.env,
3369
- maxBuffer: 4 * 1024 * 1024,
3370
- })
3371
- result = { stdout, stderr, exit_code: 0 }
3372
- } catch (err) {
3373
- result = {
3374
- stdout: err.stdout || "",
3375
- stderr: err.stderr || err.message || "",
3376
- exit_code: typeof err.code === "number" ? err.code : 1,
3377
- }
3378
- }
3379
- } else {
3380
- result = await execTmuxImpl(args)
3381
- }
3382
- ctx.client.send({
3383
- type: "tmux.result",
3384
- request_id: msg.request_id,
3385
- stdout: result.stdout,
3386
- stderr: result.stderr,
3387
- exit_code: result.exit_code,
3388
- plugin: hookResult?.plugin || null,
3389
- })
3390
- }
3391
-
3392
3332
  /**
3393
3333
  * claude.attach の ready 直後にpushする履歴を組み立てる。
3394
3334
  * Browserからの claude.history.request 往復を省くため、resume時だけ先回りして読む。
@@ -4501,7 +4441,7 @@ export async function dispatch(msg, ctx) {
4501
4441
  case "codex.mcp.status":
4502
4442
  case "codex.mcp.reconnect":
4503
4443
  case "codex.mcp.authenticate": {
4504
- // Codex 版 MCP control (agent-global, セッション非依存) — 2026-07-14。
4444
+ // stream_id selects the live conversation client. Legacy callers retain control scope.
4505
4445
  // claude.mcp.* と同じ request_id 相関・同じ応答形 (servers[] は Cockpit の
4506
4446
  // McpServerStatus に正規化済み) にすることで、フロントの Popover / 再認証 UI を
4507
4447
  // そのまま再利用する。
@@ -4511,11 +4451,15 @@ export async function dispatch(msg, ctx) {
4511
4451
  // (authenticate の応答で callbackExpected: false を返す)
4512
4452
  // - clearauth: 対応する RPC が無い (CLI の `codex mcp logout` のみ)
4513
4453
  const request_id = msg.request_id
4514
- if (!ctx.codexMcpControl || !codexAvailableForDispatch(ctx)) {
4454
+ const sessionScope = Object.hasOwn(msg, "stream_id")
4455
+ const scope = sessionScope ? "session" : "control"
4456
+ const correlation = { ...(sessionScope ? { stream_id: msg.stream_id } : {}), scope }
4457
+ if ((sessionScope ? !ctx.codexBridge : !ctx.codexMcpControl) || !codexAvailableForDispatch(ctx)) {
4515
4458
  ctx.client.send({
4516
4459
  type: "codex.mcp.response",
4517
4460
  request_id,
4518
4461
  action: msg.type,
4462
+ ...correlation,
4519
4463
  error: "codex_cli_unavailable",
4520
4464
  })
4521
4465
  return
@@ -4525,19 +4469,22 @@ export async function dispatch(msg, ctx) {
4525
4469
  switch (msg.type) {
4526
4470
  case "codex.mcp.status":
4527
4471
  result = {
4528
- servers: await ctx.codexMcpControl.status({
4529
- reload: msg.reload === true,
4530
- }),
4472
+ servers: sessionScope
4473
+ ? await ctx.codexBridge.mcpStatus({ stream_id: msg.stream_id })
4474
+ : await ctx.codexMcpControl.status({ reload: msg.reload === true }),
4531
4475
  }
4532
4476
  break
4533
4477
  case "codex.mcp.reconnect": {
4534
- const r = await ctx.codexMcpControl.reconnect(msg.server_name)
4535
- result = { servers: r.servers, targeted: r.targeted }
4478
+ result = sessionScope
4479
+ ? await ctx.codexBridge.mcpReconnect(msg.server_name, { stream_id: msg.stream_id })
4480
+ : await ctx.codexMcpControl.reconnect(msg.server_name)
4536
4481
  break
4537
4482
  }
4538
4483
  case "codex.mcp.authenticate":
4539
4484
  result = {
4540
- auth: await ctx.codexMcpControl.authenticate(msg.server_name),
4485
+ auth: sessionScope
4486
+ ? await ctx.codexBridge.mcpAuthenticate(msg.server_name, { stream_id: msg.stream_id })
4487
+ : await ctx.codexMcpControl.authenticate(msg.server_name),
4541
4488
  }
4542
4489
  break
4543
4490
  }
@@ -4546,6 +4493,7 @@ export async function dispatch(msg, ctx) {
4546
4493
  request_id,
4547
4494
  action: msg.type,
4548
4495
  server_name: msg.server_name,
4496
+ ...correlation,
4549
4497
  ...result,
4550
4498
  })
4551
4499
  } catch (err) {
@@ -4558,6 +4506,7 @@ export async function dispatch(msg, ctx) {
4558
4506
  request_id,
4559
4507
  action: msg.type,
4560
4508
  server_name: msg.server_name,
4509
+ ...correlation,
4561
4510
  error: err?.message || "mcp_control_failed",
4562
4511
  })
4563
4512
  }
@@ -5461,11 +5410,6 @@ export async function dispatch(msg, ctx) {
5461
5410
  }
5462
5411
  return
5463
5412
  }
5464
- case "tmux.exec": {
5465
- // ロジックは handleTmuxExec へ抽出 (テストから直接呼べる)。
5466
- await handleTmuxExec(msg, ctx)
5467
- return
5468
- }
5469
5413
  case "tmux.list_sessions": {
5470
5414
  try {
5471
5415
  const snapshot = await tmuxSessionsSnapshotCache.get(async () => {
package/src/tmux.mjs CHANGED
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * tmux サブプロセス呼び出し抽象 (Sprint E)。
3
3
  *
4
- * - `execTmux(args)`: raw 実行 (stdout / stderr / exit_code を返す)
5
4
  * - `listSessions()`: 構造化された TmuxSession[] を返す。各 session に
6
5
  * status (state.mjs の detectSessionState) と cwd (active pane の current_path)
7
6
  * を並列で付与
@@ -601,28 +600,6 @@ export async function setTmuxGlobalEnv(name, value, opts = {}) {
601
600
  }
602
601
  }
603
602
 
604
- /**
605
- * 汎用 tmux 実行 (raw)。任意の tmux サブコマンドを実行して結果を返す低レベル API。
606
- *
607
- * @returns {Promise<{stdout: string, stderr: string, exit_code: number}>}
608
- */
609
- export async function execTmux(args, opts = {}) {
610
- if (!Array.isArray(args)) throw new TypeError("execTmux requires args: string[]")
611
- try {
612
- const { stdout, stderr } = await execFileP(tmuxBin(opts), args, {
613
- env: opts.env || process.env,
614
- maxBuffer: 4 * 1024 * 1024,
615
- })
616
- return { stdout, stderr, exit_code: 0 }
617
- } catch (err) {
618
- return {
619
- stdout: err.stdout || "",
620
- stderr: err.stderr || err.message || "",
621
- exit_code: typeof err.code === "number" ? err.code : 1,
622
- }
623
- }
624
- }
625
-
626
603
  // P1 (重複排除): cwd 取得は state.mjs の getSessionCwd (キャッシュ付き) を共用する。
627
604
  // 旧 tmux.mjs 私有実装は display-message を毎回 spawn しており、state loop の
628
605
  // listSessionStates と list_sessions が別キャッシュ無しで二重 fork していた。