@cocorograph/hub-agent 0.8.99 → 0.8.100

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.99",
3
+ "version": "0.8.100",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの Claude / Codex チャットと tmux セッションを中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -427,6 +427,9 @@ function isSessionRunningEvent(msg) {
427
427
  * 正しく終端として扱う (ガードが固着して _busy が永久 true になる = T06052 の再発を防ぐ安全弁)。 */
428
428
  const STALE_IDLE_GUARD_MS = 5000
429
429
 
430
+ // sibling CLI の既定待ち 10 秒より短く受理状態を待つ。超過は失敗と断定しない。
431
+ export const SIBLING_ACCEPT_WAIT_MS = 1000
432
+
430
433
  /** per-message の title control request を待って InputQueue を閉じる上限。 */
431
434
  const SESSION_TITLE_CLOSE_WAIT_MS = 15_000
432
435
 
@@ -1408,7 +1411,7 @@ export class ClaudeStreamSession {
1408
1411
  "resident busy, message queued",
1409
1412
  )
1410
1413
  this._emitQueueState()
1411
- return { accepted: true }
1414
+ return { accepted: true, queued: true }
1412
1415
  }
1413
1416
  if (this._closed) return { accepted: false, reason: "queue_dead" }
1414
1417
  this._beginResidentTurn(prompt)
@@ -1460,7 +1463,7 @@ export class ClaudeStreamSession {
1460
1463
  "claude busy, message queued",
1461
1464
  )
1462
1465
  this._emitQueueState()
1463
- return { accepted: true }
1466
+ return { accepted: true, queued: true }
1464
1467
  }
1465
1468
  // Phase 2 (major#1): per-message idle 直接実行も started を出さないため、注入なら source 付きで
1466
1469
  // 昇格バブルを作るよう started を先出しする (通常メッセージは no-op)。
@@ -3695,7 +3698,7 @@ export class ClaudeStreamBridge extends EventEmitter {
3695
3698
  *
3696
3699
  * @param {string} cwd
3697
3700
  * @param {string} text
3698
- * @returns {{status:"delivered", session:object} | {status:"not_found"} | {status:"ambiguous", candidates:object[]}}
3701
+ * @returns {object | Promise<object>} 配送先なし/曖昧は同期、配送先確定後は受理結果を待つ。
3699
3702
  */
3700
3703
  sendToCwd(cwd, text, source) {
3701
3704
  if (!cwd || !text) return { status: "not_found" }
@@ -3712,32 +3715,7 @@ export class ClaudeStreamBridge extends EventEmitter {
3712
3715
  }
3713
3716
  }
3714
3717
  const { session, attached: att } = tier[0]
3715
- // 連携カード種別 (Phase 2): 配送先が確定した後に記録する (ambiguous/not_found では記録しない)。
3716
- // handle を控え、失敗時にその entry だけを除去する (round2: text 除去は並行同一本文で誤消費)。
3717
- const injectHandle = session._recordInjectSource?.(text, source)
3718
- session
3719
- .sendMessage({ role: "user", content: text })
3720
- .then((r) => {
3721
- // Phase 2 (major#3): sendMessage は throw せず {accepted:false} で失敗を返す経路がある
3722
- // (queue_dead / empty_message / pre-canceled)。この場合は echo/started は来ないので
3723
- // stale な live source entry を掃除し、**永続化もしない** (persist round1 major#1: 失敗を
3724
- // 永続化すると 30 日 stale が残り後続の同文 human を誤カード化する)。
3725
- if (r && r.accepted === false) {
3726
- session._removeInjectSource?.(injectHandle)
3727
- return
3728
- }
3729
- // 受理された (queued/started) ときだけ hydrate 用に永続化する (best-effort、live と独立)。
3730
- recordInjectSourcePersistent(cwd, text, source).catch(() => {})
3731
- })
3732
- .catch((err) => {
3733
- // 配送失敗 (throw) でも echo は来ないので stale な live entry を掃除する (永続化はしない)。
3734
- session._removeInjectSource?.(injectHandle)
3735
- this.logger?.error(
3736
- { cwd, err: err?.message },
3737
- "sendToCwd: sendMessage threw unexpectedly",
3738
- )
3739
- })
3740
- return { status: "delivered", session: this._describeCandidate(session, att) }
3718
+ return this._sendSiblingMessage(cwd, text, source, session, att)
3741
3719
  }
3742
3720
 
3743
3721
  /**
@@ -3761,7 +3739,7 @@ export class ClaudeStreamBridge extends EventEmitter {
3761
3739
  * @param {string} cwd 呼び出し元がトークン検証した cwd (この cwd のセッションにのみ注入)
3762
3740
  * @param {string} sessionId 宛先 session_id (describeLiveSessions の session_id)
3763
3741
  * @param {string} text
3764
- * @returns {{status: "delivered", session: object} | {status: "not_found"} | {status: "ambiguous", candidates: object[]}}
3742
+ * @returns {object | Promise<object>} 配送先なし/曖昧は同期、配送先確定後は受理結果を待つ。
3765
3743
  */
3766
3744
  sendToStream(cwd, sessionId, text, source) {
3767
3745
  if (!cwd || !sessionId || !text) return { status: "not_found" }
@@ -3780,28 +3758,53 @@ export class ClaudeStreamBridge extends EventEmitter {
3780
3758
  }
3781
3759
  }
3782
3760
  const { session, attached } = matches[0]
3783
- // 連携カード種別 (Phase 2): 宛先確定後に記録 (not_found/ambiguous では記録しない)。
3761
+ return this._sendSiblingMessage(cwd, text, source, session, attached)
3762
+ }
3763
+
3764
+ /** 受理結果だけを有限時間待つ。遅れて確定した結果も source の後始末には使う。 */
3765
+ async _sendSiblingMessage(cwd, text, source, session, attached) {
3766
+ const target = this._describeCandidate(session, attached)
3767
+ const delivered = { status: "delivered", session: target }
3784
3768
  const injectHandle = session._recordInjectSource?.(text, source)
3785
- session
3786
- .sendMessage({ role: "user", content: text })
3787
- .then((r) => {
3788
- // Phase 2 (major#3 / persist major#1): {accepted:false} は echo/started 来ないので live を
3789
- // 掃除し永続化もしない (失敗を永続化すると 30 日 stale で後続同文 human を誤カード化)。
3790
- if (r && r.accepted === false) {
3769
+ let timer
3770
+ const outcome = (async () => {
3771
+ try {
3772
+ const result = await session.sendMessage({ role: "user", content: text })
3773
+ if (result?.accepted === false) {
3791
3774
  session._removeInjectSource?.(injectHandle)
3792
- return
3775
+ return { status: "delivery_failed", session: target, reason: result.reason }
3793
3776
  }
3794
- // 受理時のみ hydrate 用に永続化 (best-effort)。
3777
+ // 受理時のみ永続化する。timeout 後に受理が確定した場合も維持する。
3795
3778
  recordInjectSourcePersistent(cwd, text, source).catch(() => {})
3796
- })
3797
- .catch((err) => {
3779
+ if (result?.queued === true) {
3780
+ return { status: "delivery_queued", session: target, reason: "queued_behind_active_turn" }
3781
+ }
3782
+ return delivered
3783
+ } catch (err) {
3798
3784
  session._removeInjectSource?.(injectHandle)
3799
3785
  this.logger?.error(
3800
- { session_id: sessionId, err: err?.message },
3801
- "sendToStream: sendMessage threw unexpectedly",
3786
+ { cwd, session_id: session.sessionId, err: err?.message },
3787
+ "sibling sendMessage threw unexpectedly; delivery remains unconfirmed",
3802
3788
  )
3803
- })
3804
- return { status: "delivered", session: this._describeCandidate(session, attached) }
3789
+ return delivered
3790
+ }
3791
+ })()
3792
+ try {
3793
+ return await Promise.race([
3794
+ outcome,
3795
+ new Promise((resolve) => {
3796
+ timer = setTimeout(() => {
3797
+ this.logger?.warn(
3798
+ { cwd, session_id: session.sessionId, timeout_ms: SIBLING_ACCEPT_WAIT_MS },
3799
+ "sibling sendMessage acceptance timed out; delivery remains unconfirmed",
3800
+ )
3801
+ resolve(delivered)
3802
+ }, SIBLING_ACCEPT_WAIT_MS)
3803
+ }),
3804
+ ])
3805
+ } finally {
3806
+ clearTimeout(timer)
3807
+ }
3805
3808
  }
3806
3809
 
3807
3810
  /**