@cocorograph/hub-agent 0.7.83 → 0.7.84

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.83",
3
+ "version": "0.7.84",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの tmux/pty を中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -65,22 +65,24 @@ const CHAT_RESIDENT_ENABLED = process.env.HUB_AGENT_CHAT_RESIDENT !== "0"
65
65
  * 1 セッション=1 query で system/init を初回 1 回のみにする。VS Code 拡張 / Claude Web
66
66
  * と同じ挙動)。query 起動時に options.resume で過去文脈を引き継ぐ。
67
67
  *
68
- * ⚠️ デフォルト無効化 (2026-06-02): 「眠っていたセッションを起こすと
69
- * 『Continue from where you left off.』→『No response requested.』や /clear の
70
- * local-command-caveat が合成ユーザーバブルとして表示され、処理中になり、実メッセージが
71
- * キュー待ちになる」回帰が確認されたため既定 OFF に戻す。
68
+ * ✅ 既定 ON へ復帰 (2026-07-21, T06118)。旧既定 OFF の理由だった「resume + 空 InputQueue」
69
+ * 窓 (下記) を **push 先行起動** (_startResidentTurnWithInput) で塞いだため、resumed セッション
70
+ * も常駐化して安全になった。既定 ON にする狙いは **背景エージェントの信頼性回復**:
71
+ * per-message (resume=1query) だとターン結果が返ると query プロセスが畳まれ、run_in_background
72
+ * で起動した子エージェントが親ターンより長生きして「previous Claude Code process exited」で
73
+ * 孤児化・停止していた (実害)。常駐化すれば query プロセスがターンをまたいで生存する。
72
74
  *
73
- * 根本原因: resident 経路 (sendMessage) は **空の InputQueue のまま query を起動してから
74
- * push** する設計のため、「resume + 入力がまだ空」の窓で SDK が中断/未完了セッションの
75
- * 自動継続ターン (Continue from where you left off) を差し込む。per-message 経路は
76
- * InputQueue に push→即 close してから起動するためこの窓が無く、自動継続は起きない
77
- * (0.6.4 で暴走を潰した設計)。よって resumed セッションは per-message 経路に戻す。
75
+ * 旧回帰の根本原因 (履歴): resident 経路 (sendMessage) が **空の InputQueue のまま query を
76
+ * 起動してから push** していたため、「resume + 入力がまだ空」の窓で SDK が中断/未完了
77
+ * セッションの自動継続ターン (Continue from where you left off) を差し込んでいた。per-message
78
+ * 経路は InputQueue に push→即 close してから起動するためこの窓が無い (0.6.4 で暴走を潰した
79
+ * 設計)。今回 resident の 3 起動サイトも「push してから起動」に揃え、初回ターンの ultracode は
80
+ * 起動時 options.settings で焼き込む (applyFlagSettings のための空窓も作らない)。
78
81
  *
79
- * env HUB_AGENT_CHAT_RESIDENT_RESUME="1" で個別に再有効化できる (空キュー窓を塞ぐ
80
- * 根治を入れたら既定 ON へ戻す候補)。新規セッションの常駐化は CHAT_RESIDENT_ENABLED 側で
81
- * 独立制御 (新規は resume しないため自動継続の窓が無く、既定 ON のまま安全)。 */
82
+ * env HUB_AGENT_CHAT_RESIDENT_RESUME="0" で個別に無効化 (従来の per-message へロールバック)
83
+ * できる。新規セッションの常駐化は CHAT_RESIDENT_ENABLED 側で独立制御。 */
82
84
  const CHAT_RESIDENT_RESUME_ENABLED =
83
- process.env.HUB_AGENT_CHAT_RESIDENT_RESUME === "1"
85
+ process.env.HUB_AGENT_CHAT_RESIDENT_RESUME !== "0"
84
86
 
85
87
  /** MCP control 専用 query のアイドル破棄時間 (2026-06-03)。MCP サーバーは Claude Code
86
88
  * 設定 (アカウント単位) で全セッション共有のため、チャットセッションとは独立した
@@ -435,6 +437,20 @@ class ClaudeStreamSession {
435
437
  * false に戻す。 */
436
438
  this._ultracodeCurrent = false
437
439
 
440
+ /** 空 InputQueue 窓封鎖 (T06118): fresh start する resume 常駐 query の「初回ターンの
441
+ * ultracode 目標」。_startResidentTurnWithInput が push 先行で積むメッセージと一緒に
442
+ * セットし、_runResidentQuery が起動時 options.settings へ焼き込んで消費する
443
+ * (起動前 applyFlagSettings のための空 InputQueue 窓を作らないため)。 */
444
+ this._pendingInitialUltracode = false
445
+
446
+ /** ABA/FIFO 直列化 (T06118 codex-review major): 起動済み query 経路の sendMessage が
447
+ * reconcile (applyFlagSettings) を await している間 true。この間に旧 query の generator が
448
+ * 自然終了しても、_runResidentQuery の finally は pending を drain して新 query を先行起動
449
+ * してはならない (in-flight のこの送信が後着 pending に追い越されて FIFO が逆転するため)。
450
+ * finally は query を畳むだけに留め、await 復帰後の送信が自分のメッセージを push 先行で
451
+ * fresh-start する。pending はその turn settle 後に FIFO で drain される。 */
452
+ this._residentSendInFlight = false
453
+
438
454
  /** @type {Map<string, {resolve: (decision: object) => void, tool_name: string, input: object}>}
439
455
  * permission 応答待ち。tool_name / input も保持し、reattach 時に未応答分を
440
456
  * onPermission で再送 (replay) して「裏で動いていたセッションを開いたら確認待ち
@@ -915,15 +931,6 @@ class ClaudeStreamSession {
915
931
 
916
932
  // 改修2+4: 常駐query対象セッション。
917
933
  if (this._residentEligible) {
918
- // B6: 死亡後の (再)起動経路。既に query を起動した実績があり (_residentStarted)、かつ
919
- // 現在 query が無い (_residentQuery=null = 異常終了済み) 場合、既存 _inputQueue は前 query で
920
- // consume し切った (generator return 済み) インスタンス。同じものを次の sdk.query に渡すと
921
- // _q/_wake の残留が新 generator と競合し得るため、push 前に必ず作り直す。
922
- if (this._residentStarted && !this._residentQuery) {
923
- this._inputQueue = new InputQueue()
924
- } else if (!this._inputQueue) {
925
- this._inputQueue = new InputQueue()
926
- }
927
934
  // 改修4 (A): ターンのシリアライズ。実行中ターン (busy) は InputQueue へ即 push せず
928
935
  // pending へ退避し、現ターンの result 後に 1 件ずつ drain する。前ターン未完了のまま
929
936
  // 次を push すると SDK streaming-input で割り込み扱いになり得るため (公式 interrupt 警告)。
@@ -937,31 +944,29 @@ class ClaudeStreamSession {
937
944
  this._emitQueueState()
938
945
  return { accepted: true }
939
946
  }
947
+ if (this._closed) return { accepted: false, reason: "queue_dead" }
940
948
  this._beginResidentTurn()
941
- // 改修4 (A): 死亡ガード。常駐 query が未起動 or (エラー等で) 終了済み (_residentQuery=null)
942
- // なら (再)起動する。_runResidentQuery は起動時 options.resume=this.sessionId で文脈を
943
- // 復元するため、途中死からの復活でも過去コンテキストは失われない。
944
- // ultracode のとき: query を先に起動 (空 InputQueue なので入力待ちでブロック) し、
945
- // applyFlagSettings を await してから push することで、設定適用前にターンが
946
- // 消費されるレースを防ぐ。
947
- if (!this._residentQuery) {
948
- this._residentStarted = true
949
- this._startResidentQuery()
950
- }
951
- await this._reconcileResidentUltracode(ultracode)
952
- // A-2 修正 (2026-07-05): 上の await 中に常駐 query の generator が自然終了している
953
- // ことがある (_residentQueryReturned が finally で true になる)。その場合 this._residentQuery
954
- // はまだ非 null (finally 未実行) に見えても、実体は既に誰も consume しない返却済み
955
- // generator であり、そのまま push すると消失する。fresh-start と同じ手順 (InputQueue の
956
- // 再生成 + 再起動) を踏んでから push することで、既存の resident 復活ガード
957
- // (B6 / 異常終了時の自動再起動) に合流させる。
958
- if (this._residentQueryReturned || !this._residentQuery) {
959
- this._inputQueue = new InputQueue()
960
- this._residentStarted = true
961
- this._startResidentQuery()
949
+ // 空 InputQueue 窓封鎖 (T06118): fresh start。常駐 query が未起動 (初回) / 異常終了済み
950
+ // (_residentQuery=null = 死亡ガード B6) / generator 返却済み (_residentQueryReturned) の
951
+ // いずれも「新しい query を起動する」ケース。従来は空の InputQueue で起動してから push
952
+ // していたため「resume + 空入力」の窓で SDK が自動継続ターン (Continue from where you
953
+ // left off) を差し込む回帰があった。実メッセージを積んでから起動する push 先行
954
+ // (_startResidentTurnWithInput、per-message と同じ) に統一してこの窓を無くす。
955
+ // 初回ターンの ultracode は起動時 options.settings に焼き込むため、起動前 reconcile
956
+ // (applyFlagSettings) のための空窓も不要。
957
+ if (!this._residentQuery || this._residentQueryReturned) {
958
+ this._startResidentTurnWithInput(toSDKUserMessage(prompt), ultracode)
959
+ return { accepted: true }
962
960
  }
963
- if (this._closed) return { accepted: false, reason: "queue_dead" }
964
- this._inputQueue.push(toSDKUserMessage(prompt))
961
+ // 起動済みの常駐 query へ次ターンを流す。init 済みで空窓が無いため、ultracode は走行中
962
+ // query への applyFlagSettings (reconcile) で適用してから push する。reconcile await 中の
963
+ // 旧 query 終了 × 後着 pending の ABA/FIFO 逆転は _reconcileThenDeliver が直列化する
964
+ // (drain 経路 _drainResidentPending と共通)。
965
+ const outcome = await this._reconcileThenDeliver(
966
+ toSDKUserMessage(prompt),
967
+ ultracode,
968
+ )
969
+ if (outcome === "closed") return { accepted: false, reason: "queue_dead" }
965
970
  return { accepted: true }
966
971
  }
967
972
 
@@ -1244,6 +1249,24 @@ class ClaudeStreamSession {
1244
1249
  }
1245
1250
  }
1246
1251
 
1252
+ /** 空 InputQueue 窓封鎖 (T06118): fresh start する resume 常駐 query を「入力を先に積んで
1253
+ * から起動」する (push 先行)。fresh な InputQueue を作り、実ユーザーメッセージを push して
1254
+ * から query を起動するため、「resume + 空入力」の窓で SDK が自動継続ターン (Continue from
1255
+ * where you left off) を差し込む余地が無い (per-message の push→close→起動と同じ原理)。
1256
+ * 初回ターンの ultracode は options.settings 経由で焼き込む (_runResidentQuery が
1257
+ * _pendingInitialUltracode を読む) ため、起動前 applyFlagSettings のための空窓も作らない。
1258
+ * ⚠️ _inputQueue は必ず作り直す (B6): 前 query で consume し切った (generator return 済み)
1259
+ * インスタンスを次の sdk.query に渡すと _q/_wake の残留が新 generator と競合し得るため。
1260
+ * @param {object} sdkUserMessage toSDKUserMessage() 済みの初回ユーザーメッセージ
1261
+ * @param {boolean} initialUltracode 初回ターンの ultracode 目標 */
1262
+ _startResidentTurnWithInput(sdkUserMessage, initialUltracode) {
1263
+ this._inputQueue = new InputQueue()
1264
+ this._inputQueue.push(sdkUserMessage)
1265
+ this._pendingInitialUltracode = initialUltracode === true
1266
+ this._residentStarted = true
1267
+ this._startResidentQuery()
1268
+ }
1269
+
1247
1270
  /** 改修4 (A): 常駐 query を resume 付きで (再)起動する。_runResidentQuery 起動時に
1248
1271
  * options.resume=this.sessionId で過去文脈を復元するため、途中死からの復活でも安全。 */
1249
1272
  _startResidentQuery() {
@@ -1282,6 +1305,45 @@ class ClaudeStreamSession {
1282
1305
  }
1283
1306
  }
1284
1307
 
1308
+ /** ABA/FIFO 直列化の共通デリバリ (T06118 codex-review major×2)。起動済みの走行中 query へ
1309
+ * 「ultracode を reconcile してから 1 メッセージを届ける」処理を、直接送信経路 (sendMessage) と
1310
+ * drain 経路 (_drainResidentPending) の両方から使う共通化ポイント。reconcile は control request
1311
+ * の往復で await するため、その窓の間に旧 query の generator が自然終了しうる (finally が走る)。
1312
+ * 所有権ガード (_residentSendInFlight) で await 中の finally に pending drain を見送らせ、await
1313
+ * 前後の query ハンドル比較 (ABA 検出) で交代/終了を捕捉して push 先行 fresh-start へ切り替える。
1314
+ *
1315
+ * 前提: 呼び出し側が既に _beginResidentTurn() 済みで、呼び出し時点の _residentQuery が走行中
1316
+ * (非 null・未返却) であること。
1317
+ * @param {object} sdkMsg toSDKUserMessage() 済みメッセージ
1318
+ * @param {boolean} ultracode このターンの ultracode 目標
1319
+ * @returns {Promise<"pushed"|"restarted"|"closed">} 届け方の結果
1320
+ */
1321
+ async _reconcileThenDeliver(sdkMsg, ultracode) {
1322
+ const activeQuery = this._residentQuery
1323
+ this._residentSendInFlight = true
1324
+ try {
1325
+ await this._reconcileResidentUltracode(ultracode)
1326
+ } finally {
1327
+ this._residentSendInFlight = false
1328
+ }
1329
+ if (this._closed) return "closed"
1330
+ if (
1331
+ this._residentQueryReturned ||
1332
+ !this._residentQuery ||
1333
+ this._residentQuery !== activeQuery
1334
+ ) {
1335
+ // ABA 検出: await 中に query が終了/交代した。finally は _residentSendInFlight を見て
1336
+ // pending drain を見送っているので、自分のメッセージを push 先行で fresh-start する
1337
+ // (pending はこの turn settle 後に _drainResidentPending が FIFO で流す)。await 中の
1338
+ // finally で _busy が false へ落ちているため、_beginResidentTurn で張り直してから起動する。
1339
+ this._beginResidentTurn()
1340
+ this._startResidentTurnWithInput(sdkMsg, ultracode)
1341
+ return "restarted"
1342
+ }
1343
+ this._inputQueue.push(sdkMsg)
1344
+ return "pushed"
1345
+ }
1346
+
1285
1347
  /** SDK-P0-3 (2026-07-10): resident の「ターン開始」を 1 箇所に集約する。
1286
1348
  * _busy を立てるのと同時に、そのターンの終端フラグ (_turnSettled) と進行フラグ
1287
1349
  * (_turnProgressed) を必ずリセットする。ターン開始は 3 箇所ある
@@ -1368,10 +1430,18 @@ class ClaudeStreamSession {
1368
1430
  // 次の drain がやり直せる状態に必ず回復する (例外は既存方針通り warn で握る)。
1369
1431
  this._beginResidentTurn()
1370
1432
  try {
1371
- // ultracode reconcile (0.6.28) も await で例外を投げうるので try の中に含める。
1372
- await this._reconcileResidentUltracode(next.ultracode === true)
1373
- this._inputQueue.push(toSDKUserMessage(next.text))
1374
- this._emitQueueState([next.text])
1433
+ // ABA/FIFO 直列化 (T06118 codex-review major×2): drain も reconcile を await するため、
1434
+ // その窓で旧 query が終了して finally が「その先の pending」を新 query へ先行投入すると、
1435
+ // 今 drain 中の next が追い越されて FIFO が逆転する。直接送信経路と同じ _reconcileThenDeliver
1436
+ // を通し、_residentSendInFlight による finally の drain 見送り + ABA 検出で next を先に届ける。
1437
+ const outcome = await this._reconcileThenDeliver(
1438
+ toSDKUserMessage(next.text),
1439
+ next.ultracode === true,
1440
+ )
1441
+ // codex-review minor: session が close 中 (outcome==="closed") は next を配送していない。
1442
+ // started 通知 (queue_state に [next.text] を載せる) を出すと未配送メッセージが「開始済み」に
1443
+ // 見えるため抑止する。closed は終端状態なので pending へ戻さず破棄でよい (session 撤去中)。
1444
+ if (outcome !== "closed") this._emitQueueState([next.text])
1375
1445
  } catch (err) {
1376
1446
  this._busy = false
1377
1447
  this._pendingMessages.unshift(next)
@@ -1421,6 +1491,20 @@ class ClaudeStreamSession {
1421
1491
  // 改修4: 起動時に sessionId (= resumeSessionId) があれば resume チェーンで文脈を引き継ぐ。
1422
1492
  // query 起動時点の値のみ有効 (起動後に確定/変化する session_id は同一 query 内で継続される)。
1423
1493
  if (this.sessionId) options.resume = this.sessionId
1494
+ // 空 InputQueue 窓封鎖 (T06118): fresh start の初回ターンが ultracode を要求していれば、
1495
+ // per-message と同じく起動時 options.settings に焼き込む。こうすることで「query を空 queue
1496
+ // で先に起動 → applyFlagSettings を await → push」という空窓経路を踏まずに済む。初回だけ
1497
+ // 焼き込み、_ultracodeCurrent を true に揃えて次ターンの reconcile が正しく差分判定できる
1498
+ // ようにする。読み取り後は消費 (次の (再)起動へ持ち越さない)。
1499
+ if (this._pendingInitialUltracode === true) {
1500
+ options.settings = {
1501
+ ...(options.settings || {}),
1502
+ ultracode: true,
1503
+ enableWorkflows: true,
1504
+ }
1505
+ this._ultracodeCurrent = true
1506
+ }
1507
+ this._pendingInitialUltracode = false
1424
1508
  const denyPending = (reason) => this._denyAllPendingPermissions(reason)
1425
1509
  try {
1426
1510
  const q = this.sdk.query({ prompt: this._inputQueue, options })
@@ -1514,27 +1598,31 @@ class ClaudeStreamSession {
1514
1598
  } finally {
1515
1599
  this.onReap?.()
1516
1600
  }
1517
- } else if (!this._closed && this._pendingMessages.length > 0) {
1601
+ } else if (
1602
+ !this._closed &&
1603
+ this._pendingMessages.length > 0 &&
1604
+ !this._residentSendInFlight
1605
+ ) {
1518
1606
  // 改修4 (A): 異常終了 (close 以外) で pending が残っていれば resume 付きで再起動し
1519
1607
  // 取りこぼしを防ぐ。_runResidentQuery が options.resume=this.sessionId で文脈を復元する。
1520
- // B6: 既存 _inputQueue の generator は既に return 済み (この query で消費し切った)。
1521
- // 同じインスタンスを次の sdk.query に再利用すると _q/_wake の残留状態が新しい
1522
- // generator と競合し得るため、再起動前に必ず作り直す。close() は this._inputQueue を
1523
- // close() するだけで参照を持ち越さないので、ここで差し替えても整合する。
1524
- this._inputQueue = new InputQueue()
1608
+ // 空 InputQueue 窓封鎖 (T06118): ここも fresh start なので push 先行で起動する
1609
+ // (_startResidentTurnWithInput が fresh な InputQueue を作り、実メッセージを積んでから
1610
+ // 起動する)。従来は「空 queue で起動 → reconcile await → push」で resume + 空入力の
1611
+ // 自動継続窓があった。異常再起動対象メッセージの ultracode は options.settings 経由で
1612
+ // 焼き込むため、フラグ取りこぼし (0.6.28 由来) も起きない。
1613
+ //
1614
+ // ABA/FIFO 直列化 (T06118 codex-review major): _residentSendInFlight の間 (起動済み
1615
+ // query 経路の sendMessage が reconcile を await 中) は、ここで pending を drain して
1616
+ // 新 query を先行起動してはならない。in-flight の送信が後着 pending に追い越されて FIFO が
1617
+ // 逆転するため。この場合は query を畳むだけに留め (上で _residentQuery=null 済み)、
1618
+ // await 復帰後の送信が自分のメッセージを push 先行で fresh-start する。pending はその
1619
+ // turn settle 後に _drainResidentPending が FIFO で流す。
1525
1620
  const next = this._pendingMessages.shift()
1526
1621
  this._beginResidentTurn()
1527
- this._residentStarted = true
1528
- // ultracode (0.6.29): query を先に起動して _residentQuery を確定させてから
1529
- // reconcile → push する (sendMessage の通常経路と同順)。push してから起動すると
1530
- // applyFlagSettings を発行する先 (_residentQuery) がまだ無く、異常再起動の対象
1531
- // メッセージだけ ultracode フラグが落ちる (0.6.28 由来の取りこぼし) ため、ここで揃える。
1532
- // _startResidentQuery → _runResidentQuery は最初の await (for await) より前に
1533
- // 同期で _residentQuery と _ultracodeCurrent=false を確定するので、直後の reconcile が
1534
- // 正しく差分判定できる。_reconcileResidentUltracode は内部で例外を握るため throw しない。
1535
- this._startResidentQuery()
1536
- await this._reconcileResidentUltracode(next.ultracode === true)
1537
- this._inputQueue.push(toSDKUserMessage(next.text))
1622
+ this._startResidentTurnWithInput(
1623
+ toSDKUserMessage(next.text),
1624
+ next.ultracode === true,
1625
+ )
1538
1626
  this._emitQueueState([next.text])
1539
1627
  }
1540
1628
  // マスク (2026-06-23): 常駐 query 終了後 (= CLI が jsonl を掴んでいない) のこの地点で、