@cocorograph/hub-agent 0.8.6 → 0.8.8

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/main.mjs +56 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocorograph/hub-agent",
3
- "version": "0.8.6",
3
+ "version": "0.8.8",
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
@@ -1786,12 +1786,21 @@ function isSafeSessionIdForPath(id) {
1786
1786
  *
1787
1787
  * @param {Array} sessions listTmuxSessions の素の行 (name / cwd を使う)
1788
1788
  * @param {((name: string) => object | null) | undefined} getSnap state loop の getSnapshot
1789
+ * @param {object} [options] I/O と session ID 解決の注入点
1790
+ * @param {((session: object) => string | null) | undefined} [options.resolveFallbackSessionId]
1791
+ * snapshot の context_session_id が再起動後の `unbound:*` のとき、TUI bind / live
1792
+ * Claude bridge から同じ cwd の会話 ID を復元する。戻り値にも containment 検査を行う。
1789
1793
  * @returns {Promise<Map<string, {user: string, assistant: string}>>}
1790
1794
  */
1791
1795
  export async function readAllSessionLastTexts(
1792
1796
  sessions,
1793
1797
  getSnap,
1794
- { extract = extractLastTurnTexts, statFn = stat, resolvePath = jsonlPath } = {},
1798
+ {
1799
+ extract = extractLastTurnTexts,
1800
+ statFn = stat,
1801
+ resolvePath = jsonlPath,
1802
+ resolveFallbackSessionId,
1803
+ } = {},
1795
1804
  ) {
1796
1805
  const out = new Map()
1797
1806
  if (!getSnap) return out
@@ -1801,14 +1810,48 @@ export async function readAllSessionLastTexts(
1801
1810
  try {
1802
1811
  const snap = getSnap(s.name)
1803
1812
  if (!snap || snap.cli_kind !== "claude") return
1804
- const sessionId = snap.context_session_id
1813
+ let sessionId = snap.context_session_id
1814
+ let usedFallback = false
1815
+ // hub-agent 再起動で in-memory の TUI bind が消えると、state loop の snapshot は
1816
+ // `unbound:<session>` へ落ちる。一方、Claude bridge の warm/live session は cwd
1817
+ // から同じ会話 ID を復元できる。ここでフォールバックしないと、再起動後に全行の
1818
+ // 「最後の依頼 / 応答」だけが消え、次に chat signal が fresh になるまで戻らない。
1819
+ //
1820
+ // snapshot が安全な実 ID を持つ場合は必ずそちらを優先し、fallback は placeholder /
1821
+ // 危険値の場合だけ使う。fallback も同じ containment 検査を通してから path 化する。
1822
+ if (!isSafeSessionIdForPath(sessionId) && resolveFallbackSessionId) {
1823
+ sessionId = resolveFallbackSessionId(s)
1824
+ usedFallback = true
1825
+ }
1805
1826
  // パス要素として安全な形だけを通す (外部由来の値なので containment を局所保証)。
1806
1827
  if (!isSafeSessionIdForPath(sessionId)) return
1807
1828
  if (!s.cwd) return
1808
- const fp = resolvePath({ cwd: s.cwd, session_id: sessionId })
1829
+ let fp = resolvePath({ cwd: s.cwd, session_id: sessionId })
1809
1830
  if (!fp) return
1831
+ let st
1832
+ try {
1833
+ st = await statFn(fp)
1834
+ } catch {
1835
+ // 再起動直後は snapshot に構文上は安全でも既に失効した session ID が残ることがある。
1836
+ // 0.8.7 は unbound/fresh だけを live session へ切り替えていたため、この場合は stat
1837
+ // 失敗を外側 catch が握りつぶし、サイドバーの依頼/応答だけが空のままになった。
1838
+ // primary path が読めない場合に限って、現在の cwd-scoped live session ID を再解決する。
1839
+ // snapshot path が実在する通常系では従来どおり snapshot を優先する。
1840
+ if (usedFallback || !resolveFallbackSessionId) return
1841
+ const fallbackId = resolveFallbackSessionId(s)
1842
+ if (!isSafeSessionIdForPath(fallbackId) || fallbackId === sessionId) return
1843
+ const fallbackPath = resolvePath({ cwd: s.cwd, session_id: fallbackId })
1844
+ if (!fallbackPath) return
1845
+ try {
1846
+ st = await statFn(fallbackPath)
1847
+ } catch {
1848
+ return
1849
+ }
1850
+ sessionId = fallbackId
1851
+ fp = fallbackPath
1852
+ usedFallback = true
1853
+ }
1810
1854
  livePaths.add(fp)
1811
- const st = await statFn(fp)
1812
1855
  const version = lastTurnTextsVersion(st)
1813
1856
  const cached = lastTurnTextsCache.get(fp)
1814
1857
  if (cached && cached.version === version) {
@@ -4739,7 +4782,15 @@ async function dispatch(msg, ctx) {
4739
4782
  })
4740
4783
  const [lastEventByName, lastTextsByName] = await Promise.all([
4741
4784
  readAllSessionEvents(),
4742
- readAllSessionLastTexts(sessions, ctx.getSessionStateSnapshot),
4785
+ readAllSessionLastTexts(sessions, ctx.getSessionStateSnapshot, {
4786
+ resolveFallbackSessionId: (session) =>
4787
+ resolveStateLoopBoundSessionId(
4788
+ ctx.tuiReboundSessions,
4789
+ session.name,
4790
+ session.cwd,
4791
+ ctx.claudeBridge,
4792
+ ),
4793
+ }),
4743
4794
  ])
4744
4795
  const enriched = enrichTmuxSessionsForListResponse(
4745
4796
  sessions,