@cocorograph/hub-agent 0.6.52 → 0.6.53

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.52",
3
+ "version": "0.6.53",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの tmux/pty を中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -256,22 +256,50 @@ export async function listSessions({ cwd, projectsRoot, limit = 30, logger }) {
256
256
  * - `viewingSessionId`: フロントが今表示している (閲覧ハートビートが運ぶ) session_id。
257
257
  * - `newestSessionId`: cwd 配下で実際に最新 (mtime 降順先頭) の session_id。
258
258
  * - `lastNotifiedNewId`: 同一 new への多重通知を防ぐため、最後に通知した new_session_id。
259
+ * - `lastNotifiedAt` / `now`: 再通知スロットリング用のタイムスタンプ (ms)。両方与えられた
260
+ * ときだけ時間判定が効く (無ければ従来どおり「同一 new は一度きり」)。
261
+ * - `reNotifyMs`: 同一 new でも再通知を許す最小間隔 (既定 4000ms)。
262
+ *
263
+ * 回転とみなす条件:
264
+ * 最新が存在し、閲覧中 id と異なり、かつ次のいずれか:
265
+ * (a) 最後に通知した new と異なる (= さらに別 new へ回転した) → 即通知。
266
+ * (b) 最後に通知した new と同じだが、`reNotifyMs` 以上経過している → **再通知**。
267
+ *
268
+ * (b) が肝。回転通知は one-shot broadcast で、多タブ・再接続・hidden タブ等で取りこぼすと
269
+ * クライアントが旧 session に**恒久固着**する (ゾンビ閲覧者バグ)。viewer が依然 stale な
270
+ * session_id を報告し続けている (= newest と不一致) という事実は「まだ切り替わっていない」
271
+ * 証拠なので、スロットリングしつつ繰り返し通知して救済する。切り替わった viewer は次の
272
+ * ハートビートで viewingSessionId === newest を報告し、条件 (最新≠閲覧中) を満たさなくなる
273
+ * ため自然に再通知が止まる。`reNotifyMs` をハートビート間隔 (5s) よりやや短くしておくと、
274
+ * 固着している間は毎ハートビートで 1 回ずつ通知が飛ぶ。
259
275
  *
260
- * 回転とみなす条件: 最新が存在し、閲覧中 id と異なり、かつ直近で同じ new を通知済みでない。
261
276
  * 注意: 過去セッションを意図的に開いている (= newest 非追従) ビューでは呼び出し側が
262
277
  * `follow_newest=false` でこの判定自体をスキップすること (ピン留め閲覧を勝手に最新へ
263
278
  * 引きずらないため)。本関数は「追従中ビュー」前提で newest とのズレだけを見る。
264
279
  *
265
- * @param {{viewingSessionId?: string|null, newestSessionId?: string|null, lastNotifiedNewId?: string|null}} args
280
+ * @param {{viewingSessionId?: string|null, newestSessionId?: string|null, lastNotifiedNewId?: string|null, lastNotifiedAt?: number|null, now?: number|null, reNotifyMs?: number}} args
266
281
  * @returns {{rotated: boolean, newSessionId?: string}}
267
282
  */
268
283
  export function decideSessionRotation({
269
284
  viewingSessionId,
270
285
  newestSessionId,
271
286
  lastNotifiedNewId,
287
+ lastNotifiedAt,
288
+ now,
289
+ reNotifyMs = 4000,
272
290
  } = {}) {
273
291
  if (!newestSessionId || !viewingSessionId) return { rotated: false }
274
292
  if (newestSessionId === viewingSessionId) return { rotated: false }
275
- if (newestSessionId === lastNotifiedNewId) return { rotated: false }
293
+ if (newestSessionId === lastNotifiedNewId) {
294
+ // 同一 new。時刻情報が揃っていて再通知間隔を超えていれば、固着救済のため再通知する。
295
+ if (
296
+ typeof now === "number" &&
297
+ typeof lastNotifiedAt === "number" &&
298
+ now - lastNotifiedAt >= reNotifyMs
299
+ ) {
300
+ return { rotated: true, newSessionId: newestSessionId }
301
+ }
302
+ return { rotated: false }
303
+ }
276
304
  return { rotated: true, newSessionId: newestSessionId }
277
305
  }
package/src/main.mjs CHANGED
@@ -1148,13 +1148,22 @@ async function dispatch(msg, ctx) {
1148
1148
  })
1149
1149
  const newestId = sessions?.[0]?.session_id || null
1150
1150
  const key = viewName || viewCwd
1151
+ const prev = ctx.tuiRotationNotified.get(key)
1152
+ const now = Date.now()
1151
1153
  const { rotated, newSessionId } = decideSessionRotation({
1152
1154
  viewingSessionId: viewSid,
1153
1155
  newestSessionId: newestId,
1154
- lastNotifiedNewId: ctx.tuiRotationNotified.get(key),
1156
+ // 旧形式 (文字列) と新形式 ({newId, ts}) の両対応。
1157
+ lastNotifiedNewId:
1158
+ prev && typeof prev === "object" ? prev.newId : prev,
1159
+ lastNotifiedAt:
1160
+ prev && typeof prev === "object" ? prev.ts : null,
1161
+ now,
1155
1162
  })
1156
1163
  if (!rotated) return
1157
- ctx.tuiRotationNotified.set(key, newSessionId)
1164
+ // {newId, ts} で保存し、同一 new への再通知をスロットリングする
1165
+ // (固着した viewer は throttle 間隔ごとに 1 回ずつ救済通知を受ける)。
1166
+ ctx.tuiRotationNotified.set(key, { newId: newSessionId, ts: now })
1158
1167
  // 冪等マップも新 id へ更新し、将来の remount bind が旧 id へ
1159
1168
  // respawn (claude 再起動) してしまうのを防ぐ。
1160
1169
  if (viewName) ctx.tuiReboundSessions.set(viewName, newSessionId)