@cocorograph/hub-agent 0.7.76 → 0.7.78

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.76",
3
+ "version": "0.7.78",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの tmux/pty を中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -126,7 +126,27 @@ function flattenThreadItems(thread, { maxItems = HISTORY_MAX_ITEMS, maxBytes = H
126
126
  const turns = Array.isArray(thread?.turns) ? thread.turns : []
127
127
  const all = []
128
128
  for (const turn of turns) {
129
- if (Array.isArray(turn?.items)) all.push(...turn.items)
129
+ if (!Array.isArray(turn?.items)) continue
130
+ // 表示用バブル時刻 (症状⑧: 履歴 hydrate で全バブルが reload 時刻 = Date.now() になる問題)。
131
+ // app-server の ThreadItem は per-item timestamp を持たない (生成スキーマで確認済み)。
132
+ // ターン単位の時刻 (単位は秒) を各 item に at_ms (ms epoch) として付与する。
133
+ //
134
+ // ⚠️ ライブ経路との整合 (round2 minor): item の種別で使う時刻を変える。
135
+ // - userMessage: ライブでは item/completed.completedAtMs = **ターン冒頭**の時刻で届く
136
+ // (ユーザー入力の echo は即完了するため)。履歴で completedAt(ターン終了) を当てると、
137
+ // 長いターンでリロード後にユーザーバブルが終了時刻へ飛ぶ。→ Turn.startedAt を優先。
138
+ // - それ以外 (agentMessage/tool 等): ライブでは各 item の完了時刻で届くので、
139
+ // ターン終了に近い Turn.completedAt を優先 (無ければ startedAt)。
140
+ const num = (v) => (typeof v === "number" ? v : null)
141
+ const startSec = num(turn.startedAt)
142
+ const endSec = num(turn.completedAt)
143
+ const secFor = (item) =>
144
+ item?.type === "userMessage" ? (startSec ?? endSec) : (endSec ?? startSec)
145
+ for (const item of turn.items) {
146
+ const sec = secFor(item)
147
+ const atMs = sec != null ? Math.round(sec * 1000) : null
148
+ all.push(atMs != null && item && typeof item === "object" ? { ...item, at_ms: atMs } : item)
149
+ }
130
150
  }
131
151
  const total = all.length
132
152
  const kept = []
@@ -341,6 +361,12 @@ export class CodexStreamBridge extends EventEmitter {
341
361
  // 通知 (ターン進捗) の両方で更新する。LRU は「最終活性が最も古い非 protected」を退避する。
342
362
  // Date.now() は hub-agent 実行環境で利用可。
343
363
  lastActivityMs: Date.now(),
364
+ // 相方選定・一覧の「新しさ」用の会話ターン境界時刻。lastActivityMs(keepalive 閲覧で
365
+ // 更新される warm/reap 保護用)と役割を分離し、**turn/started・turn/completed のときだけ**
366
+ // 更新する。これにより「見ているだけ(keepalive)」で並び順・ask-sibling 宛先選定が動く
367
+ // 問題を根治する(症状⑨と同じ「read/閲覧時刻を鮮度信号に流用しない」原則)。初期値は
368
+ // attach 時刻=新規セッションは「たった今作った」で上位に来る(閲覧中の古い会話に負けない)。
369
+ lastTurnAtMs: Date.now(),
344
370
  ready: null,
345
371
  }
346
372
  session.ready = new Promise((resolve) => {
@@ -590,9 +616,12 @@ export class CodexStreamBridge extends EventEmitter {
590
616
  switch (msg.method) {
591
617
  case "turn/started":
592
618
  session.activeTurnId = msg.params?.turn?.id || null
619
+ // 会話ターン境界で「新しさ」を更新(閲覧 keepalive では更新しない)。
620
+ session.lastTurnAtMs = Date.now()
593
621
  break
594
622
  case "turn/completed":
595
623
  session.activeTurnId = null
624
+ session.lastTurnAtMs = Date.now()
596
625
  this._autoResolveStalePendingApprovals(session, msg.params?.turn?.id)
597
626
  // Stage 5: turn 完了後の reap 予約は不要。以降 idle になれば周期 reconcile が
598
627
  // 長 idle TTL / LRU で扱う(detach 済みでも 7 日 warm 保持)。
@@ -813,7 +842,9 @@ export class CodexStreamBridge extends EventEmitter {
813
842
  thread_id: session.threadId || null,
814
843
  busy: !!session.activeTurnId,
815
844
  detached: !!session.detachRequested,
816
- idle_ms: now - (session.lastActivityMs ?? now),
845
+ // idle_ms は「新しさ」の指標(一覧の並び・宛先選定に使う)なので、閲覧 keepalive で
846
+ // 動く lastActivityMs ではなく **会話ターン境界時刻 lastTurnAtMs** を基準にする。
847
+ idle_ms: now - (session.lastTurnAtMs ?? session.lastActivityMs ?? now),
817
848
  })
818
849
  }
819
850
  return out
@@ -859,7 +890,9 @@ export class CodexStreamBridge extends EventEmitter {
859
890
  let bestAt = -1
860
891
  for (const session of this.sessions.values()) {
861
892
  if (session.dead || session.cwd !== cwd) continue
862
- const at = session.lastActivityMs ?? 0
893
+ // 宛先選定の「最新」は会話ターン境界時刻で判定する(閲覧 keepalive で動く
894
+ // lastActivityMs を使うと「見ているだけの古い会話」が本命を追い越す)。
895
+ const at = session.lastTurnAtMs ?? session.lastActivityMs ?? 0
863
896
  if (at >= bestAt) {
864
897
  bestAt = at
865
898
  best = session