@cocorograph/hub-agent 0.8.81 → 0.8.82
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 +10 -4
- package/src/chat-signals.mjs +81 -247
- package/src/claude-stream-bridge.mjs +30 -7
- package/src/codex-stream-bridge.mjs +30 -4
- package/src/dispatch-scheduler.mjs +5 -0
- package/src/main.mjs +206 -558
- package/src/usage.mjs +8 -3
package/src/main.mjs
CHANGED
|
@@ -116,7 +116,7 @@ import {
|
|
|
116
116
|
validateInitialPrompt,
|
|
117
117
|
} from "./tmux.mjs"
|
|
118
118
|
import { expandTilde, prepareChatWorkspace, isCwdUnavailableError } from "./chat-workspace.mjs"
|
|
119
|
-
import { buildChatSessionsSnapshot } from "./chat-sessions-snapshot.mjs"
|
|
119
|
+
import { buildChatSessionsSnapshot, refreshChatSessionsSignals } from "./chat-sessions-snapshot.mjs"
|
|
120
120
|
import { chatTargetsCache } from "./chat-targets.mjs"
|
|
121
121
|
import { ensureChatSession } from "./chat-session-starter.mjs"
|
|
122
122
|
import { beginRemoval } from "./chat-removal-guard.mjs"
|
|
@@ -125,7 +125,6 @@ import { describeLiveSessionsUnion, isSessionWarm } from "./live-sessions.mjs"
|
|
|
125
125
|
import { applyPointerContexts, resolvePointerContexts } from "./pointer-context.mjs"
|
|
126
126
|
import {
|
|
127
127
|
boundSessionId,
|
|
128
|
-
codexContextPercentFromTokenUsage,
|
|
129
128
|
codexUsageFromAccountRateLimits,
|
|
130
129
|
codexLiveContextForProducer,
|
|
131
130
|
codexSessionUsages,
|
|
@@ -136,7 +135,6 @@ import {
|
|
|
136
135
|
getSessionUsages,
|
|
137
136
|
getUsage,
|
|
138
137
|
isFreshUnboundBind,
|
|
139
|
-
jsonlContextForCwd,
|
|
140
138
|
recordChatRateLimit,
|
|
141
139
|
recordChatUsageSnapshot,
|
|
142
140
|
readAccountUsageSnapshot,
|
|
@@ -145,12 +143,14 @@ import {
|
|
|
145
143
|
turnActiveForCwd,
|
|
146
144
|
} from "./usage.mjs"
|
|
147
145
|
import {
|
|
148
|
-
clearChatSignal,
|
|
149
|
-
getChatSignal,
|
|
150
146
|
getPromptAcceptedAt,
|
|
151
|
-
recordChatActivity,
|
|
152
|
-
recordChatStreamTick,
|
|
153
147
|
recordPromptAccepted,
|
|
148
|
+
readRowChatState,
|
|
149
|
+
conversationContext,
|
|
150
|
+
loadResumeChatContext,
|
|
151
|
+
representativeRows,
|
|
152
|
+
getRepresentativeConversation,
|
|
153
|
+
hasRepresentativeRow,
|
|
154
154
|
} from "./chat-signals.mjs"
|
|
155
155
|
import { cliKindForSessionName, sessionNameFor } from "./session-names.mjs"
|
|
156
156
|
import {
|
|
@@ -1071,7 +1071,7 @@ export async function startDaemon({ version, claudeSdk } = {}) {
|
|
|
1071
1071
|
// そのまま browser に転送する。SDK 未インストール時は claudeBridge=null で全 attach が
|
|
1072
1072
|
// claude.error を返す経路に分岐 (dispatch 側で判定)。
|
|
1073
1073
|
if (claudeBridge) {
|
|
1074
|
-
claudeBridge.on("event", ({ stream_id, session_id, cwd, event }) => {
|
|
1074
|
+
claudeBridge.on("event", ({ stream_id, session_id, cwd, session, event }) => {
|
|
1075
1075
|
// SDK の rate_limit_event を捕捉して usage の 5h/7d 実値に反映する
|
|
1076
1076
|
// (チャットモードでは statusLine が更新されないため、これが live 値の唯一の源)。
|
|
1077
1077
|
if (event?.type === "rate_limit_event" && event.rate_limit_info) {
|
|
@@ -1081,76 +1081,10 @@ export async function startDaemon({ version, claudeSdk } = {}) {
|
|
|
1081
1081
|
/* ignore */
|
|
1082
1082
|
}
|
|
1083
1083
|
}
|
|
1084
|
-
|
|
1085
|
-
// 記録する。startStateLoop が cwd 一致で session.state (ステータスドット/各行
|
|
1086
|
-
// ドーナツ) と session.event (ソート) に橋渡しする。チャットは tmux ペインに
|
|
1087
|
-
// TUI を出さず、capture-pane スクレイプも bundle hook も発火しないための補完。
|
|
1088
|
-
// - assistant (生成中) → processing + usage から context%
|
|
1089
|
-
// - session_state_changed:idle → waiting
|
|
1090
|
-
// result は結果 flush であり、SDK が権威的ターン終了と定義する idle より早く届く。
|
|
1091
|
-
// assistant / idle はターンの進行/完了 = permission 応答後に必ず流れるため、
|
|
1092
|
-
// ここで inputPending を落として「確認待ち」ドット/通知を自己解消させる。
|
|
1093
|
-
if (event?.type === "assistant") {
|
|
1094
|
-
// 分母は result で学習したセッション固有の窓サイズ (無ければグローバル概算)。
|
|
1095
|
-
// マシン全体で 1 個の statusLine 値を使うと、別モデルの並走で 5 倍ずれる。
|
|
1096
|
-
const pct = resolveChatContextPct(cwd, "claude", event)
|
|
1097
|
-
try {
|
|
1098
|
-
// cliKind は chat-signals のキーの一部 (T05945)。同一 cwd で走る codex の信号と
|
|
1099
|
-
// 混ざらないよう、claude 由来のイベントは必ず "claude" バケットへ書く。
|
|
1100
|
-
// sessionId は同一 CLI 内でのセッション乗り換え検知用 identity (S1)。
|
|
1101
|
-
recordChatActivity(cwd, "claude", {
|
|
1102
|
-
status: "processing",
|
|
1103
|
-
contextPct: pct,
|
|
1104
|
-
inputPending: false,
|
|
1105
|
-
sessionId: session_id || null,
|
|
1106
|
-
})
|
|
1107
|
-
} catch {
|
|
1108
|
-
/* ignore */
|
|
1109
|
-
}
|
|
1110
|
-
} else if (event?.type === "result") {
|
|
1111
|
-
// SDK の権威的な窓サイズ (result.modelUsage[model].contextWindow) をここで学習する。
|
|
1112
|
-
resolveChatContextPct(cwd, "claude", event)
|
|
1113
|
-
} else if (
|
|
1114
|
-
event?.type === "system" &&
|
|
1115
|
-
event?.subtype === "session_state_changed" &&
|
|
1116
|
-
event?.state === "idle"
|
|
1117
|
-
) {
|
|
1118
|
-
try {
|
|
1119
|
-
recordChatActivity(cwd, "claude", {
|
|
1120
|
-
status: "waiting",
|
|
1121
|
-
inputPending: false,
|
|
1122
|
-
sessionId: session_id || null,
|
|
1123
|
-
})
|
|
1124
|
-
} catch {
|
|
1125
|
-
/* ignore */
|
|
1126
|
-
}
|
|
1127
|
-
} else if (event?.type === "stream_event") {
|
|
1128
|
-
// RC4 (症状④): 拡張思考 (thinking_delta) や長時間ツール中の partial delta では
|
|
1129
|
-
// assistant / result が流れず statusAt / updatedAtMs が前進しないため、>90s の無音で
|
|
1130
|
-
// getChatSignal が CHAT_SIGNAL_STALE_MS により null を返し、capture-pane が idle な
|
|
1131
|
-
// 純 SDK チャットで三点リーダーが消える。stream_event 受信を throttle 付き processing
|
|
1132
|
-
// tick として記録し statusAt を前進させる (本文は読まずタイムスタンプ前進のみ)。
|
|
1133
|
-
try {
|
|
1134
|
-
recordChatStreamTick(cwd, "claude")
|
|
1135
|
-
} catch {
|
|
1136
|
-
/* ignore */
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1084
|
+
resolveChatContextPct(session, event)
|
|
1139
1085
|
client.send({ type: "claude.event", stream_id, session_id, event })
|
|
1140
1086
|
})
|
|
1141
1087
|
claudeBridge.on("permission", ({ stream_id, session_id, cwd, request_id, tool_name, input }) => {
|
|
1142
|
-
// 確認待ち信号 (2026-06-02): (cwd, cliKind) キーで inputPending を立て、サイドバーの
|
|
1143
|
-
// 「確認待ち」ドット & 通知の発火源にする。次の assistant/result で落ちる。
|
|
1144
|
-
// これにより「裏で動いているセッションがどれか分からない」発見性の問題を解消する。
|
|
1145
|
-
// T05945: 旧実装は cliKind を伝播しておらず、codex の承認カードでも claude 行の
|
|
1146
|
-
// 確認待ちドットが点く (逆も然り) 交差汚染があった。CLI を明示して分離する。
|
|
1147
|
-
if (cwd) {
|
|
1148
|
-
try {
|
|
1149
|
-
recordChatActivity(cwd, "claude", { inputPending: true })
|
|
1150
|
-
} catch {
|
|
1151
|
-
/* ignore */
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
1088
|
// 多端末共有: session_id を載せて backend が session group へ broadcast できるように
|
|
1155
1089
|
// する (全端末に許可ダイアログ → 先着 1 件採用)。
|
|
1156
1090
|
client.send({
|
|
@@ -1182,34 +1116,14 @@ export async function startDaemon({ version, claudeSdk } = {}) {
|
|
|
1182
1116
|
/* ignore */
|
|
1183
1117
|
}
|
|
1184
1118
|
}
|
|
1185
|
-
if (cwd && request_ids?.length) {
|
|
1186
|
-
// 確認待ちドットも落とす (次の assistant/result を待たずに自己解消させる)。
|
|
1187
|
-
try {
|
|
1188
|
-
recordChatActivity(cwd, "claude", { inputPending: false })
|
|
1189
|
-
} catch {
|
|
1190
|
-
/* ignore */
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
1119
|
},
|
|
1194
1120
|
)
|
|
1195
|
-
claudeBridge.on("exit", ({ stream_id, code, reason, session_id }) => {
|
|
1121
|
+
claudeBridge.on("exit", ({ stream_id, code, reason, session_id, cwd }) => {
|
|
1196
1122
|
client.send({ type: "claude.exit", stream_id, code, reason, session_id })
|
|
1197
1123
|
})
|
|
1198
1124
|
claudeBridge.on("sessionoptions", (message) => {
|
|
1199
1125
|
client.send(buildClaudeSessionOptionsPayload(message))
|
|
1200
1126
|
})
|
|
1201
|
-
claudeBridge.on("turnsettled", ({ cwd }) => {
|
|
1202
|
-
// RC-4: result を届けずにターンが終わった (abort/異常終了)。チャット信号の
|
|
1203
|
-
// status=processing を waiting に落として三点リーダー/ステータスドットの固着を防ぐ
|
|
1204
|
-
// (正常完了は上の result ハンドラが既に waiting にしている)。
|
|
1205
|
-
if (cwd) {
|
|
1206
|
-
try {
|
|
1207
|
-
recordChatActivity(cwd, "claude", { status: "waiting", inputPending: false })
|
|
1208
|
-
} catch {
|
|
1209
|
-
/* ignore */
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
})
|
|
1213
1127
|
claudeBridge.on("error", ({ stream_id, session_id, error }) => {
|
|
1214
1128
|
client.send({ type: "claude.error", stream_id, session_id, error })
|
|
1215
1129
|
})
|
|
@@ -1218,97 +1132,15 @@ export async function startDaemon({ version, claudeSdk } = {}) {
|
|
|
1218
1132
|
// codex 版 (Slice 3): codex-stream-bridge.mjs の生 notification/permission/exit/error を
|
|
1219
1133
|
// browser にそのまま転送する。claudeBridge と同じ「素通し」方針 (codex-stream-bridge.mjs
|
|
1220
1134
|
// 冒頭コメント参照)。claude 版にある rate_limit/MCP control query 相当の処理は無い
|
|
1221
|
-
// (codex 側にまだ存在しない/後続スライス)
|
|
1222
|
-
// turn/started・turn/completed のみ最小限で反映する (claude の assistant/result 相当)。
|
|
1135
|
+
// (codex 側にまだ存在しない/後続スライス)。live usage だけを producer 単位で記録する。
|
|
1223
1136
|
if (codexBridge) {
|
|
1224
|
-
// T06407 増分2 (codex-review round1 major#3): この producer(session.seq) が cwd の
|
|
1225
|
-
// **権威 producer** (liveSessionForCwd = 会話ターン最新) かを判定する。チャット信号
|
|
1226
|
-
// (recordChatActivity) の context/sessionId は権威 producer の分だけ書く。非権威 producer
|
|
1227
|
-
// の context/sessionId をチャット信号へ書くと、権威 thread の live 未取得の窓で overlay 経由
|
|
1228
|
-
// に混入し、usage.response (権威 producer ベース) と分岐してドーナツが前後する。
|
|
1229
|
-
const isCodexAuthority = (cwd, producerSeq) =>
|
|
1230
|
-
typeof producerSeq === "number" &&
|
|
1231
|
-
codexBridge?.liveSessionForCwd?.(cwd)?.seq === producerSeq
|
|
1232
1137
|
codexBridge.on("event", ({ stream_id, producer_seq, thread_id, cwd, notification }) => {
|
|
1233
|
-
if (notification.method === "
|
|
1234
|
-
|
|
1235
|
-
// (processing/waiting) こそ更新されるがコンテキスト% は claude 由来の古い値の
|
|
1236
|
-
// ままだった (症状③の一因)。codexUsageForThread は rollout ファイルの I/O を
|
|
1237
|
-
// 伴うため fire-and-forget (await せず、取得できた時点で反映)。
|
|
1238
|
-
;(async () => {
|
|
1239
|
-
let contextPct = null
|
|
1240
|
-
try {
|
|
1241
|
-
const usage = await codexUsageForThread(thread_id, { producerSeq: producer_seq })
|
|
1242
|
-
contextPct = typeof usage?.context === "number" ? usage.context : null
|
|
1243
|
-
} catch {
|
|
1244
|
-
/* ignore */
|
|
1245
|
-
}
|
|
1246
|
-
try {
|
|
1247
|
-
recordChatActivity(
|
|
1248
|
-
cwd,
|
|
1249
|
-
"codex",
|
|
1250
|
-
buildCodexChatPatch("processing", contextPct, thread_id, isCodexAuthority(cwd, producer_seq)),
|
|
1251
|
-
)
|
|
1252
|
-
} catch {
|
|
1253
|
-
/* ignore */
|
|
1254
|
-
}
|
|
1255
|
-
})()
|
|
1256
|
-
} else if (notification.method === "turn/completed") {
|
|
1257
|
-
;(async () => {
|
|
1258
|
-
let contextPct = null
|
|
1259
|
-
try {
|
|
1260
|
-
const usage = await codexUsageForThread(thread_id, { producerSeq: producer_seq })
|
|
1261
|
-
contextPct = typeof usage?.context === "number" ? usage.context : null
|
|
1262
|
-
} catch {
|
|
1263
|
-
/* ignore */
|
|
1264
|
-
}
|
|
1265
|
-
try {
|
|
1266
|
-
recordChatActivity(
|
|
1267
|
-
cwd,
|
|
1268
|
-
"codex",
|
|
1269
|
-
buildCodexChatPatch("waiting", contextPct, thread_id, isCodexAuthority(cwd, producer_seq)),
|
|
1270
|
-
)
|
|
1271
|
-
} catch {
|
|
1272
|
-
/* ignore */
|
|
1273
|
-
}
|
|
1274
|
-
})()
|
|
1275
|
-
} else if (notification.method === "thread/tokenUsage/updated") {
|
|
1276
|
-
// T06407 増分2 (症状⑥ ドーナツ前後動の根治): App Server の live tokenUsage を
|
|
1277
|
-
// context% の正本にする。**producer(session.seq) 単位**でストアへ記録し (usage.response /
|
|
1278
|
-
// state loop が cwd の権威 producer の seq で参照する)、同時にチャット信号 (overlay 経路) の
|
|
1279
|
-
// context_pct も同じ live 値で更新する。status は上書きしない (このイベントはターン進行中に
|
|
1280
|
-
// 届くため keep-prev。processing/waiting は turn/started・turn/completed が管理する)。
|
|
1281
|
-
const tokenUsage = notification.params?.tokenUsage
|
|
1282
|
-
if (typeof producer_seq === "number" && tokenUsage) {
|
|
1283
|
-
try {
|
|
1284
|
-
recordCodexLiveTokenUsage(producer_seq, tokenUsage)
|
|
1285
|
-
} catch {
|
|
1286
|
-
/* ignore */
|
|
1287
|
-
}
|
|
1288
|
-
// major#3: チャット信号の context/sessionId は権威 producer のときだけ書く。
|
|
1289
|
-
if (isCodexAuthority(cwd, producer_seq)) {
|
|
1290
|
-
const contextPct = codexContextPercentFromTokenUsage(tokenUsage)
|
|
1291
|
-
if (typeof contextPct === "number" && cwd) {
|
|
1292
|
-
try {
|
|
1293
|
-
recordChatActivity(cwd, "codex", { contextPct, sessionId: thread_id })
|
|
1294
|
-
} catch {
|
|
1295
|
-
/* ignore */
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1138
|
+
if (notification.method === "thread/tokenUsage/updated") {
|
|
1139
|
+
recordCodexLiveTokenUsage(producer_seq, notification.params?.tokenUsage)
|
|
1300
1140
|
}
|
|
1301
1141
|
client.send({ type: "codex.event", stream_id, thread_id, event: notification })
|
|
1302
1142
|
})
|
|
1303
1143
|
codexBridge.on("permission", ({ stream_id, thread_id, cwd, request_id, method, params }) => {
|
|
1304
|
-
if (cwd) {
|
|
1305
|
-
try {
|
|
1306
|
-
// T05945: codex の承認待ちは codex 行にだけ立てる (claude 行へ漏らさない)。
|
|
1307
|
-
recordChatActivity(cwd, "codex", { inputPending: true })
|
|
1308
|
-
} catch {
|
|
1309
|
-
/* ignore */
|
|
1310
|
-
}
|
|
1311
|
-
}
|
|
1312
1144
|
client.send({
|
|
1313
1145
|
type: "codex.permission.request",
|
|
1314
1146
|
stream_id,
|
|
@@ -1318,7 +1150,7 @@ export async function startDaemon({ version, claudeSdk } = {}) {
|
|
|
1318
1150
|
params,
|
|
1319
1151
|
})
|
|
1320
1152
|
})
|
|
1321
|
-
codexBridge.on("exit", ({ stream_id, producer_seq, thread_id, code, reason }) => {
|
|
1153
|
+
codexBridge.on("exit", ({ stream_id, producer_seq, thread_id, code, reason, cwd }) => {
|
|
1322
1154
|
// T06407 増分2: この producer(session.seq) の live tokenUsage スロットを破棄する。
|
|
1323
1155
|
// seq は immutable なので、reattach で stream_id が付け替わっても alias slot / Map リークは
|
|
1324
1156
|
// 生じない (stream_id キー時代の major#1 を解消)。
|
|
@@ -1554,31 +1386,6 @@ export async function refreshContextWindow() {
|
|
|
1554
1386
|
}
|
|
1555
1387
|
}
|
|
1556
1388
|
|
|
1557
|
-
/**
|
|
1558
|
-
* セッション単位で学習した context 窓サイズ (トークン)。
|
|
1559
|
-
* キーは `${cliKind}\u0000${cwd}` (chat-signals と同じ粒度)。
|
|
1560
|
-
*
|
|
1561
|
-
* 【なぜセッション単位が要るか】(2026-07-10 監査)
|
|
1562
|
-
* 旧実装は分子 (SDK assistant.usage = セッション単位) に対し、分母を
|
|
1563
|
-
* resolvedContextWindow = statusLine cache (`~/.hub/usage/latest.json`) 由来の
|
|
1564
|
-
* **マシン全体で 1 個のグローバル値**にしていた。同ファイルは「最後に statusLine を
|
|
1565
|
-
* 書いた任意のセッション」の値なので、1M 窓の Opus と 200k 窓の Sonnet を同時に
|
|
1566
|
-
* 走らせると、分母が取り違わってドーナツが 5 倍ずれる (分子と分母のスケール不一致)。
|
|
1567
|
-
*
|
|
1568
|
-
* SDK は `result.modelUsage[model].contextWindow` として **そのターンで実際に使われた
|
|
1569
|
-
* モデルの窓サイズ**を毎ターン渡してくる。これが唯一の権威なので、result のたびに
|
|
1570
|
-
* ここへ学習し、以降の assistant イベントの分母に使う。
|
|
1571
|
-
*/
|
|
1572
|
-
const contextWindowByChatKey = new Map()
|
|
1573
|
-
|
|
1574
|
-
/** 直近の assistant メッセージのモデル名。result の modelUsage から主対話モデルを選ぶ手掛かり。 */
|
|
1575
|
-
const lastAssistantModelByChatKey = new Map()
|
|
1576
|
-
|
|
1577
|
-
/** chat-signals と同じ (cwd, cliKind) 粒度のキーを作る。 */
|
|
1578
|
-
export function chatKey(cwd, cliKind) {
|
|
1579
|
-
return `${cliKind || "claude"}\u0000${cwd || ""}`
|
|
1580
|
-
}
|
|
1581
|
-
|
|
1582
1389
|
/**
|
|
1583
1390
|
* SDK `result` の `modelUsage` から、そのセッションの context 窓サイズを選ぶ。
|
|
1584
1391
|
*
|
|
@@ -1638,38 +1445,28 @@ export function contextPctFromUsage(u, contextWindow) {
|
|
|
1638
1445
|
return Math.min(100, Math.round((tokens / win) * 1000) / 10)
|
|
1639
1446
|
}
|
|
1640
1447
|
|
|
1641
|
-
/**
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
* ハンドラ本体から切り出して単体テスト可能にしている (学習 → 次ターンで反映、
|
|
1646
|
-
* という状態遷移こそがこの機能の本体であり、純関数だけでは配線を検証できないため)。
|
|
1647
|
-
*
|
|
1648
|
-
* @returns {number|null} assistant のとき context% / それ以外は null
|
|
1649
|
-
*/
|
|
1650
|
-
export function resolveChatContextPct(cwd, cliKind, event) {
|
|
1651
|
-
const key = chatKey(cwd, cliKind)
|
|
1448
|
+
/** usage・主対話モデル・窓サイズは、イベントを生成した実体に保持する。 */
|
|
1449
|
+
export function resolveChatContextPct(session, event) {
|
|
1450
|
+
if (!session || session._closed) return null
|
|
1451
|
+
const context = conversationContext(session, "claude")
|
|
1652
1452
|
if (event?.type === "assistant") {
|
|
1653
|
-
|
|
1654
|
-
if (
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
const win = pickContextWindow(event.modelUsage,
|
|
1662
|
-
if (win)
|
|
1453
|
+
if (event.message?.model) context.model = event.message.model
|
|
1454
|
+
if (event.message?.usage) {
|
|
1455
|
+
context.usage = event.message.usage
|
|
1456
|
+
context.live = true
|
|
1457
|
+
context.percent = contextPctFromUsage(context.usage, context.window)
|
|
1458
|
+
return context.percent
|
|
1459
|
+
}
|
|
1460
|
+
} else if (event?.type === "result") {
|
|
1461
|
+
const win = pickContextWindow(event.modelUsage, context.model)
|
|
1462
|
+
if (win) {
|
|
1463
|
+
context.window = win
|
|
1464
|
+
if (context.usage) context.percent = contextPctFromUsage(context.usage, win)
|
|
1465
|
+
}
|
|
1663
1466
|
}
|
|
1664
1467
|
return null
|
|
1665
1468
|
}
|
|
1666
1469
|
|
|
1667
|
-
/** テスト用: 学習済みの窓サイズ / モデル名をリセットする。 */
|
|
1668
|
-
export function _resetContextWindowLearning() {
|
|
1669
|
-
contextWindowByChatKey.clear()
|
|
1670
|
-
lastAssistantModelByChatKey.clear()
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
1470
|
/**
|
|
1674
1471
|
* session_event の「サイドバー並び順に効くか (sort_affecting)」判定用の per-session 状態。
|
|
1675
1472
|
* 「その session-run で実ユーザーターン (prompt_submit) を見たか」を持つ。
|
|
@@ -1979,7 +1776,7 @@ async function readAllSessionEvents() {
|
|
|
1979
1776
|
// 複数 Cockpit タブは各自 8 秒 poll を持つため、位相がずれると agent は同じ
|
|
1980
1777
|
// tmux/worktree/jsonl スナップショットを数秒おきに再構築してしまう。5 秒だけ成功値を
|
|
1981
1778
|
// 共有し、同時要求は single-flight に畳む。create/kill/worktree 変更時は明示 clear する。
|
|
1982
|
-
const tmuxSessionsSnapshotCache = new AsyncTtlCache({ ttlMs: 5_000 })
|
|
1779
|
+
export const tmuxSessionsSnapshotCache = new AsyncTtlCache({ ttlMs: 5_000 })
|
|
1983
1780
|
|
|
1984
1781
|
/**
|
|
1985
1782
|
* `/tmp/cockpit_session_events/<tmux_session>.json` を fs.watch して
|
|
@@ -2260,7 +2057,7 @@ export function enrichTmuxSessionsForListResponse(sessions, lastEventByName, get
|
|
|
2260
2057
|
}
|
|
2261
2058
|
|
|
2262
2059
|
/**
|
|
2263
|
-
* state loop 内で「チャット信号 (
|
|
2060
|
+
* state loop 内で「チャット信号 (readRowChatState) でペイン status/context_pct を上書きするか」
|
|
2264
2061
|
* を決める純関数 (startStateLoop から抽出、単体テスト用)。
|
|
2265
2062
|
*
|
|
2266
2063
|
* バグ修正 (2026-07): 従来コードは書き換え「後」の status で processing ガードを判定して
|
|
@@ -2273,7 +2070,7 @@ export function enrichTmuxSessionsForListResponse(sessions, lastEventByName, get
|
|
|
2273
2070
|
*
|
|
2274
2071
|
* @param {string} paneStatus capture-pane 由来の生 status (上書き前)
|
|
2275
2072
|
* @param {number|null|undefined} paneContextPct capture-pane 由来の生 context_pct
|
|
2276
|
-
* @param {{status?:string|null, context_pct?:number|null}|null} chat
|
|
2073
|
+
* @param {{status?:string|null, context_pct?:number|null}|null} chat readRowChatState() の戻り値
|
|
2277
2074
|
* @returns {{status:string, contextPct:number|null|undefined}}
|
|
2278
2075
|
*/
|
|
2279
2076
|
export function applyChatOverlay(paneStatus, paneContextPct, chat) {
|
|
@@ -2289,26 +2086,35 @@ export function applyChatOverlay(paneStatus, paneContextPct, chat) {
|
|
|
2289
2086
|
return { status, contextPct }
|
|
2290
2087
|
}
|
|
2291
2088
|
|
|
2089
|
+
function codexRowSession(bridge, cwd) {
|
|
2090
|
+
// 代表行(終了印を含む)は代表実体だけを見る。背景会話へ落ちると終了後も履歴 I/O が走る。
|
|
2091
|
+
if (hasRepresentativeRow(cwd, "codex")) {
|
|
2092
|
+
const rep = getRepresentativeConversation(cwd, "codex")
|
|
2093
|
+
return rep && !rep.dead ? rep : null
|
|
2094
|
+
}
|
|
2095
|
+
return bridge?.liveSessionForCwd?.(cwd) ?? null
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2292
2098
|
/**
|
|
2293
|
-
*
|
|
2294
|
-
*
|
|
2295
|
-
* 常に書き、**context_pct / sessionId は権威 producer のときだけ**含める。非権威 producer の
|
|
2296
|
-
* context/sessionId をチャット信号へ書くと、権威 thread の live 未取得の窓で overlay 経由に
|
|
2297
|
-
* 混入し、usage.response (権威 producer ベース) と分岐してドーナツが前後する。
|
|
2298
|
-
*
|
|
2299
|
-
* @param {'processing'|'waiting'} status
|
|
2300
|
-
* @param {number|null} contextPct
|
|
2301
|
-
* @param {string|null} threadId
|
|
2302
|
-
* @param {boolean} isAuthority この producer が cwd の権威 (liveSessionForCwd) か
|
|
2303
|
-
* @returns {{status:string, inputPending:boolean, contextPct?:number, sessionId?:string|null}}
|
|
2099
|
+
* codex 行の使用率を、行の代表会話の live 値で上書きするかを決める。
|
|
2100
|
+
* @returns {number|null}
|
|
2304
2101
|
*/
|
|
2305
|
-
export function
|
|
2306
|
-
|
|
2307
|
-
if (
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2102
|
+
export function resolveCodexRowContextPct({ contextPct, paneIsProcessing, rowSession, liveContextPct }) {
|
|
2103
|
+
if (typeof rowSession?.seq !== "number" || paneIsProcessing) return contextPct
|
|
2104
|
+
if (typeof liveContextPct === "number") return liveContextPct
|
|
2105
|
+
if (rowSession.isFresh === true) return 0
|
|
2106
|
+
return contextPct
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
/** 代表行は状態・使用率・ID・承認待ちを一度に上書きする。 */
|
|
2110
|
+
export function applyRowChatState(payload, cwd, kind) {
|
|
2111
|
+
const chat = readRowChatState(cwd, kind)
|
|
2112
|
+
if (!chat) return payload
|
|
2113
|
+
return { ...payload, status: chat.status, context_pct: chat.context_pct,
|
|
2114
|
+
context_session_id: chat.sessionId, chat_session_id: chat.sessionId,
|
|
2115
|
+
turn_active: chat.turn_active, inputPending: chat.inputPending, input_pending: chat.inputPending,
|
|
2116
|
+
...(Object.hasOwn(payload, "pointer_context_pct") ? { pointer_context_pct: chat.context_pct } : {}),
|
|
2117
|
+
stable: chat.status !== "processing", proc_busy: false, child_busy: false, stalled: false }
|
|
2312
2118
|
}
|
|
2313
2119
|
|
|
2314
2120
|
/** 次ターンに使う desired options。現在ターンの setter 成功を観測した値ではない。 */
|
|
@@ -2450,7 +2256,7 @@ export function filterCodexSessionsByAuthority(codexSessions, producerByCwd, seq
|
|
|
2450
2256
|
* `listSessionStatesFn` は単体テスト用の DI ポイント (既定は実 `listSessionStates`、tmux を
|
|
2451
2257
|
* 起動する)。実運用では省略してよい。
|
|
2452
2258
|
*
|
|
2453
|
-
* @param {{ client: object, plugins: object, logger?: object, cwd: string, cliKind: 'claude'|'codex', sessionId: string|null, contextPct?: number|null, listSessionStatesFn?: Function }} args
|
|
2259
|
+
* @param {{ client: object, plugins: object, logger?: object, cwd: string, cliKind: 'claude'|'codex', sessionId: string|null, streamId?: string|null, contextPct?: number|null, listSessionStatesFn?: Function }} args
|
|
2454
2260
|
*/
|
|
2455
2261
|
export async function pushImmediateChatState({
|
|
2456
2262
|
client,
|
|
@@ -2459,6 +2265,7 @@ export async function pushImmediateChatState({
|
|
|
2459
2265
|
cwd,
|
|
2460
2266
|
cliKind,
|
|
2461
2267
|
sessionId,
|
|
2268
|
+
streamId,
|
|
2462
2269
|
contextPct = 0,
|
|
2463
2270
|
listSessionStatesFn = listSessionStates,
|
|
2464
2271
|
}) {
|
|
@@ -2468,23 +2275,26 @@ export async function pushImmediateChatState({
|
|
|
2468
2275
|
for (const s of states) {
|
|
2469
2276
|
if (s.cwd !== cwd) continue
|
|
2470
2277
|
if (cliKindForSessionName(s.session_name) !== cliKind) continue
|
|
2278
|
+
const status = "waiting"
|
|
2279
|
+
// 代表行は直後の applyRowChatState が代表会話の実 ID で上書きする。
|
|
2280
|
+
const contextSessionId = sessionId ?? null
|
|
2471
2281
|
tmuxSessionsSnapshotCache.clear()
|
|
2472
|
-
client.send({
|
|
2282
|
+
client.send(applyRowChatState({
|
|
2473
2283
|
type: "session.state",
|
|
2474
2284
|
session_name: s.session_name,
|
|
2475
|
-
status
|
|
2476
|
-
context_pct: typeof contextPct === "number" ? contextPct :
|
|
2477
|
-
stable:
|
|
2285
|
+
status,
|
|
2286
|
+
context_pct: typeof contextPct === "number" ? contextPct : null,
|
|
2287
|
+
stable: status !== "processing",
|
|
2478
2288
|
proc_busy: false,
|
|
2479
2289
|
child_busy: false,
|
|
2480
2290
|
stalled: false,
|
|
2481
|
-
turn_active:
|
|
2291
|
+
turn_active: status == null ? null : status === "processing",
|
|
2482
2292
|
cli_kind: cliKind,
|
|
2483
2293
|
// context_pct がどの会話の値かを明示する。frontend は ID が変わったときだけ
|
|
2484
2294
|
// 0% を採用し、同一会話の一時的な 0/null は直近の確定値を保持する。
|
|
2485
|
-
context_session_id:
|
|
2486
|
-
chat_session_id:
|
|
2487
|
-
})
|
|
2295
|
+
context_session_id: contextSessionId,
|
|
2296
|
+
chat_session_id: contextSessionId,
|
|
2297
|
+
}, cwd, cliKind))
|
|
2488
2298
|
}
|
|
2489
2299
|
} catch (err) {
|
|
2490
2300
|
logger?.warn?.({ err: err?.message, cwd }, "pushImmediateChatState failed")
|
|
@@ -2622,7 +2432,7 @@ export function decideTurnActiveSource({
|
|
|
2622
2432
|
return "none"
|
|
2623
2433
|
}
|
|
2624
2434
|
|
|
2625
|
-
export function startStateLoop({ client, plugins, logger, intervalMs, claudeBridge, codexBridge, readinessTracker, boundSessions }) {
|
|
2435
|
+
export function startStateLoop({ client, plugins, logger, intervalMs, claudeBridge, codexBridge, readinessTracker, boundSessions, listSessionStatesFn = listSessionStates, writeSessionEventFileFn = writeSessionEventFile }) {
|
|
2626
2436
|
const lastByName = new Map() // session_name → {status, context_pct}
|
|
2627
2437
|
const lastTurnAtByName = new Map() // session_name → 最後に event ファイル化した turnAt
|
|
2628
2438
|
// session_name → 最後に書いた chat 由来 event 種別。エッジ駆動 (種別遷移時のみ書く) にして
|
|
@@ -2695,7 +2505,7 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
|
|
|
2695
2505
|
// 実コンテキスト窓サイズ (1M ベータ等) を反映。contextPctFromUsage の分母を
|
|
2696
2506
|
// 5s ごとに最新化し、ドーナツが 200k 固定で振り切れるのを防ぐ。
|
|
2697
2507
|
await refreshContextWindow()
|
|
2698
|
-
const states = await
|
|
2508
|
+
const states = await listSessionStatesFn({ plugins, logger })
|
|
2699
2509
|
for (const s of states) {
|
|
2700
2510
|
// T05945: この行がどの CLI のセッションかは **セッション名から構造的に決まる**
|
|
2701
2511
|
// (`<dir>` = claude / `<dir>__codex` = codex)。chat 信号の読み出し・turn_active の
|
|
@@ -2704,50 +2514,18 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
|
|
|
2704
2514
|
// チャットモード補完: tmux ペインのスクレイプは TUI 前提で効かないため、
|
|
2705
2515
|
// session の cwd に一致する新鮮なチャット信号があれば status/context% を
|
|
2706
2516
|
// 上書きする (Hub ホスト型 Cockpit のステータスドット/各行ドーナツ用)。
|
|
2707
|
-
//
|
|
2708
|
-
//
|
|
2709
|
-
// 内部で必ず「上書き前の pane 由来 status (s.status)」を使う。2026-07 バグ修正:
|
|
2710
|
-
// 旧実装は書き換え後の status で判定していたため、SDK チャット生成中はドーナツが
|
|
2711
|
-
// 一切更新されなかった)。素のシェル (SDK チャットのペイン) は processing を
|
|
2712
|
-
// 返せないため、SDK チャットの補完は従来どおり効く。TUI 生成中に stale な SDK
|
|
2713
|
-
// status (waiting) が勝ってステータスドット/三点リーダーが消える事故の防止
|
|
2714
|
-
// (2026-06-12) は paneIsProcessing ガードのまま維持される。
|
|
2517
|
+
// 代表が設定済みなら、その会話の値だけを使い、欠けた値は null とする。
|
|
2518
|
+
// 代表未設定の行は従来の applyChatOverlay / pane processing ガードを使う。
|
|
2715
2519
|
// (cwd, cliKind) キーで自 CLI のチャット信号だけを読む。同一 cwd の別 CLI 行が
|
|
2716
2520
|
// 立てた status/context_pct を拾わない (交差汚染の遮断)。
|
|
2717
|
-
const
|
|
2521
|
+
const representative = hasRepresentativeRow(s.cwd, sessionCliKind)
|
|
2522
|
+
const chat = s.cwd ? readRowChatState(s.cwd, sessionCliKind) : null
|
|
2718
2523
|
const overlay = applyChatOverlay(s.status, s.context_pct, chat)
|
|
2719
|
-
let status = overlay.status
|
|
2720
|
-
let contextPct = overlay.contextPct
|
|
2721
|
-
if (chat) {
|
|
2722
|
-
// チャットのターン境界を sort / 状態用 session-event に橋渡しする。
|
|
2723
|
-
// tmux ペインが動かず bundle hook が発火しないため、ここで代替発火する。
|
|
2724
|
-
//
|
|
2725
|
-
// ⚠️ **エッジ駆動** (種別が変わったときだけ書く)。旧実装は turnAt が前進するたびに
|
|
2726
|
-
// 書いており、turnAt は assistant イベント毎に前進するため **生成中ずっと
|
|
2727
|
-
// prompt_submit を書き続けていた**。並行生成中のセッション同士が assistant の
|
|
2728
|
-
// 到着順で入れ替わり、サイドバーが揺れる原因になる (2026-07-29)。
|
|
2729
|
-
// 種別遷移でのみ書けば、prompt_submit は生成開始の 1 回だけになる。
|
|
2730
|
-
//
|
|
2731
|
-
// 確認待ち (inputPending) は専用 event "input_required"、生成中は "prompt_submit"、
|
|
2732
|
-
// それ以外は "stop"。並び順に効くのは computeSortAffecting により prompt_submit だけ。
|
|
2733
|
-
const prevTurnAt = lastTurnAtByName.get(s.session_name) || 0
|
|
2734
|
-
const prevEv = lastChatEventByName.get(s.session_name)
|
|
2735
|
-
const eventForTick = chatSessionEventForTick({
|
|
2736
|
-
chat,
|
|
2737
|
-
prevEv,
|
|
2738
|
-
prevTurnAt,
|
|
2739
|
-
directAt: s.cwd ? getPromptAcceptedAt(s.cwd, sessionCliKind) : undefined,
|
|
2740
|
-
})
|
|
2741
|
-
if (eventForTick) {
|
|
2742
|
-
const { ev, at } = eventForTick
|
|
2743
|
-
lastTurnAtByName.set(s.session_name, chat.turnAt)
|
|
2744
|
-
lastChatEventByName.set(s.session_name, ev)
|
|
2745
|
-
writeSessionEventFile(s.session_name, ev, at).catch(() => {})
|
|
2746
|
-
}
|
|
2747
|
-
}
|
|
2524
|
+
let status = representative ? chat?.status ?? null : overlay.status
|
|
2525
|
+
let contextPct = representative ? chat?.context_pct ?? null : overlay.contextPct
|
|
2748
2526
|
// 内容安定で「確実に停止」と判定できたか (frontend が 2 サンプル消灯確認を省いて
|
|
2749
2527
|
// 即消灯するためのフラグ)。chat 信号で processing へ上書きされた場合は false。
|
|
2750
|
-
|
|
2528
|
+
let stable = status !== "processing" && s.stable === true
|
|
2751
2529
|
// プロセス内省由来の busy レベル (Stop フック/ツール実行中=true)。frontend は
|
|
2752
2530
|
// これでペイン由来の早期消灯を抑止し、tool/Stop フック中もスピナーを保つ。
|
|
2753
2531
|
// 高速 readiness loop が維持する値をそのまま相乗せする (この loop では計算しない)。
|
|
@@ -2786,65 +2564,30 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
|
|
|
2786
2564
|
now: nowMs,
|
|
2787
2565
|
})
|
|
2788
2566
|
|
|
2789
|
-
// turn_active
|
|
2790
|
-
//
|
|
2791
|
-
// 同期させ、Esc 中断後の青固着 (PROMPT_STALE_SHORT_MS=60s) を解消する。
|
|
2792
|
-
// - チャットモード: SDK の rate_limit_event/assistant が最も新鮮なので chat.status を採用
|
|
2793
|
-
// - TUI モード: bound session の jsonl 末尾 assistant.stop_reason から判定 (turnActiveForCwd)
|
|
2794
|
-
// bound session_id を渡すことで、同一 cwd dir 内の別セッション / サブエージェント /
|
|
2795
|
-
// headless `claude -p` / `/clear` ローテの jsonl を「最新」と取り違えて turn_active を
|
|
2796
|
-
// 誤る (cross-activity contamination = メタ欠陥#1) のを防ぐ。
|
|
2797
|
-
// T2: tool_use 末尾 active 中の中断マーカー無しクラッシュ判定のため、tmux pane_current_command
|
|
2798
|
-
// から claude プロセス生存を 3 値で返す paneClaudeAliveOrUnknown を probe として渡す。
|
|
2799
|
-
// 'dead' (シェル前景)→active=false に倒す。'alive'/'unknown' は active 維持(偽陰性ゼロ)。
|
|
2800
|
-
// - チャット信号が stale (90s 無活動) でこの else-if に落ちるケース: SDK チャットは
|
|
2801
|
-
// tmux bind 台帳 (tuiReboundSessions) を持たないため、従来は boundSessionId が常に
|
|
2802
|
-
// null を返し mtime 最新 jsonl へフォールバックしていた (chat 版の cross-activity
|
|
2803
|
-
// 汚染, 2026-07 発覚)。resolveStateLoopBoundSessionId が claudeBridge のライブ
|
|
2804
|
-
// セッション id へフォールバックすることで TUI と同じ方式で根治する。
|
|
2805
|
-
// - Codex (T05699 残タスク #1, 2026-07-04): この cwd にライブな codexBridge セッション
|
|
2806
|
-
// (chat モード) があれば、chat 信号の 90s TTL に依存しない rollout ベースの判定
|
|
2807
|
-
// (codexTurnActiveForThread, task_started/task_complete イベント対) を使う。長時間
|
|
2808
|
-
// ターン中に chat 信号が stale 化してドットが消える欠落を根治する。Claude 版のような
|
|
2809
|
-
// paneAliveProbe は渡さない (chat モードのペインは素のシェルのまま = 実処理は別
|
|
2810
|
-
// プロセスのため、pane 生存判定を適用すると誤って dead 扱いになる)。cwd がライブな
|
|
2811
|
-
// codex セッションと一致しなければ従来通り Claude jsonl (turnActiveForCwd) にフォール
|
|
2812
|
-
// バックする (TUI/bare codex pane は現状 bound thread 解決が無いため、これまで通り
|
|
2813
|
-
// null=不明のまま、退行はしない)。
|
|
2814
|
-
// 分岐の優先順位は decideTurnActiveSource (純関数、単体テスト済み) が権威。
|
|
2567
|
+
// 代表行は下の同期読み出しが turn_active も確定する。
|
|
2568
|
+
// 代表未設定の行だけ、従来の履歴・pane probe を使う。
|
|
2815
2569
|
let turnActive = null
|
|
2816
2570
|
// codex 行のときだけ rollout ベース判定の材料を引く (claude 行では常に null)。
|
|
2817
|
-
//
|
|
2818
|
-
// seq(context% の live source 参照キー) の両方を同一 session
|
|
2819
|
-
// usage.response
|
|
2820
|
-
//
|
|
2571
|
+
// 行の**代表会話の session** を 1 度だけ引き、threadId(turn_active 判定用・会話 ID) と
|
|
2572
|
+
// seq(context% の live source 参照キー) の両方を同一 session から取る。⚠️ 別々に引くと
|
|
2573
|
+
// 「背景会話の使用率に代表会話の ID が付く」不整合になる。usage.response と同じ
|
|
2574
|
+
// codexRowSession を使い、代表が live でなければ null(別会話の値で埋めない)。
|
|
2821
2575
|
const codexAuthSession =
|
|
2822
2576
|
s.cwd && sessionCliKind === "codex"
|
|
2823
|
-
? codexBridge
|
|
2577
|
+
? codexRowSession(codexBridge, s.cwd)
|
|
2824
2578
|
: null
|
|
2825
2579
|
const codexThreadId = codexAuthSession?.threadId || null
|
|
2826
2580
|
// T06407 増分2 (axis3): codex 行の context%(ドーナツ) は TTL 非依存の live store
|
|
2827
2581
|
// (thread/tokenUsage/updated 由来) を権威 producer(seq) で引き、chat overlay より優先して
|
|
2828
|
-
//
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
!paneIsProcessing
|
|
2838
|
-
) {
|
|
2839
|
-
const liveCtx = codexLiveContextForProducer(codexAuthSession.seq)
|
|
2840
|
-
if (typeof liveCtx === "number") {
|
|
2841
|
-
contextPct = liveCtx
|
|
2842
|
-
} else if (codexAuthSession.isFresh === true) {
|
|
2843
|
-
// 新規 Codex thread は thread/start の時点で ID だけ確定し、最初の
|
|
2844
|
-
// usage.response までは使用量がない。この間に tmux の旧 pane 値を
|
|
2845
|
-
// 同じ新 thread ID で送ると、即時 push した 0% を打ち消してしまう。
|
|
2846
|
-
contextPct = 0
|
|
2847
|
-
}
|
|
2582
|
+
// 恒久収束させる。代表未設定の行だけ、従来の pane processing ガードを使う。
|
|
2583
|
+
const paneIsProcessing = !representative && s.status === "processing"
|
|
2584
|
+
if (sessionCliKind === "codex") {
|
|
2585
|
+
const liveCtx = typeof codexAuthSession?.seq === "number" && !paneIsProcessing
|
|
2586
|
+
? codexLiveContextForProducer(codexAuthSession.seq)
|
|
2587
|
+
: null
|
|
2588
|
+
contextPct = resolveCodexRowContextPct({
|
|
2589
|
+
contextPct, paneIsProcessing, rowSession: codexAuthSession, liveContextPct: liveCtx,
|
|
2590
|
+
})
|
|
2848
2591
|
}
|
|
2849
2592
|
// fresh bind は実 session_id がまだ確定していない。tmux.list の statusLine / cwd 最新
|
|
2850
2593
|
// JSONL は直前会話の値を返し得るため、この間は必ず 0% を正本にする。これをしないと
|
|
@@ -2852,11 +2595,11 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
|
|
|
2852
2595
|
// ドーナツを復活させる。
|
|
2853
2596
|
const freshUnbound =
|
|
2854
2597
|
sessionCliKind === "claude" && isFreshUnboundBind(boundSessions?.get(s.session_name))
|
|
2855
|
-
if (freshUnbound) contextPct = 0
|
|
2856
|
-
const turnActiveSource = decideTurnActiveSource({
|
|
2598
|
+
if (freshUnbound && !representative) contextPct = 0
|
|
2599
|
+
const turnActiveSource = representative ? "representative" : decideTurnActiveSource({
|
|
2857
2600
|
chatStatus: chat?.status,
|
|
2858
2601
|
cwd: s.cwd,
|
|
2859
|
-
isFreshUnbound: freshUnbound,
|
|
2602
|
+
isFreshUnbound: freshUnbound && !representative,
|
|
2860
2603
|
codexThreadId,
|
|
2861
2604
|
sessionCliKind,
|
|
2862
2605
|
})
|
|
@@ -2882,10 +2625,10 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
|
|
|
2882
2625
|
// 離れて detach され this.sessions から外れた warm セッションでも、稼働中ターンの turn_active
|
|
2883
2626
|
// が維持され、非選択行のドットが赤へ誤落ちしない。加えて bare-shell pane を dead 判定して
|
|
2884
2627
|
// 赤へ倒す paneAliveProbe 経路 (チャットは実体が SDK 子プロセスで pane は素のシェル) も回避する。
|
|
2885
|
-
//
|
|
2886
|
-
//
|
|
2628
|
+
// 代表があれば、その会話の busy だけを読む。不在なら null のままにする。
|
|
2629
|
+
// 代表未設定の行だけ、従来の cwd 最新 / jsonl / pane probe を使う。
|
|
2887
2630
|
const chatActive = claudeBridge?.chatTurnActiveForCwd?.(s.cwd)
|
|
2888
|
-
if (chatActive === true || chatActive === false) {
|
|
2631
|
+
if (representative || chatActive === true || chatActive === false) {
|
|
2889
2632
|
turnActive = chatActive
|
|
2890
2633
|
} else {
|
|
2891
2634
|
turnActive = await turnActiveForCwd(
|
|
@@ -2917,79 +2660,46 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
|
|
|
2917
2660
|
? boundSessions?.get(s.session_name) ?? `unbound:${s.session_name}`
|
|
2918
2661
|
: codexThreadId)
|
|
2919
2662
|
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2663
|
+
// 履歴・pane の非同期処理が終わった後に、現在の実体を同期で読む。
|
|
2664
|
+
const payload = applyRowChatState({
|
|
2665
|
+
status, context_pct: contextPct, stable, proc_busy: procBusy, child_busy: childBusy,
|
|
2666
|
+
stalled, turn_active: turnActive, cli_kind: chatCliKind,
|
|
2667
|
+
context_session_id: contextSessionId, chat_session_id: contextSessionId,
|
|
2668
|
+
}, s.cwd, sessionCliKind)
|
|
2669
|
+
const currentChat = readRowChatState(s.cwd, sessionCliKind)
|
|
2670
|
+
if (currentChat) {
|
|
2671
|
+
// チャットのターン境界を sort / 状態用 session-event に橋渡しする。
|
|
2672
|
+
// tmux ペインが動かず bundle hook が発火しないため、ここで代替発火する。
|
|
2673
|
+
//
|
|
2674
|
+
// ⚠️ **エッジ駆動** (種別が変わったときだけ書く)。旧実装は turnAt が前進するたびに
|
|
2675
|
+
// 書いており、turnAt は assistant イベント毎に前進するため **生成中ずっと
|
|
2676
|
+
// prompt_submit を書き続けていた**。並行生成中のセッション同士が assistant の
|
|
2677
|
+
// 到着順で入れ替わり、サイドバーが揺れる原因になる (2026-07-29)。
|
|
2678
|
+
// 種別遷移でのみ書けば、prompt_submit は生成開始の 1 回だけになる。
|
|
2679
|
+
//
|
|
2680
|
+
// 確認待ち (inputPending) は専用 event "input_required"、生成中は "prompt_submit"、
|
|
2681
|
+
// それ以外は "stop"。並び順に効くのは computeSortAffecting により prompt_submit だけ。
|
|
2682
|
+
const prevTurnAt = lastTurnAtByName.get(s.session_name) || 0
|
|
2683
|
+
const prevEv = lastChatEventByName.get(s.session_name)
|
|
2684
|
+
const eventForTick = chatSessionEventForTick({
|
|
2685
|
+
chat: currentChat,
|
|
2686
|
+
prevEv,
|
|
2687
|
+
prevTurnAt,
|
|
2688
|
+
directAt: s.cwd ? getPromptAcceptedAt(s.cwd, sessionCliKind) : undefined,
|
|
2945
2689
|
})
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
{
|
|
2952
|
-
session_name: s.session_name,
|
|
2953
|
-
status,
|
|
2954
|
-
proc_busy: procBusy,
|
|
2955
|
-
child_busy: childBusy,
|
|
2956
|
-
output_active: outputActive,
|
|
2957
|
-
stalled,
|
|
2958
|
-
armed,
|
|
2959
|
-
stable,
|
|
2960
|
-
context_pct: contextPct,
|
|
2961
|
-
turn_active: turnActive,
|
|
2962
|
-
cli_kind: chatCliKind,
|
|
2963
|
-
context_session_id: contextSessionId,
|
|
2964
|
-
},
|
|
2965
|
-
"session.state push",
|
|
2966
|
-
)
|
|
2690
|
+
if (eventForTick) {
|
|
2691
|
+
const { ev, at } = eventForTick
|
|
2692
|
+
lastTurnAtByName.set(s.session_name, currentChat.turnAt)
|
|
2693
|
+
lastChatEventByName.set(s.session_name, ev)
|
|
2694
|
+
writeSessionEventFileFn(s.session_name, ev, at).catch(() => {})
|
|
2967
2695
|
}
|
|
2968
|
-
|
|
2969
|
-
|
|
2696
|
+
}
|
|
2697
|
+
const prev = lastByName.get(s.session_name)
|
|
2698
|
+
if (!prev || Object.keys(payload).some((key) => prev[key] !== payload[key])) {
|
|
2699
|
+
lastByName.set(s.session_name, payload)
|
|
2700
|
+
if (STATE_TRACE) logger?.info({ session_name: s.session_name, ...payload }, "session.state push")
|
|
2970
2701
|
tmuxSessionsSnapshotCache.clear()
|
|
2971
|
-
client.send({
|
|
2972
|
-
type: "session.state",
|
|
2973
|
-
session_name: s.session_name,
|
|
2974
|
-
status,
|
|
2975
|
-
context_pct: contextPct,
|
|
2976
|
-
stable,
|
|
2977
|
-
proc_busy: procBusy,
|
|
2978
|
-
// child_busy: outputActive を含まない真の子プロセス busy。frontend の shouldQueue が
|
|
2979
|
-
// 出力残り火 (proc_busy 残存) でアイドルなのにキューへ逃がす誤キュー (症状A) を断つために使う。
|
|
2980
|
-
child_busy: childBusy,
|
|
2981
|
-
stalled,
|
|
2982
|
-
// turn_active: ステータスドット 4 状態統一の権威ソース。null は旧クライアント互換
|
|
2983
|
-
// (frontend は欠落時に旧 prompt_submit/stale 降格ロジックへフォールバック)。
|
|
2984
|
-
turn_active: turnActive,
|
|
2985
|
-
// cli_kind (T05945): セッション名由来の構造値 ('claude' | 'codex')。frontend の
|
|
2986
|
-
// 単一セレクタ (useCockpitSessionDisplay) が「この push はどの CLI 行のものか」を
|
|
2987
|
-
// 判定する唯一の軸。context_session_id は context% の会話 ID。
|
|
2988
|
-
cli_kind: chatCliKind,
|
|
2989
|
-
context_session_id: contextSessionId,
|
|
2990
|
-
// 旧クライアント互換。新規 consumer は context_session_id を使う。
|
|
2991
|
-
chat_session_id: contextSessionId,
|
|
2992
|
-
})
|
|
2702
|
+
client.send({ type: "session.state", session_name: s.session_name, ...payload })
|
|
2993
2703
|
}
|
|
2994
2704
|
// 消滅セッションの GC (perf監査/メモリリーク対策): tmux から消えたセッションの状態 Map を
|
|
2995
2705
|
// まとめて解放する。clear() は再接続時(onReopen)のみのため、worktree 案件の生成/破棄で
|
|
@@ -3033,6 +2743,7 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
|
|
|
3033
2743
|
t0.unref?.()
|
|
3034
2744
|
|
|
3035
2745
|
return {
|
|
2746
|
+
tick,
|
|
3036
2747
|
stop() {
|
|
3037
2748
|
stopped = true
|
|
3038
2749
|
clearInterval(ti)
|
|
@@ -3326,7 +3037,7 @@ export function handleClaudeChatInput(msg, ctx) {
|
|
|
3326
3037
|
// 書かれる。bridge が拒否しても取り消さない (仕様。notifyChatPromptSubmit の JSDoc)。
|
|
3327
3038
|
const cwdSnapshot = ctx.claudeBridge.getCwd?.(stream_id) ?? null
|
|
3328
3039
|
if (cwdSnapshot) {
|
|
3329
|
-
recordPromptAccepted(cwdSnapshot, "claude", acceptedAt)
|
|
3040
|
+
recordPromptAccepted(cwdSnapshot, "claude", acceptedAt, ctx.claudeBridge.sessions?.get(stream_id), ctx.claudeBridge)
|
|
3330
3041
|
notifyChatPromptSubmit({
|
|
3331
3042
|
cwd: cwdSnapshot,
|
|
3332
3043
|
cliKind: "claude",
|
|
@@ -3552,7 +3263,7 @@ export function handleCodexChatInput(msg, ctx) {
|
|
|
3552
3263
|
// (handleClaudeChatInput のコメント参照)。
|
|
3553
3264
|
const cwdSnapshot = ctx.codexBridge.getCwd?.(stream_id) ?? null
|
|
3554
3265
|
if (cwdSnapshot) {
|
|
3555
|
-
recordPromptAccepted(cwdSnapshot, "codex", acceptedAt)
|
|
3266
|
+
recordPromptAccepted(cwdSnapshot, "codex", acceptedAt, ctx.codexBridge.sessions?.get(stream_id), ctx.codexBridge)
|
|
3556
3267
|
notifyChatPromptSubmit({
|
|
3557
3268
|
cwd: cwdSnapshot,
|
|
3558
3269
|
cliKind: "codex",
|
|
@@ -3833,17 +3544,14 @@ export async function buildUsagePayload(ctx) {
|
|
|
3833
3544
|
// 別数値のため、`usage` にマージせず独立フィールド `codexUsage`/`codexSessions`
|
|
3834
3545
|
// として返す (frontend 側でどちらのアカウントの残量か混同しないため)。
|
|
3835
3546
|
let codexUsage = null
|
|
3547
|
+
let codexContextCwd = null
|
|
3836
3548
|
let codexSessions = []
|
|
3837
3549
|
const activeCodexProfile = await codexProfiles.getActiveProfile()
|
|
3838
3550
|
const codexHasCredentials = await codexProfiles.hasCodexCredentials(activeCodexProfile.configDir)
|
|
3839
3551
|
if (ctx.codexBridge && codexHasCredentials) {
|
|
3840
3552
|
try {
|
|
3841
|
-
//
|
|
3842
|
-
//
|
|
3843
|
-
// 複数 thread があると frontend の usageByCwd (updatedAtMs 同値で first-wins) が
|
|
3844
|
-
// 任意のスレッドを拾い、state loop (権威 producer) と別 thread を選んでドーナツが
|
|
3845
|
-
// 前後した。liveSessionForCwd の同一規則で cwd → 権威 producer(session) を一意化し、
|
|
3846
|
-
// producerSeq を context% の live source として通して両経路を収束させる。
|
|
3553
|
+
// キャッシュは従来の cwd / アカウントデータだけを作る。
|
|
3554
|
+
// 代表会話の値は handleUsageRequest の送信直前に付ける。
|
|
3847
3555
|
const cwds = new Set()
|
|
3848
3556
|
for (const s of ctx.codexBridge.sessions.values()) {
|
|
3849
3557
|
if (s.threadId && s.cwd) cwds.add(s.cwd)
|
|
@@ -3863,6 +3571,7 @@ export async function buildUsagePayload(ctx) {
|
|
|
3863
3571
|
if (liveCodex.length > 0) {
|
|
3864
3572
|
// 5h/7d はアカウント全体の値 (どの live session から読んでも同じ契約の残量)
|
|
3865
3573
|
// なので、代表として最初の 1 件から取る (context% はその producer の live を使う)。
|
|
3574
|
+
codexContextCwd = liveCodex[0].cwd
|
|
3866
3575
|
codexUsage = await codexUsageForThread(liveCodex[0].threadId, {
|
|
3867
3576
|
producerSeq: liveCodex[0].producerSeq,
|
|
3868
3577
|
})
|
|
@@ -3910,6 +3619,7 @@ export async function buildUsagePayload(ctx) {
|
|
|
3910
3619
|
|
|
3911
3620
|
return {
|
|
3912
3621
|
payload: { usage, sessions, codexUsage, codexSessions },
|
|
3622
|
+
codexContextCwd,
|
|
3913
3623
|
timings: {
|
|
3914
3624
|
credentials_ms: credentialsMs,
|
|
3915
3625
|
account_usage_ms: accountUsageMs,
|
|
@@ -3920,6 +3630,27 @@ export async function buildUsagePayload(ctx) {
|
|
|
3920
3630
|
}
|
|
3921
3631
|
}
|
|
3922
3632
|
|
|
3633
|
+
/** キャッシュを変えず、要求の送信時点の代表で cwd ごとに置き換える。 */
|
|
3634
|
+
export function applyUsageRowChatState(payload, codexContextCwd = null) {
|
|
3635
|
+
const result = { ...payload }
|
|
3636
|
+
for (const [kind, field] of [["claude", "sessions"], ["codex", "codexSessions"]]) {
|
|
3637
|
+
const cwds = representativeRows(kind)
|
|
3638
|
+
if (cwds.length === 0) continue
|
|
3639
|
+
const selected = new Set(cwds)
|
|
3640
|
+
const entries = (payload[field] ?? []).filter((entry) => !selected.has(entry.cwd))
|
|
3641
|
+
for (const cwd of cwds) {
|
|
3642
|
+
const chat = readRowChatState(cwd, kind)
|
|
3643
|
+
entries.push({ cwd, sessionId: chat.sessionId, contextPercent: chat.context_pct,
|
|
3644
|
+
updatedAtMs: chat.turnAt, status: chat.status, inputPending: chat.inputPending,
|
|
3645
|
+
context_session_id: chat.sessionId, chat_session_id: chat.sessionId })
|
|
3646
|
+
}
|
|
3647
|
+
result[field] = entries
|
|
3648
|
+
}
|
|
3649
|
+
const codexChat = readRowChatState(codexContextCwd, "codex")
|
|
3650
|
+
if (codexChat && payload.codexUsage) result.codexUsage = { ...payload.codexUsage, context: codexChat.context_pct }
|
|
3651
|
+
return result
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3923
3654
|
/**
|
|
3924
3655
|
* usage.request の処理。中身は TTL キャッシュ + single-flight で共有し、request_id
|
|
3925
3656
|
* だけを要求ごとに差し込む。
|
|
@@ -3956,7 +3687,7 @@ export async function handleUsageRequest(msg, ctx, deps = {}) {
|
|
|
3956
3687
|
}
|
|
3957
3688
|
ctx.client.send({
|
|
3958
3689
|
type: "usage.response",
|
|
3959
|
-
...result.payload,
|
|
3690
|
+
...applyUsageRowChatState(result.payload, result.codexContextCwd),
|
|
3960
3691
|
request_id: msg.request_id,
|
|
3961
3692
|
})
|
|
3962
3693
|
} catch (err) {
|
|
@@ -4343,60 +4074,15 @@ export async function dispatch(msg, ctx) {
|
|
|
4343
4074
|
"failed to push Claude attach history",
|
|
4344
4075
|
)
|
|
4345
4076
|
})
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
// 由来の一意な仮IDを先に送る。実 session_id が来た時点で置換される。
|
|
4353
|
-
// - resume: bound session の jsonl から実 context% を解決し、cli_kind='claude' +
|
|
4354
|
-
// 実 session_id で identity を確定させて即時 push する (症状③)。
|
|
4355
|
-
const resolvedCwd = cwdArg || process.env.HOME || process.cwd()
|
|
4356
|
-
if (resolvedCwd) {
|
|
4357
|
-
if (!resumeSessionId) {
|
|
4358
|
-
const freshContextSessionId = `fresh:${stream_id}`
|
|
4359
|
-
clearChatSignal(resolvedCwd, "claude")
|
|
4360
|
-
recordChatActivity(resolvedCwd, "claude", {
|
|
4361
|
-
status: "waiting",
|
|
4362
|
-
contextPct: 0,
|
|
4363
|
-
inputPending: false,
|
|
4364
|
-
sessionId: freshContextSessionId,
|
|
4365
|
-
})
|
|
4366
|
-
pushImmediateChatState({
|
|
4367
|
-
client: ctx.client,
|
|
4368
|
-
plugins: ctx.plugins,
|
|
4369
|
-
logger: ctx.logger,
|
|
4370
|
-
cwd: resolvedCwd,
|
|
4371
|
-
cliKind: "claude",
|
|
4372
|
-
sessionId: freshContextSessionId,
|
|
4373
|
-
contextPct: 0,
|
|
4374
|
-
}).catch(() => {})
|
|
4375
|
-
} else {
|
|
4376
|
-
jsonlContextForCwd(resolvedCwd, resumeSessionId)
|
|
4377
|
-
.catch(() => null)
|
|
4378
|
-
.then((jsonlCtx) => {
|
|
4379
|
-
const contextPct =
|
|
4380
|
-
typeof jsonlCtx?.percent === "number" ? jsonlCtx.percent : 0
|
|
4381
|
-
recordChatActivity(resolvedCwd, "claude", {
|
|
4382
|
-
status: "waiting",
|
|
4383
|
-
contextPct,
|
|
4384
|
-
inputPending: false,
|
|
4385
|
-
sessionId: resumeSessionId,
|
|
4386
|
-
})
|
|
4387
|
-
return pushImmediateChatState({
|
|
4388
|
-
client: ctx.client,
|
|
4389
|
-
plugins: ctx.plugins,
|
|
4390
|
-
logger: ctx.logger,
|
|
4391
|
-
cwd: resolvedCwd,
|
|
4392
|
-
cliKind: "claude",
|
|
4393
|
-
sessionId: resumeSessionId,
|
|
4394
|
-
contextPct,
|
|
4395
|
-
})
|
|
4396
|
-
})
|
|
4397
|
-
.catch(() => {})
|
|
4398
|
-
}
|
|
4077
|
+
const session = ctx.claudeBridge.sessions?.get(stream_id)
|
|
4078
|
+
const resolvedCwd = session?.cwd || cwdArg || process.env.HOME || process.cwd()
|
|
4079
|
+
const push = () => pushImmediateChatState({ client: ctx.client, plugins: ctx.plugins,
|
|
4080
|
+
logger: ctx.logger, cwd: resolvedCwd, cliKind: "claude" })
|
|
4081
|
+
if (session && resumeSessionId) {
|
|
4082
|
+
void loadResumeChatContext(session, "claude").then(push).catch(() => {})
|
|
4399
4083
|
}
|
|
4084
|
+
void push().catch(() => {})
|
|
4085
|
+
|
|
4400
4086
|
} catch (err) {
|
|
4401
4087
|
ctx.client.send({
|
|
4402
4088
|
type: "claude.error",
|
|
@@ -4781,68 +4467,14 @@ export async function dispatch(msg, ctx) {
|
|
|
4781
4467
|
} catch (err) {
|
|
4782
4468
|
ctx.logger?.warn?.({ err: err?.message }, "failed to replay review events")
|
|
4783
4469
|
}
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
// - resume: codexUsageForThread で実 context% を解決し即時 push (症状③)。
|
|
4790
|
-
if (resolvedCwd) {
|
|
4791
|
-
// Stage4 caller レビュー major #6: fresh/resume の分岐は **実際の attach 結果**
|
|
4792
|
-
// (info.resuming) で判定する。in-memory reattach は元 msg.resume_thread_id が空
|
|
4793
|
-
// (fresh 再接続) でも既存 thread への復帰 = info.resuming=true なので、resume 側の
|
|
4794
|
-
// 実 context% 解決に載せる (fresh 扱いで context を 0 に戻さない)。
|
|
4795
|
-
if (!info.resuming) {
|
|
4796
|
-
clearChatSignal(resolvedCwd, "codex")
|
|
4797
|
-
recordChatActivity(resolvedCwd, "codex", {
|
|
4798
|
-
status: "waiting",
|
|
4799
|
-
contextPct: 0,
|
|
4800
|
-
inputPending: false,
|
|
4801
|
-
sessionId: info.threadId || null,
|
|
4802
|
-
})
|
|
4803
|
-
pushImmediateChatState({
|
|
4804
|
-
client: ctx.client,
|
|
4805
|
-
plugins: ctx.plugins,
|
|
4806
|
-
logger: ctx.logger,
|
|
4807
|
-
cwd: resolvedCwd,
|
|
4808
|
-
cliKind: "codex",
|
|
4809
|
-
sessionId: info.threadId || null,
|
|
4810
|
-
contextPct: 0,
|
|
4811
|
-
}).catch(() => {})
|
|
4812
|
-
} else {
|
|
4813
|
-
// 増分2: context% は cwd の権威 producer(seq) の live を正本にする (resume 直後の
|
|
4814
|
-
// このセッションは lastTurnAtMs=now で権威。live 未取得なら rollout フォールバック)。
|
|
4815
|
-
const resumeProducerSeq = ctx.codexBridge?.liveSessionForCwd?.(resolvedCwd)?.seq
|
|
4816
|
-
codexUsageForThread(info.threadId, { producerSeq: resumeProducerSeq })
|
|
4817
|
-
.catch(() => null)
|
|
4818
|
-
.then((usage) => {
|
|
4819
|
-
// ⚠️ codex-review round2 major#2: rollout I/O 中に別 session が権威化しうる。
|
|
4820
|
-
// await 後に権威 seq を再検証し、この resume session が現権威でなければ即時 push を
|
|
4821
|
-
// 見送る (古い context/sessionId を書かない。state loop が ≤5s で正す)。
|
|
4822
|
-
const stillAuthority =
|
|
4823
|
-
resumeProducerSeq != null &&
|
|
4824
|
-
ctx.codexBridge?.liveSessionForCwd?.(resolvedCwd)?.seq === resumeProducerSeq
|
|
4825
|
-
if (!stillAuthority) return undefined
|
|
4826
|
-
const contextPct = typeof usage?.context === "number" ? usage.context : 0
|
|
4827
|
-
recordChatActivity(resolvedCwd, "codex", {
|
|
4828
|
-
status: "waiting",
|
|
4829
|
-
contextPct,
|
|
4830
|
-
inputPending: false,
|
|
4831
|
-
sessionId: info.threadId,
|
|
4832
|
-
})
|
|
4833
|
-
return pushImmediateChatState({
|
|
4834
|
-
client: ctx.client,
|
|
4835
|
-
plugins: ctx.plugins,
|
|
4836
|
-
logger: ctx.logger,
|
|
4837
|
-
cwd: resolvedCwd,
|
|
4838
|
-
cliKind: "codex",
|
|
4839
|
-
sessionId: info.threadId,
|
|
4840
|
-
contextPct,
|
|
4841
|
-
})
|
|
4842
|
-
})
|
|
4843
|
-
.catch(() => {})
|
|
4844
|
-
}
|
|
4470
|
+
const session = ctx.codexBridge.sessions?.get(stream_id)
|
|
4471
|
+
const push = () => pushImmediateChatState({ client: ctx.client, plugins: ctx.plugins,
|
|
4472
|
+
logger: ctx.logger, cwd: resolvedCwd, cliKind: "codex" })
|
|
4473
|
+
if (session && info.resuming) {
|
|
4474
|
+
void loadResumeChatContext(session, "codex").then(push).catch(() => {})
|
|
4845
4475
|
}
|
|
4476
|
+
void push().catch(() => {})
|
|
4477
|
+
|
|
4846
4478
|
} catch (err) {
|
|
4847
4479
|
ctx.client.send({
|
|
4848
4480
|
type: "codex.error",
|
|
@@ -5584,6 +5216,18 @@ export async function dispatch(msg, ctx) {
|
|
|
5584
5216
|
case "chat.session.stop": {
|
|
5585
5217
|
const result = { type: "chat.session.stop.result", request_id: msg.request_id, stopped: 0 }
|
|
5586
5218
|
try {
|
|
5219
|
+
if (msg.scope === "cwd") {
|
|
5220
|
+
const cwd = typeof msg.cwd === "string" ? msg.cwd : ""
|
|
5221
|
+
if (msg.cli_kind !== "claude" && msg.cli_kind !== "codex") result.error = "unsupported cli_kind"
|
|
5222
|
+
else if (!cwd || !path.isAbsolute(cwd)) result.error = "cwd required"
|
|
5223
|
+
else if (msg.cli_kind === "claude") {
|
|
5224
|
+
result.stopped = await ctx.claudeBridge.stopSessionsFor({ cwd, reason: "chat-stop" })
|
|
5225
|
+
} else {
|
|
5226
|
+
result.stopped = await ctx.codexBridge.stopSessionsForCwd({ cwd, reason: "chat-stop" })
|
|
5227
|
+
}
|
|
5228
|
+
ctx.client.send(result)
|
|
5229
|
+
return
|
|
5230
|
+
}
|
|
5587
5231
|
if (msg.cli_kind === "claude") {
|
|
5588
5232
|
const cwd = typeof msg.cwd === "string" ? msg.cwd : ""
|
|
5589
5233
|
const session_id = typeof msg.session_id === "string" ? msg.session_id : ""
|
|
@@ -5648,7 +5292,7 @@ export async function dispatch(msg, ctx) {
|
|
|
5648
5292
|
bridges: { claudeBridge: ctx.claudeBridge, codexBridge: ctx.codexBridge },
|
|
5649
5293
|
logger: ctx.logger,
|
|
5650
5294
|
})
|
|
5651
|
-
ctx.client.send({ type: "chat.sessions", request_id: msg.request_id, ...snapshot })
|
|
5295
|
+
ctx.client.send({ type: "chat.sessions", request_id: msg.request_id, ...refreshChatSessionsSignals(snapshot, ctx.logger) })
|
|
5652
5296
|
} catch (err) {
|
|
5653
5297
|
ctx.client.send({
|
|
5654
5298
|
type: "chat.sessions", request_id: msg.request_id, targets: [], live: [],
|
|
@@ -5676,7 +5320,11 @@ export async function dispatch(msg, ctx) {
|
|
|
5676
5320
|
const enriched = enrichTmuxSessionsForListResponse(
|
|
5677
5321
|
sessions,
|
|
5678
5322
|
lastEventByName,
|
|
5679
|
-
|
|
5323
|
+
(name) => {
|
|
5324
|
+
const row = sessions.find((item) => item.name === name)
|
|
5325
|
+
if (hasRepresentativeRow(row?.cwd, cliKindForSessionName(name))) return null
|
|
5326
|
+
return ctx.getSessionStateSnapshot?.(name)
|
|
5327
|
+
},
|
|
5680
5328
|
// warm = このセッションへ依頼 (cockpit_orch.py send) を送れるか。sibling-inject の
|
|
5681
5329
|
// 宛先候補列挙と同一関数 (live-sessions.mjs) を通す。
|
|
5682
5330
|
(row) =>
|
|
@@ -5719,7 +5367,7 @@ export async function dispatch(msg, ctx) {
|
|
|
5719
5367
|
type: "tmux.sessions",
|
|
5720
5368
|
request_id: msg.request_id,
|
|
5721
5369
|
...snapshot,
|
|
5722
|
-
sessions,
|
|
5370
|
+
sessions: sessions.map((row) => applyRowChatState(row, row.cwd, cliKindForSessionName(row.name))),
|
|
5723
5371
|
})
|
|
5724
5372
|
} catch (err) {
|
|
5725
5373
|
ctx.client.send({
|