@cocorograph/hub-agent 0.8.85 → 0.8.86
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/chat-sessions-snapshot.mjs +27 -6
- package/src/main.mjs +69 -3
package/package.json
CHANGED
|
@@ -73,6 +73,31 @@ function attachedOf(item) {
|
|
|
73
73
|
return null
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/** listChatTargets の戻り値を cwd 引きの Map にする。 */
|
|
77
|
+
export function chatTargetsByCwd(targets) {
|
|
78
|
+
return new Map((targets ?? []).map((target) => [target.cwd, target]))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* cwd から一覧の行キー (session_name) を解決する。**行名の正本はここ**。
|
|
83
|
+
*
|
|
84
|
+
* ⚠️ sessionNameFor は `__codex` を付け外しするだけで basename 化しない。cwd を直接
|
|
85
|
+
* 渡すと絶対パスがそのまま名前になり、一覧の行にも通知の宛先にも一致しない。
|
|
86
|
+
*
|
|
87
|
+
* `targetByCwd` は任意。現在 listChatTargets が返す名前は workspace / worktree とも
|
|
88
|
+
* `sanitizeTmuxName(basename(cwd))` と同値なので、渡しても渡さなくても結果は変わらない
|
|
89
|
+
* (この同値は test/chat-targets-naming.test.mjs で固定している)。target 固有の名前を
|
|
90
|
+
* 将来入れるなら、targets を引かない呼び出し元 (state loop の通知) も同時に直すこと。
|
|
91
|
+
*
|
|
92
|
+
* ⚠️ cwd が無い記述子で path.basename に undefined を渡すと throw し、呼び出し元の map ごと
|
|
93
|
+
* 失敗して会話一覧が丸ごと空になる。名前を解決できない 1 本のために全部を失わない。
|
|
94
|
+
*/
|
|
95
|
+
export function chatRowSessionName(cwd, cliKind, targetByCwd) {
|
|
96
|
+
const key = typeof cwd === "string" ? cwd : ""
|
|
97
|
+
const dir = targetByCwd?.get(key)?.name ?? (key ? sanitizeTmuxName(path.basename(key)) : "")
|
|
98
|
+
return dir ? sessionNameFor(dir, cliKind) : ""
|
|
99
|
+
}
|
|
100
|
+
|
|
76
101
|
/** ローカル検出した宛先と、毎回取得するライブ会話を返す。 */
|
|
77
102
|
export async function buildChatSessionsSnapshot({
|
|
78
103
|
bridges,
|
|
@@ -81,15 +106,11 @@ export async function buildChatSessionsSnapshot({
|
|
|
81
106
|
}) {
|
|
82
107
|
const descriptors = describeLiveSessionsUnion(bridges)
|
|
83
108
|
const { targets, errors } = await listChatTargets({ logger })
|
|
84
|
-
const targetByCwd =
|
|
109
|
+
const targetByCwd = chatTargetsByCwd(targets)
|
|
85
110
|
const named = descriptors.map((descriptor) => {
|
|
86
111
|
const item = { ...descriptor }
|
|
87
|
-
// ⚠️ cwd が無い記述子で path.basename に undefined を渡すと throw し、map ごと失敗して
|
|
88
|
-
// 会話一覧が丸ごと空になる (呼び出し側の catch が targets:[] live:[] を返す)。名前を
|
|
89
|
-
// 解決できない 1 本のために全部を失わない。
|
|
90
112
|
const cwd = typeof item.cwd === "string" ? item.cwd : ""
|
|
91
|
-
|
|
92
|
-
return { item, cwd, sessionName: dir ? sessionNameFor(dir, item.kind) : "" }
|
|
113
|
+
return { item, cwd, sessionName: chatRowSessionName(cwd, item.kind, targetByCwd) }
|
|
93
114
|
})
|
|
94
115
|
|
|
95
116
|
// ⚠️ session_name は dir 名から作るので、別 workspace 配下の同名 worktree は同じ名前へ
|
package/src/main.mjs
CHANGED
|
@@ -94,7 +94,7 @@ import {
|
|
|
94
94
|
} from "./claude-credentials.mjs"
|
|
95
95
|
import { createWorktreeDir, listWorktreeNameHistory, removeWorktree as removeWorktreeDir } from "./tmux.mjs"
|
|
96
96
|
import { expandTilde, prepareChatWorkspace, isCwdUnavailableError } from "./chat-workspace.mjs"
|
|
97
|
-
import { buildChatSessionsSnapshot, refreshChatSessionsSignals } from "./chat-sessions-snapshot.mjs"
|
|
97
|
+
import { buildChatSessionsSnapshot, chatRowSessionName, refreshChatSessionsSignals } from "./chat-sessions-snapshot.mjs"
|
|
98
98
|
import { chatTargetsCache } from "./chat-targets.mjs"
|
|
99
99
|
import { ensureChatSession } from "./chat-session-starter.mjs"
|
|
100
100
|
import { beginRemoval } from "./chat-removal-guard.mjs"
|
|
@@ -1881,12 +1881,17 @@ export function filterCodexSessionsByAuthority(codexSessions, producerByCwd, seq
|
|
|
1881
1881
|
*
|
|
1882
1882
|
* @returns {{ev: string, at: number}|null}
|
|
1883
1883
|
*/
|
|
1884
|
-
|
|
1885
|
-
|
|
1884
|
+
/** 現在のチャット状態から境界イベント名を導く。初回観測の基準値もここから取る。 */
|
|
1885
|
+
export function deriveChatEvent(chat) {
|
|
1886
|
+
return chat.inputPending
|
|
1886
1887
|
? "input_required"
|
|
1887
1888
|
: chat.status === "processing"
|
|
1888
1889
|
? "prompt_submit"
|
|
1889
1890
|
: "stop"
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
export function chatSessionEventForTick({ chat, prevEv, prevTurnAt, directAt }) {
|
|
1894
|
+
const ev = deriveChatEvent(chat)
|
|
1890
1895
|
if (!(chat.turnAt > prevTurnAt && ev !== prevEv)) return null
|
|
1891
1896
|
const at = ev === "prompt_submit" && typeof directAt === "number" && directAt > 0
|
|
1892
1897
|
? directAt
|
|
@@ -1966,6 +1971,7 @@ export function startStateLoop({ client, logger, intervalMs, claudeBridge, codex
|
|
|
1966
1971
|
// 保護されていて刈れない」tick は 5 秒ごとに同じ行を出し続けるので、状態が変わったときと
|
|
1967
1972
|
// 一定間隔でだけ記録する(warm-bounds-total.mjs の _shouldLog)。
|
|
1968
1973
|
const warmBoundsLogState = { lastUnresolved: 0, lastLoggedAtMs: 0 }
|
|
1974
|
+
const previousChatEvents = new Map()
|
|
1969
1975
|
let stopped = false
|
|
1970
1976
|
|
|
1971
1977
|
const tick = async () => {
|
|
@@ -2012,6 +2018,66 @@ export function startStateLoop({ client, logger, intervalMs, claudeBridge, codex
|
|
|
2012
2018
|
// 実コンテキスト窓サイズ (1M ベータ等) を反映。contextPctFromUsage の分母を
|
|
2013
2019
|
// 5s ごとに最新化し、ドーナツが 200k 固定で振り切れるのを防ぐ。
|
|
2014
2020
|
await refreshContextWindow()
|
|
2021
|
+
// 配送前に停止契約を取り直す。タイマー解除は進行中の tick を止めないので、
|
|
2022
|
+
// await をまたいだ後の観測で通知を出さないのはここだけが担保する。
|
|
2023
|
+
if (stopped) return
|
|
2024
|
+
try {
|
|
2025
|
+
// ⚠️ ここで listChatTargets を引かないこと。TTL は 3 秒 (chat-targets.mjs) で 5 秒 tick では
|
|
2026
|
+
// 毎回失効し、全 worktree に対して `git branch --show-current` をタイムアウト無しで
|
|
2027
|
+
// 直列実行する (tmux.mjs)。無関係な worktree の Git 処理待ちで全会話の通知観測が止まる。
|
|
2028
|
+
// 現在の target 名は workspace / worktree とも sanitizeTmuxName(basename(cwd)) と同値
|
|
2029
|
+
// なので、cwd から直接導出する。この同値は test/chat-targets-naming.test.mjs で固定する。
|
|
2030
|
+
for (const kind of ["claude", "codex"]) {
|
|
2031
|
+
for (const cwd of representativeRows(kind)) {
|
|
2032
|
+
// 観測キーは行名でなく cwd+kind。同名 worktree は 1 行へ畳まれるため、
|
|
2033
|
+
// 行名で観測を共用すると別 cwd の状態が混ざる。
|
|
2034
|
+
const key = `${kind}\u0000${cwd}`
|
|
2035
|
+
const chat = readRowChatState(cwd, kind)
|
|
2036
|
+
const sessionName = chat ? chatRowSessionName(cwd, kind) : ""
|
|
2037
|
+
if (!chat || !sessionName) {
|
|
2038
|
+
previousChatEvents.delete(key)
|
|
2039
|
+
continue
|
|
2040
|
+
}
|
|
2041
|
+
const directAt = getPromptAcceptedAt(cwd, kind)
|
|
2042
|
+
const previous = previousChatEvents.get(key)
|
|
2043
|
+
// 起動時の過去状態は通知せず、次の変化を比較する基準だけを保存する。
|
|
2044
|
+
if (!previous) {
|
|
2045
|
+
previousChatEvents.set(key, { ev: deriveChatEvent(chat), turnAt: chat.turnAt, status: chat.status })
|
|
2046
|
+
continue
|
|
2047
|
+
}
|
|
2048
|
+
const event = chatSessionEventForTick({
|
|
2049
|
+
chat, prevEv: previous.ev, prevTurnAt: previous.turnAt, directAt,
|
|
2050
|
+
})
|
|
2051
|
+
// ⚠️ status は event の有無に関わらず毎 tick 更新する。イベントが出たときだけ
|
|
2052
|
+
// 更新すると、代表の終了が「イベントなし」で素通りしたときに古い status が残る。
|
|
2053
|
+
if (!event) {
|
|
2054
|
+
previousChatEvents.set(key, { ...previous, status: chat.status })
|
|
2055
|
+
continue
|
|
2056
|
+
}
|
|
2057
|
+
// 代表会話が終了して実体が戻ってきた遷移。ターンの完了ではない。
|
|
2058
|
+
const revived = previous.status === null && chat.status !== null
|
|
2059
|
+
previousChatEvents.set(key, { ev: event.ev, turnAt: chat.turnAt, status: chat.status })
|
|
2060
|
+
// ⚠️ 行は代表会話が終了しても消えず、status だけが null になる。これを "完了" と
|
|
2061
|
+
// して送ると、処理中の会話を閉じただけで完了通知が出る。復帰も同じで、終了中に
|
|
2062
|
+
// 承認待ちを送った後に別の会話が waiting で代表になると、ev が
|
|
2063
|
+
// input_required → stop と変わって偽の完了になる。
|
|
2064
|
+
// 一方 inputPending は同 cwd の生存会話すべてから集約されるので、代表終了後に
|
|
2065
|
+
// 背景の会話が承認待ちになったときは送る必要がある。stop だけを絞る。
|
|
2066
|
+
// prompt_submit はこの経路の対象外 (確定仕様として stop / input_required のみ通知する)。
|
|
2067
|
+
const deliverable = event.ev === "input_required"
|
|
2068
|
+
|| (event.ev === "stop" && chat.status !== null && !revived)
|
|
2069
|
+
if (!deliverable) continue
|
|
2070
|
+
// ⚠️ chat の読み取りから publish まで await を挟まない。
|
|
2071
|
+
// 既知の取りこぼし: 前回 tick も今回 tick も waiting で、その間にターンが
|
|
2072
|
+
// 開始・完了すると ev が stop のまま変わらないため、通知は遅延ではなく欠落する。
|
|
2073
|
+
// 5 秒 tick を前提にした仕様。
|
|
2074
|
+
publishSessionEvent({ client, logger }, sessionName, event.ev, event.at, { arm: false })
|
|
2075
|
+
.catch(err => logger?.warn({ err: err?.message }, "chat session.event publish failed"))
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
} catch (err) {
|
|
2079
|
+
logger?.warn({ err: err?.message }, "chat session.event tick failed")
|
|
2080
|
+
}
|
|
2015
2081
|
} catch (err) {
|
|
2016
2082
|
logger?.warn({ err: err.message }, "state loop tick failed")
|
|
2017
2083
|
}
|