@cocorograph/hub-agent 0.8.40 → 0.8.41

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.8.40",
3
+ "version": "0.8.41",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの Claude / Codex チャットと tmux セッションを中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -56,6 +56,11 @@ import { readPositiveIntEnv, sanitizePositiveInt } from "./positive-int-env.mjs"
56
56
  import { nextSessionSeq } from "./session-seq.mjs"
57
57
  import { codexRolloutPresence, listCodexThreadsFromDisk } from "./usage.mjs"
58
58
 
59
+ function parseTurnMismatch(reason) {
60
+ const mismatch = /^expected active turn id (\S+) but found (\S+)$/.exec(reason)
61
+ return mismatch ? { expected: mismatch[1], found: mismatch[2] } : null
62
+ }
63
+
59
64
  /**
60
65
  * sibling-inject (2026-07-04) 用: spawn する codex app-server プロセス自身の
61
66
  * 環境変数に、この cwd の capability トークンを乗せる。ディスクには書かず、
@@ -2463,15 +2468,51 @@ export class CodexStreamBridge extends EventEmitter {
2463
2468
  if (!session.dead) this._emitQueueState(session)
2464
2469
  }
2465
2470
 
2466
- /** @returns {Promise<void>} */
2471
+ /**
2472
+ * @returns {Promise<
2473
+ * | { outcome: "interrupted", turnId: string, corrected: boolean }
2474
+ * | { outcome: "no_active_turn" }
2475
+ * | { outcome: "failed", reason: string }
2476
+ * >}
2477
+ */
2467
2478
  async interrupt({ stream_id }) {
2468
2479
  const session = this._require(stream_id)
2469
2480
  this._touch(session, stream_id)
2470
- if (!session.activeTurnId) return
2471
- await session.client.request("turn/interrupt", {
2472
- threadId: session.threadId,
2473
- turnId: session.activeTurnId,
2474
- })
2481
+ if (!session.activeTurnId) return { outcome: "no_active_turn" }
2482
+
2483
+ const turnId = session.activeTurnId
2484
+ try {
2485
+ await session.client.request("turn/interrupt", {
2486
+ threadId: session.threadId,
2487
+ turnId,
2488
+ })
2489
+ return { outcome: "interrupted", turnId, corrected: false }
2490
+ } catch (err) {
2491
+ const reason = err?.message || String(err)
2492
+ const mismatch = parseTurnMismatch(reason)
2493
+ if (!mismatch) return { outcome: "failed", reason }
2494
+
2495
+ const correctedTurnId = mismatch.found
2496
+ // 是正は再送前に行い、再送中の corrected turn の完了通知を正しく照合できるようにする。
2497
+ // 初回リクエスト中に状態が進んでいた場合は、その新しい状態を上書きしない。
2498
+ if (session.activeTurnId === turnId) {
2499
+ session.activeTurnId = correctedTurnId
2500
+ }
2501
+ try {
2502
+ await session.client.request("turn/interrupt", {
2503
+ threadId: session.threadId,
2504
+ turnId: correctedTurnId,
2505
+ })
2506
+ return { outcome: "interrupted", turnId: correctedTurnId, corrected: true }
2507
+ } catch (retryErr) {
2508
+ const retryReason = retryErr?.message || String(retryErr)
2509
+ const retryMismatch = parseTurnMismatch(retryReason)
2510
+ if (retryMismatch && session.activeTurnId === correctedTurnId) {
2511
+ session.activeTurnId = retryMismatch.found
2512
+ }
2513
+ return { outcome: "failed", reason: retryReason }
2514
+ }
2515
+ }
2475
2516
  }
2476
2517
 
2477
2518
  /** サーバー発の承認/入力要求への応答。result の shape は method ごとに呼び出し側が組み立てる。 */
package/src/main.mjs CHANGED
@@ -3194,6 +3194,30 @@ export function handleCodexChatInput(msg, ctx) {
3194
3194
  return p
3195
3195
  }
3196
3196
 
3197
+ export async function handleCodexInterrupt(msg, ctx) {
3198
+ let result
3199
+ if (!ctx.codexBridge) {
3200
+ result = { outcome: "failed", reason: "bridge_unavailable" }
3201
+ } else {
3202
+ try {
3203
+ result = await ctx.codexBridge.interrupt({ stream_id: msg.stream_id })
3204
+ if (result.outcome === "failed") {
3205
+ ctx.logger?.warn?.({ err: result.reason }, "codex.interrupt failed")
3206
+ }
3207
+ } catch (err) {
3208
+ const reason = err?.message || String(err)
3209
+ ctx.logger?.warn?.({ err: reason }, "codex.interrupt failed")
3210
+ result = { outcome: "failed", reason }
3211
+ }
3212
+ }
3213
+ ctx.client.send({
3214
+ type: "codex.interrupt.response",
3215
+ stream_id: msg.stream_id,
3216
+ request_id: msg.request_id ?? null,
3217
+ ...result,
3218
+ })
3219
+ }
3220
+
3197
3221
  /**
3198
3222
  * tmux.kill_session の処理 (症状3 根治)。要求された session を kill し、要求元へ
3199
3223
  * tmux.kill_session.result を返したうえで、kill できた各 session について
@@ -4333,9 +4357,10 @@ async function dispatch(msg, ctx) {
4333
4357
  })
4334
4358
  return
4335
4359
  case "codex.interrupt":
4336
- if (!ctx.codexBridge) return
4337
- ctx.codexBridge.interrupt({ stream_id: msg.stream_id }).catch((err) => {
4338
- ctx.logger?.warn?.({ err: err.message }, "codex.interrupt failed")
4360
+ // fire-and-forget だが async なので、送信失敗を未処理 rejection にしない
4361
+ // (handleBundleSyncRequest と同じ作法)。停止経路が例外で無言死しないための保険。
4362
+ handleCodexInterrupt(msg, ctx).catch((err) => {
4363
+ ctx.logger?.warn?.({ err: err?.message || String(err) }, "codex.interrupt handler failed")
4339
4364
  })
4340
4365
  return
4341
4366
  case "codex.detach":