@cocorograph/hub-agent 0.8.8 → 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 +50 -9
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,12 @@ 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
|
|
1814
1854
|
let usedFallback = false
|
|
1815
1855
|
// hub-agent 再起動で in-memory の TUI bind が消えると、state loop の snapshot は
|
|
1816
1856
|
// `unbound:<session>` へ落ちる。一方、Claude bridge の warm/live session は cwd
|
|
@@ -1820,7 +1860,7 @@ export async function readAllSessionLastTexts(
|
|
|
1820
1860
|
// snapshot が安全な実 ID を持つ場合は必ずそちらを優先し、fallback は placeholder /
|
|
1821
1861
|
// 危険値の場合だけ使う。fallback も同じ containment 検査を通してから path 化する。
|
|
1822
1862
|
if (!isSafeSessionIdForPath(sessionId) && resolveFallbackSessionId) {
|
|
1823
|
-
sessionId = resolveFallbackSessionId(s)
|
|
1863
|
+
sessionId = await resolveFallbackSessionId(s)
|
|
1824
1864
|
usedFallback = true
|
|
1825
1865
|
}
|
|
1826
1866
|
// パス要素として安全な形だけを通す (外部由来の値なので containment を局所保証)。
|
|
@@ -1838,7 +1878,7 @@ export async function readAllSessionLastTexts(
|
|
|
1838
1878
|
// primary path が読めない場合に限って、現在の cwd-scoped live session ID を再解決する。
|
|
1839
1879
|
// snapshot path が実在する通常系では従来どおり snapshot を優先する。
|
|
1840
1880
|
if (usedFallback || !resolveFallbackSessionId) return
|
|
1841
|
-
const fallbackId = resolveFallbackSessionId(s)
|
|
1881
|
+
const fallbackId = await resolveFallbackSessionId(s)
|
|
1842
1882
|
if (!isSafeSessionIdForPath(fallbackId) || fallbackId === sessionId) return
|
|
1843
1883
|
const fallbackPath = resolvePath({ cwd: s.cwd, session_id: fallbackId })
|
|
1844
1884
|
if (!fallbackPath) return
|
|
@@ -4783,13 +4823,14 @@ async function dispatch(msg, ctx) {
|
|
|
4783
4823
|
const [lastEventByName, lastTextsByName] = await Promise.all([
|
|
4784
4824
|
readAllSessionEvents(),
|
|
4785
4825
|
readAllSessionLastTexts(sessions, ctx.getSessionStateSnapshot, {
|
|
4786
|
-
resolveFallbackSessionId: (session) =>
|
|
4826
|
+
resolveFallbackSessionId: async (session) =>
|
|
4787
4827
|
resolveStateLoopBoundSessionId(
|
|
4788
4828
|
ctx.tuiReboundSessions,
|
|
4789
4829
|
session.name,
|
|
4790
4830
|
session.cwd,
|
|
4791
4831
|
ctx.claudeBridge,
|
|
4792
|
-
)
|
|
4832
|
+
) ||
|
|
4833
|
+
(await latestClaudeSessionIdForSidebar(session.cwd, ctx.logger)),
|
|
4793
4834
|
}),
|
|
4794
4835
|
])
|
|
4795
4836
|
const enriched = enrichTmuxSessionsForListResponse(
|