@cocorograph/hub-agent 0.7.49 → 0.7.50

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.49",
3
+ "version": "0.7.50",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの tmux/pty を中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -209,6 +209,24 @@ function extractPromptText(message) {
209
209
  return ""
210
210
  }
211
211
 
212
+ /**
213
+ * SDK-P0-3 (2026-07-10): SDK の権威的ターン終了シグナル。
214
+ * sdk.d.ts (SDKSessionStateChangedMessage) いわく
215
+ * "'idle' fires after heldBackResult flushes and the bg-agent do-while exits
216
+ * — authoritative turn-over signal."
217
+ * つまり result が流れるターンでは `result` → `idle` の順で届き、result が届かない
218
+ * ターン (held back のまま終わる等) でも idle は届く。frontend は SDK-P0-1 で既に
219
+ * idle をターン終端として扱っているが、agent 側 (resident query) は result しか見て
220
+ * おらず _busy が固着していた。その非対称を解消するための判定。
221
+ */
222
+ function isSessionIdleEvent(msg) {
223
+ return (
224
+ msg?.type === "system" &&
225
+ msg?.subtype === "session_state_changed" &&
226
+ msg?.state === "idle"
227
+ )
228
+ }
229
+
212
230
  /**
213
231
  * 1 stream に対応する Claude セッション (per-message query モデル)。
214
232
  */
@@ -331,6 +349,18 @@ class ClaudeStreamSession {
331
349
  this._permissionResolvers = new Map()
332
350
  /** 現在ターン実行中か (多重 query 防止) */
333
351
  this._busy = false
352
+ /** SDK-P0-3 (2026-07-10): 現ターンが既に終端処理済みか (result / idle / abort の
353
+ * いずれか 1 回だけ _busy=false + drain させるための冪等ガード)。resident query は
354
+ * 1 ターンにつき `result` と `session_state_changed:idle` の**両方**が流れうるため、
355
+ * 素朴に両方で drain すると二重 drain (FIFO 崩れ) を起こす。ターン開始時
356
+ * (_beginResidentTurn) に false へ戻す。 */
357
+ this._turnSettled = false
358
+ /** SDK-P0-3: 現ターンが 1 件でもストリームイベントを出したか。
359
+ * 「前ターンの遅延 idle が、drain で始まったばかりの次ターンを誤終了させる」レースの
360
+ * ガードに使う。generator のイベント順序は保証される (turn N の idle は turn N+1 の
361
+ * 最初のイベントより必ず先に届く) ため、`idle 到着時に現ターンがまだ何も出していない`
362
+ * = その idle は前ターンのもの、と判定できる。ターン開始時に false へ戻す。 */
363
+ this._turnProgressed = false
334
364
  /** detach 済みフラグ (新規ターン受付停止) */
335
365
  this._closed = false
336
366
  /** 実行中ターンの完走を待ってからクローズする予約フラグ (graceful detach 用)。
@@ -721,7 +751,7 @@ class ClaudeStreamSession {
721
751
  this._emitQueueState()
722
752
  return { accepted: true }
723
753
  }
724
- this._busy = true
754
+ this._beginResidentTurn()
725
755
  // 改修4 (A): 死亡ガード。常駐 query が未起動 or (エラー等で) 終了済み (_residentQuery=null)
726
756
  // なら (再)起動する。_runResidentQuery は起動時 options.resume=this.sessionId で文脈を
727
757
  // 復元するため、途中死からの復活でも過去コンテキストは失われない。
@@ -1067,6 +1097,40 @@ class ClaudeStreamSession {
1067
1097
  }
1068
1098
  }
1069
1099
 
1100
+ /** SDK-P0-3 (2026-07-10): resident の「ターン開始」を 1 箇所に集約する。
1101
+ * _busy を立てるのと同時に、そのターンの終端フラグ (_turnSettled) と進行フラグ
1102
+ * (_turnProgressed) を必ずリセットする。ターン開始は 3 箇所ある
1103
+ * (sendMessage の直接 push / _drainResidentPending / _runResidentQuery finally の
1104
+ * 異常再起動 push) ので、リセット漏れを構造的に防ぐためのヘルパー。 */
1105
+ _beginResidentTurn() {
1106
+ this._busy = true
1107
+ this._turnSettled = false
1108
+ this._turnProgressed = false
1109
+ }
1110
+
1111
+ /** SDK-P0-3 (2026-07-10): resident の「ターン終端」を 1 箇所に集約する (result / idle 共通)。
1112
+ * 同一ターンで 2 回以上呼ばれても最初の 1 回しか作用しない (_turnSettled ガード)。
1113
+ * これにより `result` → `idle` の順で両方届く通常ケースでも drain は 1 回だけになる。
1114
+ * @param {string} reason "result" | "idle" — ログ用
1115
+ * @param {(msg: string) => void} [denyPending] 未解決 permission を閉じるクロージャ
1116
+ */
1117
+ _settleResidentTurn(reason, denyPending) {
1118
+ if (this._turnSettled) return
1119
+ this._turnSettled = true
1120
+ denyPending?.("turn ended")
1121
+ this._busy = false
1122
+ // 改修4 (A): シリアライズした pending があれば次の 1 件を InputQueue へ流す。
1123
+ // ultracode (0.6.28): _drainResidentPending は applyFlagSettings を await するため
1124
+ // async。for await ループ内からは fire-and-forget で呼ぶ (最初の await より前に
1125
+ // _beginResidentTurn が同期実行されるので、以降の sendMessage は busy を正しく観測する)。
1126
+ this._drainResidentPending().catch((err) =>
1127
+ this.logger?.warn(
1128
+ { stream_id: this.stream_id, err: err?.message, reason },
1129
+ "drainResidentPending threw",
1130
+ ),
1131
+ )
1132
+ }
1133
+
1070
1134
  /** 改修4 (A): ターン完了時に pending の先頭 1 件を InputQueue へ流す (ターンのシリアライズ)。
1071
1135
  * queue_state を更新して送信待ちチップを drain させる (frontend がバブル昇格する)。
1072
1136
  * ultracode (0.6.28): 次ターンの目標 ultracode 状態へ reconcile してから push する
@@ -1082,7 +1146,7 @@ class ClaudeStreamSession {
1082
1146
  // _busy=true のまま残り、以降の drain が全て弾かれて pending が永久に積み上がる
1083
1147
  // デッドロックになる。失敗時は取り出したメッセージを先頭へ戻し _busy を解除して、
1084
1148
  // 次の drain がやり直せる状態に必ず回復する (例外は既存方針通り warn で握る)。
1085
- this._busy = true
1149
+ this._beginResidentTurn()
1086
1150
  try {
1087
1151
  // ultracode reconcile (0.6.28) も await で例外を投げうるので try の中に含める。
1088
1152
  await this._reconcileResidentUltracode(next.ultracode === true)
@@ -1100,8 +1164,10 @@ class ClaudeStreamSession {
1100
1164
 
1101
1165
  /** 改修2+4: 常駐 query を 1 回だけ起動し、streaming input キューから複数ターンを処理する。
1102
1166
  * system/init は最初の 1 回のみ (毎ターン init = 文脈/キャッシュ温め直しを解消)。各ターンは
1103
- * result で _busy=false にするが query は継続し次 input を待つ。close (inputQueue.close) で
1104
- * generator が終わり query が完走する。
1167
+ * 終端シグナル (result / session_state_changed:idle) で _busy=false にするが query は継続し
1168
+ * 次 input を待つ。close (inputQueue.close) で generator が終わり query が完走する。
1169
+ * SDK-P0-3 (2026-07-10): 終端を result だけに頼ると、SDK が idle でターンを終えたときに
1170
+ * _busy が固着し、以降の送信が pending へ永久滞留する (T06052)。
1105
1171
  * 改修4: resume セッションも対象。query 起動時 options.resume に過去 session_id を渡して
1106
1172
  * 文脈を引き継ぐ (init は初回 1 回のみ)。continue は使わない (自動継続の暴走回避)。初回 input は
1107
1173
  * sendMessage が積むユーザーの実メッセージなので「Continue from where you left off」は発生しない。
@@ -1153,17 +1219,34 @@ class ClaudeStreamSession {
1153
1219
  if (msg?.type === "result") {
1154
1220
  if (typeof msg.session_id === "string") this.sessionId = msg.session_id
1155
1221
  // ターン完了: 未解決 permission を閉じ、次 input 受付へ (query は継続)。
1156
- denyPending("turn ended")
1157
- this._busy = false
1158
- // 改修4 (A): シリアライズした pending があれば次の 1 件を InputQueue へ流す。
1159
- // ultracode (0.6.28): _drainResidentPending は applyFlagSettings を await するため
1160
- // async。result ハンドラ (for await ループ内) からは fire-and-forget で呼ぶ。
1161
- this._drainResidentPending().catch((err) =>
1162
- this.logger?.warn(
1163
- { stream_id: this.stream_id, err: err?.message },
1164
- "drainResidentPending threw",
1165
- ),
1166
- )
1222
+ this._settleResidentTurn("result", denyPending)
1223
+ } else if (isSessionIdleEvent(msg)) {
1224
+ // SDK-P0-3 (2026-07-10): result が届かずに idle でターンが終わるケースの救済。
1225
+ // これを見ていなかったため _busy が true で固着し、以降の送信が
1226
+ // _pendingMessages に退避されたまま (drain 契機の result が永久に来ないので)
1227
+ // 永久滞留していた ("resident busy, message queued" が出続ける / リロードで消える)。
1228
+ //
1229
+ // ガード: 「前ターンの遅延 idle」で新ターンを誤終了させない。result で settle →
1230
+ // drain → 次ターン開始 (_busy=true) の直後に、同じターンの idle が続けて届く
1231
+ // (SDK は result → idle の順に流す) ため、素朴に _busy=false にすると新ターンを
1232
+ // 撃ち抜き二重 drain / FIFO 崩れを起こす。generator のイベント順序は保証されるので
1233
+ // 「現ターンがまだ 1 件もイベントを出していない (_turnProgressed=false)」なら
1234
+ // その idle は前ターンのものと確定できる。
1235
+ if (this._busy && this._turnProgressed) {
1236
+ this._settleResidentTurn("idle", denyPending)
1237
+ // per-message の RC-4 と対称に、チャット信号 (サイドバードット) を
1238
+ // processing→waiting へ落とす。result 経由なら main.mjs が result を見て
1239
+ // waiting にするが、idle 単独ではその契機が無く 90s TTL まで固着するため。
1240
+ try {
1241
+ this.onTurnSettled?.()
1242
+ } catch {
1243
+ /* ignore */
1244
+ }
1245
+ }
1246
+ } else {
1247
+ // 現ターンが「進行した」証跡。init / assistant / stream_event / user、および
1248
+ // session_state_changed の running / requires_action もここに含む。
1249
+ this._turnProgressed = true
1167
1250
  }
1168
1251
  try {
1169
1252
  this._emit(msg)
@@ -1207,7 +1290,7 @@ class ClaudeStreamSession {
1207
1290
  // close() するだけで参照を持ち越さないので、ここで差し替えても整合する。
1208
1291
  this._inputQueue = new InputQueue()
1209
1292
  const next = this._pendingMessages.shift()
1210
- this._busy = true
1293
+ this._beginResidentTurn()
1211
1294
  this._residentStarted = true
1212
1295
  // ultracode (0.6.29): query を先に起動して _residentQuery を確定させてから
1213
1296
  // reconcile → push する (sendMessage の通常経路と同順)。push してから起動すると
@@ -1237,6 +1320,11 @@ class ClaudeStreamSession {
1237
1320
  } catch {
1238
1321
  /* ignore */
1239
1322
  }
1323
+ // SDK-P0-3 (2026-07-10): 中断も「このターンの終端」なので settled を立てる。
1324
+ // interrupt() 後に SDK が session_state_changed:idle を流しても、_turnSettled=true
1325
+ // (pending 無しで drain が早期 return するケース) か _turnProgressed=false
1326
+ // (drain が次ターンを開始したケース) のどちらかで必ず無視される。
1327
+ this._turnSettled = true
1240
1328
  this._busy = false
1241
1329
  // SDK-P0-2 (2026-07-07): resident 経路のターン終端イベント非対称の解消。
1242
1330
  // per-message の abort (_runPerMessage finally) は合成 result (aborted_by_user) を
@@ -1248,8 +1336,9 @@ class ClaudeStreamSession {
1248
1336
  // - 実行中ターンがあった (wasBusy) ときだけ emit する (未走行での空 abort に
1249
1337
  // 「中断しました」フッターを出さない)。
1250
1338
  // - close()/reap 経由の abort (_closed=true) では browser は既に離脱しているため抑止する。
1251
- // - drain タイミングは変更しない (既存の同期 drain を維持し、result+idle 二重 drain や
1252
- // 中断ターンの遅延 idle が新ターンを誤終了させる新種レースを持ち込まない)。
1339
+ // - drain タイミングは変更しない (既存の同期 drain を維持する)。result+idle の二重 drain と
1340
+ // 中断ターンの遅延 idle による新ターン誤終了は、SDK-P0-3 の _turnSettled /
1341
+ // _turnProgressed ガードで潰してある (上の _turnSettled=true もその一部)。
1253
1342
  // - 合成 result の uuid は重複排除 (isDuplicateEvent の result 署名判定) 回避のため必須。
1254
1343
  if (wasBusy && !this._closed) {
1255
1344
  try {