@cocorograph/hub-agent 0.8.7 → 0.8.9
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 +1 -1
- package/src/main.mjs +75 -10
package/package.json
CHANGED
package/src/main.mjs
CHANGED
|
@@ -1727,10 +1727,43 @@ const tmuxSessionsSnapshotCache = new AsyncTtlCache({ ttlMs: 5_000 })
|
|
|
1727
1727
|
/** cold cache 時に全セッションを一斉に読まないための同時実行上限。 */
|
|
1728
1728
|
const LAST_TEXTS_CONCURRENCY = 4
|
|
1729
1729
|
|
|
1730
|
+
/**
|
|
1731
|
+
* live bridge に存在しない workspace 行向けの低頻度 fallback。
|
|
1732
|
+
*
|
|
1733
|
+
* Cockpit は選択中でない workspace の Claude stream を常に attach しているわけではない。
|
|
1734
|
+
* hub-agent 再起動後、その行には state snapshot も live session ID も無いため、cwd 配下の
|
|
1735
|
+
* 会話履歴を「最後の実ターン時刻」順で 1 件だけ解決する。`listSessions` は mtime ではなく
|
|
1736
|
+
* JSONL 内の user/assistant timestamp を正本にするので、古い会話を閲覧しただけの mtime
|
|
1737
|
+
* 上昇では選択が入れ替わらない。
|
|
1738
|
+
*
|
|
1739
|
+
* 全 sidebar poll で全 cwd を再走査しないよう、成功/空のどちらも 60 秒だけ共有する。
|
|
1740
|
+
* 選択中の行は live bridge ID が先に選ばれるため、この cache の遅延は影響しない。
|
|
1741
|
+
*/
|
|
1742
|
+
const sidebarLatestSessionIdCache = new Map()
|
|
1743
|
+
const SIDEBAR_LATEST_SESSION_ID_TTL_MS = 60_000
|
|
1744
|
+
|
|
1745
|
+
async function latestClaudeSessionIdForSidebar(cwd, logger) {
|
|
1746
|
+
if (!cwd) return null
|
|
1747
|
+
const now = Date.now()
|
|
1748
|
+
const cached = sidebarLatestSessionIdCache.get(cwd)
|
|
1749
|
+
if (cached && now - cached.at < SIDEBAR_LATEST_SESSION_ID_TTL_MS) {
|
|
1750
|
+
return cached.promise
|
|
1751
|
+
}
|
|
1752
|
+
const entry = {
|
|
1753
|
+
at: now,
|
|
1754
|
+
promise: listSessions({ cwd, limit: 1, logger })
|
|
1755
|
+
.then((result) => result.sessions?.[0]?.session_id ?? null)
|
|
1756
|
+
.catch(() => null),
|
|
1757
|
+
}
|
|
1758
|
+
sidebarLatestSessionIdCache.set(cwd, entry)
|
|
1759
|
+
return entry.promise
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1730
1762
|
/** テスト用: module-global なキャッシュのテスト間汚染を防ぐ。 */
|
|
1731
1763
|
export function clearLastTurnTextsCacheForTest() {
|
|
1732
1764
|
lastTurnTextsCache.clear()
|
|
1733
1765
|
lastTurnTextsInFlight.clear()
|
|
1766
|
+
sidebarLatestSessionIdCache.clear()
|
|
1734
1767
|
}
|
|
1735
1768
|
|
|
1736
1769
|
/** GC: tmux から消えたセッションの jsonl は追跡しない (Map の線形増加を防ぐ)。 */
|
|
@@ -1779,7 +1812,8 @@ function isSafeSessionIdForPath(id) {
|
|
|
1779
1812
|
*
|
|
1780
1813
|
* Cockpit サイドバーが行ごとに「何を依頼したか」を出すためのデータ源 (2026-07-28)。
|
|
1781
1814
|
* frontend は選択中 1 セッションしかメッセージを保持しないため、一覧用は agent 側で
|
|
1782
|
-
* 供給するしかない。会話 ID は state loop の snapshot (`context_session_id`)
|
|
1815
|
+
* 供給するしかない。会話 ID は state loop の snapshot (`context_session_id`) を優先し、
|
|
1816
|
+
* snapshot / live bridge のどちらにも無ければ cwd の最新実ターン履歴へフォールバックする。
|
|
1783
1817
|
*
|
|
1784
1818
|
* 対象は **claude 行のみ**。codex はスレッド保存形式が異なるため対象外 (将来対応)。
|
|
1785
1819
|
* `fresh:<request_id>` は jsonl 未生成のプレースホルダなのでスキップする。
|
|
@@ -1787,9 +1821,11 @@ function isSafeSessionIdForPath(id) {
|
|
|
1787
1821
|
* @param {Array} sessions listTmuxSessions の素の行 (name / cwd を使う)
|
|
1788
1822
|
* @param {((name: string) => object | null) | undefined} getSnap state loop の getSnapshot
|
|
1789
1823
|
* @param {object} [options] I/O と session ID 解決の注入点
|
|
1790
|
-
* @param {((session: object) => string | null) | undefined}
|
|
1824
|
+
* @param {((session: object) => string | null | Promise<string | null>) | undefined}
|
|
1825
|
+
* [options.resolveFallbackSessionId]
|
|
1791
1826
|
* snapshot の context_session_id が再起動後の `unbound:*` のとき、TUI bind / live
|
|
1792
|
-
* Claude bridge
|
|
1827
|
+
* Claude bridge または履歴一覧から同じ cwd の会話 ID を復元する。
|
|
1828
|
+
* 戻り値にも containment 検査を行う。
|
|
1793
1829
|
* @returns {Promise<Map<string, {user: string, assistant: string}>>}
|
|
1794
1830
|
*/
|
|
1795
1831
|
export async function readAllSessionLastTexts(
|
|
@@ -1809,8 +1845,13 @@ export async function readAllSessionLastTexts(
|
|
|
1809
1845
|
const readOne = async (s) => {
|
|
1810
1846
|
try {
|
|
1811
1847
|
const snap = getSnap(s.name)
|
|
1812
|
-
|
|
1813
|
-
|
|
1848
|
+
// CLI 種別の正本はセッション名 (`__codex` suffix) であり、state snapshot ではない。
|
|
1849
|
+
// 再起動直後は tmux の一覧行が先に復元され、state loop の初回 tick (<5s) より前は
|
|
1850
|
+
// snapshot 自体がまだ無い。この cold window で snapshot を必須にすると、live Claude
|
|
1851
|
+
// session ID は既に復元済みでも最後の依頼/応答だけが空のまま返る。
|
|
1852
|
+
if (cliKindForSessionName(s.name) !== "claude") return
|
|
1853
|
+
let sessionId = snap?.context_session_id
|
|
1854
|
+
let usedFallback = false
|
|
1814
1855
|
// hub-agent 再起動で in-memory の TUI bind が消えると、state loop の snapshot は
|
|
1815
1856
|
// `unbound:<session>` へ落ちる。一方、Claude bridge の warm/live session は cwd
|
|
1816
1857
|
// から同じ会話 ID を復元できる。ここでフォールバックしないと、再起動後に全行の
|
|
@@ -1819,15 +1860,38 @@ export async function readAllSessionLastTexts(
|
|
|
1819
1860
|
// snapshot が安全な実 ID を持つ場合は必ずそちらを優先し、fallback は placeholder /
|
|
1820
1861
|
// 危険値の場合だけ使う。fallback も同じ containment 検査を通してから path 化する。
|
|
1821
1862
|
if (!isSafeSessionIdForPath(sessionId) && resolveFallbackSessionId) {
|
|
1822
|
-
sessionId = resolveFallbackSessionId(s)
|
|
1863
|
+
sessionId = await resolveFallbackSessionId(s)
|
|
1864
|
+
usedFallback = true
|
|
1823
1865
|
}
|
|
1824
1866
|
// パス要素として安全な形だけを通す (外部由来の値なので containment を局所保証)。
|
|
1825
1867
|
if (!isSafeSessionIdForPath(sessionId)) return
|
|
1826
1868
|
if (!s.cwd) return
|
|
1827
|
-
|
|
1869
|
+
let fp = resolvePath({ cwd: s.cwd, session_id: sessionId })
|
|
1828
1870
|
if (!fp) return
|
|
1871
|
+
let st
|
|
1872
|
+
try {
|
|
1873
|
+
st = await statFn(fp)
|
|
1874
|
+
} catch {
|
|
1875
|
+
// 再起動直後は snapshot に構文上は安全でも既に失効した session ID が残ることがある。
|
|
1876
|
+
// 0.8.7 は unbound/fresh だけを live session へ切り替えていたため、この場合は stat
|
|
1877
|
+
// 失敗を外側 catch が握りつぶし、サイドバーの依頼/応答だけが空のままになった。
|
|
1878
|
+
// primary path が読めない場合に限って、現在の cwd-scoped live session ID を再解決する。
|
|
1879
|
+
// snapshot path が実在する通常系では従来どおり snapshot を優先する。
|
|
1880
|
+
if (usedFallback || !resolveFallbackSessionId) return
|
|
1881
|
+
const fallbackId = await resolveFallbackSessionId(s)
|
|
1882
|
+
if (!isSafeSessionIdForPath(fallbackId) || fallbackId === sessionId) return
|
|
1883
|
+
const fallbackPath = resolvePath({ cwd: s.cwd, session_id: fallbackId })
|
|
1884
|
+
if (!fallbackPath) return
|
|
1885
|
+
try {
|
|
1886
|
+
st = await statFn(fallbackPath)
|
|
1887
|
+
} catch {
|
|
1888
|
+
return
|
|
1889
|
+
}
|
|
1890
|
+
sessionId = fallbackId
|
|
1891
|
+
fp = fallbackPath
|
|
1892
|
+
usedFallback = true
|
|
1893
|
+
}
|
|
1829
1894
|
livePaths.add(fp)
|
|
1830
|
-
const st = await statFn(fp)
|
|
1831
1895
|
const version = lastTurnTextsVersion(st)
|
|
1832
1896
|
const cached = lastTurnTextsCache.get(fp)
|
|
1833
1897
|
if (cached && cached.version === version) {
|
|
@@ -4759,13 +4823,14 @@ async function dispatch(msg, ctx) {
|
|
|
4759
4823
|
const [lastEventByName, lastTextsByName] = await Promise.all([
|
|
4760
4824
|
readAllSessionEvents(),
|
|
4761
4825
|
readAllSessionLastTexts(sessions, ctx.getSessionStateSnapshot, {
|
|
4762
|
-
resolveFallbackSessionId: (session) =>
|
|
4826
|
+
resolveFallbackSessionId: async (session) =>
|
|
4763
4827
|
resolveStateLoopBoundSessionId(
|
|
4764
4828
|
ctx.tuiReboundSessions,
|
|
4765
4829
|
session.name,
|
|
4766
4830
|
session.cwd,
|
|
4767
4831
|
ctx.claudeBridge,
|
|
4768
|
-
)
|
|
4832
|
+
) ||
|
|
4833
|
+
(await latestClaudeSessionIdForSidebar(session.cwd, ctx.logger)),
|
|
4769
4834
|
}),
|
|
4770
4835
|
])
|
|
4771
4836
|
const enriched = enrichTmuxSessionsForListResponse(
|