@cocorograph/hub-agent 0.6.34 → 0.6.36

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.6.34",
3
+ "version": "0.6.36",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの tmux/pty を中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -40,8 +40,14 @@ function _now() {
40
40
  * チャットのアクティビティを記録する。status / context_pct は与えられた分だけ更新し、
41
41
  * turnAt (= 最終アクティビティ時刻 / ソート用) は呼ばれるたびに前進させる。
42
42
  *
43
+ * inputPending: チャットセッションが permission / AskUserQuestion の応答待ちで
44
+ * ターンをブロックしているか。サイドバーの「確認待ち」ドット & 通知の発火源にする
45
+ * (status とは独立に持つ。SessionStatus enum を汚さず、status は processing のまま
46
+ * "確認待ち" を別軸で表現するため)。permission 要求で true、次の assistant / result
47
+ * イベント (= ユーザーが応答してターンが再開/完了) で false に落とす。
48
+ *
43
49
  * @param {string} cwd セッションの作業ディレクトリ
44
- * @param {{ status?: string, contextPct?: number|null }} patch
50
+ * @param {{ status?: string, contextPct?: number|null, inputPending?: boolean }} patch
45
51
  */
46
52
  export function recordChatActivity(cwd, patch = {}) {
47
53
  if (!cwd || typeof cwd !== "string") return
@@ -49,6 +55,7 @@ export function recordChatActivity(cwd, patch = {}) {
49
55
  const prev = _byCwd.get(cwd) || {
50
56
  status: null,
51
57
  context_pct: null,
58
+ inputPending: false,
52
59
  turnAt: 0,
53
60
  updatedAtMs: 0,
54
61
  }
@@ -59,6 +66,9 @@ export function recordChatActivity(cwd, patch = {}) {
59
66
  if (typeof patch.contextPct === "number" && Number.isFinite(patch.contextPct)) {
60
67
  next.context_pct = Math.max(0, Math.min(100, patch.contextPct))
61
68
  }
69
+ if (typeof patch.inputPending === "boolean") {
70
+ next.inputPending = patch.inputPending
71
+ }
62
72
  _byCwd.set(cwd, next)
63
73
  }
64
74
 
@@ -67,7 +77,7 @@ export function recordChatActivity(cwd, patch = {}) {
67
77
  *
68
78
  * @param {string} cwd
69
79
  * @param {number} [now]
70
- * @returns {{ status: string|null, context_pct: number|null, turnAt: number, updatedAtMs: number } | null}
80
+ * @returns {{ status: string|null, context_pct: number|null, inputPending?: boolean, turnAt: number, updatedAtMs: number } | null}
71
81
  */
72
82
  export function getChatSignal(cwd, now = _now()) {
73
83
  if (!cwd) return null
@@ -38,6 +38,11 @@ export function normalizeHistoryEvent(obj) {
38
38
  if (obj.message !== undefined) event.message = obj.message
39
39
  if (obj.subtype !== undefined) event.subtype = obj.subtype
40
40
  if (obj.uuid !== undefined) event.uuid = obj.uuid
41
+ // jsonl 各行の ISO8601 タイムスタンプ。Browser 側でバブル上の時刻表示・日付区切りに
42
+ // 使う。履歴 hydrate と live watch の両経路がこの関数を通るため、ここ 1 箇所で拾えば
43
+ // 両方に時刻が乗る (純粋なライブ SDK stream には timestamp が無く、Browser が受信時刻で
44
+ // 補完する)。
45
+ if (obj.timestamp !== undefined) event.timestamp = obj.timestamp
41
46
  if (obj.session_id !== undefined) event.session_id = obj.session_id
42
47
  else if (obj.sessionId !== undefined) event.session_id = obj.sessionId
43
48
  if (obj.model !== undefined) event.model = obj.model
@@ -37,15 +37,23 @@ const CHAT_RESIDENT_ENABLED = process.env.HUB_AGENT_CHAT_RESIDENT !== "0"
37
37
  /** 改修4 (2026-05-29): resume セッションも常駐query化する (init を毎ターン送らず、
38
38
  * 1 セッション=1 query で system/init を初回 1 回のみにする。VS Code 拡張 / Claude Web
39
39
  * と同じ挙動)。query 起動時に options.resume で過去文脈を引き継ぐ。
40
- * env HUB_AGENT_CHAT_RESIDENT_RESUME="0" で従来の per-message (毎ターン init) に
41
- * 個別ロールバック可能 (新規セッションの常駐化は CHAT_RESIDENT_ENABLED 側で独立制御)。
42
- * デフォルト有効。
43
- * 注意: 過去に「resume + 常駐」で『Continue from where you left off』暴走が疑われたが、
44
- * 原因は continue オプション (直近会話の自動継続) の誤用と Hub 不調の重なりと推定。
45
- * 本実装では continue は使わず resume (明示 session 指定) のみを用い、初回 input は
46
- * ユーザーの実メッセージのみとすることで自動継続の暴走を回避する。 */
40
+ *
41
+ * ⚠️ デフォルト無効化 (2026-06-02): 「眠っていたセッションを起こすと
42
+ * 『Continue from where you left off.』→『No response requested.』や /clear の
43
+ * local-command-caveat が合成ユーザーバブルとして表示され、処理中になり、実メッセージが
44
+ * キュー待ちになる」回帰が確認されたため既定 OFF に戻す。
45
+ *
46
+ * 根本原因: resident 経路 (sendMessage) は **空の InputQueue のまま query を起動してから
47
+ * push** する設計のため、「resume + 入力がまだ空」の窓で SDK が中断/未完了セッションの
48
+ * 自動継続ターン (Continue from where you left off) を差し込む。per-message 経路は
49
+ * InputQueue に push→即 close してから起動するためこの窓が無く、自動継続は起きない
50
+ * (0.6.4 で暴走を潰した設計)。よって resumed セッションは per-message 経路に戻す。
51
+ *
52
+ * env HUB_AGENT_CHAT_RESIDENT_RESUME="1" で個別に再有効化できる (空キュー窓を塞ぐ
53
+ * 根治を入れたら既定 ON へ戻す候補)。新規セッションの常駐化は CHAT_RESIDENT_ENABLED 側で
54
+ * 独立制御 (新規は resume しないため自動継続の窓が無く、既定 ON のまま安全)。 */
47
55
  const CHAT_RESIDENT_RESUME_ENABLED =
48
- process.env.HUB_AGENT_CHAT_RESIDENT_RESUME !== "0"
56
+ process.env.HUB_AGENT_CHAT_RESIDENT_RESUME === "1"
49
57
 
50
58
  /** 多端末共有セッション (2026-05-30)。同じ Claude セッション (session_id) を複数端末で
51
59
  * 同時に開いて、どの端末でもライブ表示・入力・許可応答できるようにする。env
@@ -229,7 +237,10 @@ class ClaudeStreamSession {
229
237
  * false に戻す。 */
230
238
  this._ultracodeCurrent = false
231
239
 
232
- /** @type {Map<string, {resolve: (decision: object) => void}>} permission 応答待ち */
240
+ /** @type {Map<string, {resolve: (decision: object) => void, tool_name: string, input: object}>}
241
+ * permission 応答待ち。tool_name / input も保持し、reattach 時に未応答分を
242
+ * onPermission で再送 (replay) して「裏で動いていたセッションを開いたら確認待ち
243
+ * カードが出ずに固まる」不具合を解消する (固まりの直接原因の修正)。 */
233
244
  this._permissionResolvers = new Map()
234
245
  /** 現在ターン実行中か (多重 query 防止) */
235
246
  this._busy = false
@@ -290,7 +301,11 @@ class ClaudeStreamSession {
290
301
  if (!this.onPermission) return { behavior: "allow", updatedInput: input }
291
302
  const request_id = randomUUID()
292
303
  return await new Promise((resolve) => {
293
- this._permissionResolvers.set(request_id, { resolve })
304
+ this._permissionResolvers.set(request_id, {
305
+ resolve,
306
+ tool_name: toolName,
307
+ input,
308
+ })
294
309
  try {
295
310
  this.onPermission({ tool_name: toolName, input, request_id })
296
311
  } catch (err) {
@@ -344,6 +359,27 @@ class ClaudeStreamSession {
344
359
  // started=[] なのでバブル昇格は起きずチップ更新のみ (冪等)。onEvent は新しい
345
360
  // stream_id 宛の stream_group relay で確実に届き、session_group fanout で他端末にも届く。
346
361
  this._emitQueueState([], { force: true })
362
+ // 固まり解消 (2026-06-02): 未応答 permission を再送する。permission は in-flight な
363
+ // callback で jsonl hydrate では復元されないため、別セッションを表示中に裏で発生した
364
+ // 確認待ちは取りこぼされ、サーバー側 Promise が永久 pending になりセッションが固まる。
365
+ // 再アタッチした端末へ現在の未応答分を再 emit し、確認待ちカードを復元させる
366
+ // (browser 側は request_id で重複排除するので、既に表示済みの端末でも二重化しない)。
367
+ this._replayPendingPermissions()
368
+ }
369
+
370
+ /** 未応答 permission を onPermission 経由で再送する (reattach から呼ぶ)。 */
371
+ _replayPendingPermissions() {
372
+ if (!this.onPermission || this._permissionResolvers.size === 0) return
373
+ for (const [request_id, r] of this._permissionResolvers) {
374
+ try {
375
+ this.onPermission({ tool_name: r.tool_name, input: r.input, request_id })
376
+ } catch (err) {
377
+ this.logger?.warn(
378
+ { err: err.message, stream_id: this.stream_id, request_id },
379
+ "replay permission failed",
380
+ )
381
+ }
382
+ }
347
383
  }
348
384
 
349
385
  /** B11: 端末の最終アクティビティ時刻を更新する (死端末 GC の生存判定に使う)。 */
@@ -1135,6 +1171,7 @@ export class ClaudeStreamBridge extends EventEmitter {
1135
1171
  this.emit("permission", {
1136
1172
  stream_id: session.stream_id,
1137
1173
  session_id: session.sessionId,
1174
+ cwd: session.cwd,
1138
1175
  request_id,
1139
1176
  tool_name,
1140
1177
  input,
package/src/main.mjs CHANGED
@@ -267,23 +267,39 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
267
267
  // TUI を出さず、capture-pane スクレイプも bundle hook も発火しないための補完。
268
268
  // - assistant (生成中) → processing + usage から context%
269
269
  // - result (ターン完了/入力待ち) → waiting
270
+ // assistant / result はターンの進行/完了 = permission 応答後に必ず流れるため、
271
+ // ここで inputPending を落として「確認待ち」ドット/通知を自己解消させる。
270
272
  if (event?.type === "assistant") {
271
273
  const pct = event.message?.usage ? contextPctFromUsage(event.message.usage) : null
272
274
  try {
273
- recordChatActivity(cwd, { status: "processing", contextPct: pct })
275
+ recordChatActivity(cwd, {
276
+ status: "processing",
277
+ contextPct: pct,
278
+ inputPending: false,
279
+ })
274
280
  } catch {
275
281
  /* ignore */
276
282
  }
277
283
  } else if (event?.type === "result") {
278
284
  try {
279
- recordChatActivity(cwd, { status: "waiting" })
285
+ recordChatActivity(cwd, { status: "waiting", inputPending: false })
280
286
  } catch {
281
287
  /* ignore */
282
288
  }
283
289
  }
284
290
  client.send({ type: "claude.event", stream_id, session_id, event })
285
291
  })
286
- claudeBridge.on("permission", ({ stream_id, session_id, request_id, tool_name, input }) => {
292
+ claudeBridge.on("permission", ({ stream_id, session_id, cwd, request_id, tool_name, input }) => {
293
+ // 確認待ち信号 (2026-06-02): cwd キーで inputPending を立て、サイドバーの
294
+ // 「確認待ち」ドット & 通知の発火源にする。次の assistant/result で落ちる。
295
+ // これにより「裏で動いているセッションがどれか分からない」発見性の問題を解消する。
296
+ if (cwd) {
297
+ try {
298
+ recordChatActivity(cwd, { inputPending: true })
299
+ } catch {
300
+ /* ignore */
301
+ }
302
+ }
287
303
  // 多端末共有: session_id を載せて backend が session group へ broadcast できるように
288
304
  // する (全端末に許可ダイアログ → 先着 1 件採用)。
289
305
  client.send({
@@ -628,7 +644,14 @@ function startStateLoop({ client, plugins, logger, intervalMs, claudeBridge }) {
628
644
  const prevTurnAt = lastTurnAtByName.get(s.session_name) || 0
629
645
  if (chat.turnAt > prevTurnAt) {
630
646
  lastTurnAtByName.set(s.session_name, chat.turnAt)
631
- const ev = chat.status === "processing" ? "prompt_submit" : "stop"
647
+ // 確認待ち (inputPending) は専用 event "input_required" を書き、サイドバーの
648
+ // 「確認待ち」ドット & 通知に橋渡しする。それ以外は従来通り processing →
649
+ // prompt_submit / それ以外 → stop。
650
+ const ev = chat.inputPending
651
+ ? "input_required"
652
+ : chat.status === "processing"
653
+ ? "prompt_submit"
654
+ : "stop"
632
655
  writeSessionEventFile(s.session_name, ev, chat.turnAt).catch(() => {})
633
656
  }
634
657
  }