@cocorograph/hub-agent 0.7.6 → 0.7.7

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.6",
3
+ "version": "0.7.7",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの tmux/pty を中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
package/src/main.mjs CHANGED
@@ -39,6 +39,7 @@ import {
39
39
  detectSessionState,
40
40
  invalidateSessionCache,
41
41
  listSessionStates,
42
+ resolveBindSnapshotStatus,
42
43
  StallTracker,
43
44
  } from "./state.mjs"
44
45
  import {
@@ -2335,25 +2336,25 @@ async function dispatch(msg, ctx) {
2335
2336
  noCache: true,
2336
2337
  logger,
2337
2338
  })
2338
- // 症状C対策: 生成中(armed / pane=processing で判定済みの generating)に TUIビューを
2339
- // 再マウントすると、frontend は全 ref/state がリセットされ、capture 再描画ギャップで
2340
- // この noCache スナップショットの status が一瞬 'waiting' に化けると「生成中なのに
2341
- // 三点リーダーが消える」偽陰性になり、shouldQueue=false でメッセージが PTY(TUI
2342
- // ネイティブキュー)へ流入する。generating を真として status='processing' を配ることで
2343
- // capture 誤読を masking し、frontend の再点灯(turnActive)と shouldQueue 第1ゲートを
2344
- // 即座に効かせる。proc_busy は配らない: outputActive(出力残り火)を含むため、ターン
2345
- // 終了直後の真 idle セッションでも proc_busy=true となり高速消灯(症状B)を再発させる。
2346
- // 終了後(非 generating)は snap.status(waiting)で従来どおり高速消灯する。
2339
+ // 【問題1 根治 2026-06-22】配る status はプロセス実体の busy を権威に解決する
2340
+ // (resolveBindSnapshotStatus)。capture(SPINNER_LINE_RE)の偽陽性に勝たせる。
2341
+ // - busy=true (ターン進行中): capture gap で snap.status waiting に化けても
2342
+ // processing を強制 = 症状C(再マウントで生成中の三点リーダーが消える偽陰性)の masking。
2343
+ // - busy=false (プロセス実体 idle): snap.status processing でも、それは静的完了行の
2344
+ // 偽陽性(無生成時の誤点灯=問題1)なので processing を配らず waiting へ降格する。bind
2345
+ // noCache 単発読みで freeze 履歴が無く偽陽性を弾けないため、ここで busy を権威にする。
2346
+ // generating(capture 由来)でなく busy(isArmed + 子孫数>baseline)を使うのが肝。
2347
+ // proc_busy は配らない: outputActive(出力残り火)を含み真 idle でも true になり高速消灯
2348
+ // (症状B)を再発させるため。
2349
+ const snapStatus = resolveBindSnapshotStatus(busy, snap.status)
2347
2350
  ctx.client.send({
2348
2351
  type: "session.state",
2349
2352
  session_name: sessionName,
2350
- status: generating ? "processing" : snap.status,
2353
+ status: snapStatus,
2351
2354
  context_pct: snap.context_pct,
2352
2355
  permission_mode: snap.permission_mode,
2353
2356
  stable:
2354
- !generating &&
2355
- snap.status !== "processing" &&
2356
- snap.stable === true,
2357
+ !busy && snapStatus !== "processing" && snap.stable === true,
2357
2358
  })
2358
2359
  } catch (err) {
2359
2360
  logger?.warn(
package/src/state.mjs CHANGED
@@ -243,8 +243,13 @@ function workingSpinnerLine(text) {
243
243
  * 不変」なら凍結とみなす。閾値は capture キャッシュ TTL (2.5s) と state loop 周期 (5s) を跨ぐ
244
244
  * 6s に取り、ライブタイマーの 1 サンプル取りこぼしでは誤判定しないようにする。
245
245
  */
246
+ // 静的な完了行 (● Updated…/⏺ Ran tool (2s) 等、ターン終了後もアイドル画面に残る) を
247
+ // SPINNER_LINE_RE が誤検出して processing に張り付く「無生成時の三点リーダー誤点灯」(問題1) の
248
+ // 滞留時間を短縮するため 6000→3500 に下げた (2026-06-22)。実スピナーは経過秒 (Ns) かグリフ
249
+ // アニメで毎秒変化するため frozen には絶対ならず、閾値短縮で偽陰性(生成中の消失)は再発しない
250
+ // (3500ms ≫ 1s アニメ周期で十分な余裕)。env で上書き可。
246
251
  const SPINNER_FREEZE_CONFIRM_MS = Number(
247
- process.env.HUB_AGENT_SPINNER_FREEZE_MS ?? 6000,
252
+ process.env.HUB_AGENT_SPINNER_FREEZE_MS ?? 3500,
248
253
  )
249
254
  /** @type {Map<string, {line: string, at: number}>} session名 → 最初にその行を見た時刻 */
250
255
  const _spinnerFreezeByName = new Map()
@@ -277,6 +282,29 @@ export function detectStatusFromText(text) {
277
282
  return "idle"
278
283
  }
279
284
 
285
+ /**
286
+ * 【問題1 根治 2026-06-22】bind 時の即時ステータススナップショットで配る status を、
287
+ * プロセス実体の busy を権威として解決する。capture (SPINNER_LINE_RE) の偽陽性に勝たせる。
288
+ *
289
+ * - busy=true (isArmed or pane_pid 子孫数 > baseline = ターン進行中): capture gap で
290
+ * capturedStatus が waiting に化けても processing を強制 (症状C=再マウントで生成中の三点
291
+ * リーダーが消える偽陰性の masking)。
292
+ * - busy=false (プロセス実体 idle): capturedStatus が processing でも、それは静的完了行の
293
+ * SPINNER_LINE_RE 偽陽性であって生成中ではないので processing を配らない (= waiting へ降格)。
294
+ * bind は noCache 単発読みで freeze 履歴が無く偽陽性を弾けないため、ここで busy を権威にする。
295
+ * 万一 no-hook セッションの無音生成中だった場合も、次の state loop tick(≤5s)が outputActive で
296
+ * processing に復帰させるので実害は限定的。
297
+ *
298
+ * @param {boolean} busy プロセス実体の busy (isArmed or 子孫数>baseline)
299
+ * @param {string} capturedStatus detectSessionState の status
300
+ * @returns {string}
301
+ */
302
+ export function resolveBindSnapshotStatus(busy, capturedStatus) {
303
+ if (busy) return "processing"
304
+ if (capturedStatus === "processing") return "waiting"
305
+ return capturedStatus
306
+ }
307
+
280
308
  export function detectContextPctFromText(text) {
281
309
  for (const re of CONTEXT_PATTERNS) {
282
310
  const m = text.match(re)