@cocorograph/hub-agent 0.8.83 → 0.8.85
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/scripts/install.sh +3 -5
- package/src/main.mjs +21 -1051
- package/src/proc-introspect.mjs +0 -20
- package/src/sibling-inject.mjs +31 -0
- package/src/tmux.mjs +7 -723
- package/src/pane-cli-flag.mjs +0 -30
- package/src/state.mjs +0 -595
package/src/main.mjs
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
* - plugin discovery + onAgentStart broadcast
|
|
6
6
|
* - WS メッセージのディスパッチ:
|
|
7
7
|
* - claude.* / codex.* → ClaudeStreamBridge / CodexStreamBridge (チャット)
|
|
8
|
-
* - tmux.* → tmux module (セッション一覧・作成・削除)
|
|
9
8
|
*
|
|
10
9
|
* 仕様書: ナレッジ/インフラ/cockpit-hub-hosted-integration-spec (id=6080)
|
|
11
10
|
*/
|
|
@@ -69,11 +68,6 @@ import {
|
|
|
69
68
|
import { listAgents } from "./agents.mjs"
|
|
70
69
|
import { listSkills } from "./skills.mjs"
|
|
71
70
|
import { readSidebarMessageSnapshot } from "./sidebar-message-snapshot.mjs"
|
|
72
|
-
import {
|
|
73
|
-
invalidateSessionCache,
|
|
74
|
-
listSessionStates,
|
|
75
|
-
StallTracker,
|
|
76
|
-
} from "./state.mjs"
|
|
77
71
|
import {
|
|
78
72
|
DEFAULT_PROFILE_ID,
|
|
79
73
|
defaultConfigDir,
|
|
@@ -98,49 +92,29 @@ import {
|
|
|
98
92
|
profileHasKeychainCredentials,
|
|
99
93
|
deleteKeychainCredentials,
|
|
100
94
|
} from "./claude-credentials.mjs"
|
|
101
|
-
import {
|
|
102
|
-
buildClaudeCmd,
|
|
103
|
-
buildCodexCmd,
|
|
104
|
-
buildWorktreeIndex,
|
|
105
|
-
createSession as createTmuxSession,
|
|
106
|
-
createWorktreeDir,
|
|
107
|
-
killManySessions,
|
|
108
|
-
killSession as killTmuxSession,
|
|
109
|
-
listSessions as listTmuxSessions,
|
|
110
|
-
listWorktreeNameHistory,
|
|
111
|
-
listWorktreeStubs,
|
|
112
|
-
paneClaudeAliveOrUnknown,
|
|
113
|
-
removeWorktree as removeWorktreeDir,
|
|
114
|
-
setTmuxGlobalEnv,
|
|
115
|
-
shouldLaunchPaneCli,
|
|
116
|
-
validateInitialPrompt,
|
|
117
|
-
} from "./tmux.mjs"
|
|
95
|
+
import { createWorktreeDir, listWorktreeNameHistory, removeWorktree as removeWorktreeDir } from "./tmux.mjs"
|
|
118
96
|
import { expandTilde, prepareChatWorkspace, isCwdUnavailableError } from "./chat-workspace.mjs"
|
|
119
97
|
import { buildChatSessionsSnapshot, refreshChatSessionsSignals } from "./chat-sessions-snapshot.mjs"
|
|
120
98
|
import { chatTargetsCache } from "./chat-targets.mjs"
|
|
121
99
|
import { ensureChatSession } from "./chat-session-starter.mjs"
|
|
122
100
|
import { beginRemoval } from "./chat-removal-guard.mjs"
|
|
123
101
|
import { SiblingInjectBridge } from "./sibling-inject.mjs"
|
|
124
|
-
import { describeLiveSessionsUnion
|
|
125
|
-
import { applyPointerContexts, resolvePointerContexts } from "./pointer-context.mjs"
|
|
102
|
+
import { describeLiveSessionsUnion } from "./live-sessions.mjs"
|
|
126
103
|
import {
|
|
127
104
|
boundSessionId,
|
|
128
105
|
codexUsageFromAccountRateLimits,
|
|
129
106
|
codexLiveContextForProducer,
|
|
130
107
|
codexSessionUsages,
|
|
131
|
-
codexTurnActiveForThread,
|
|
132
108
|
codexUsageForThread,
|
|
133
109
|
contextWindowSize,
|
|
134
110
|
forgetCodexLiveTokenUsage,
|
|
135
111
|
getSessionUsages,
|
|
136
112
|
getUsage,
|
|
137
|
-
isFreshUnboundBind,
|
|
138
113
|
recordChatRateLimit,
|
|
139
114
|
recordChatUsageSnapshot,
|
|
140
115
|
readAccountUsageSnapshot,
|
|
141
116
|
recordAccountUsageSnapshot,
|
|
142
117
|
recordCodexLiveTokenUsage,
|
|
143
|
-
turnActiveForCwd,
|
|
144
118
|
} from "./usage.mjs"
|
|
145
119
|
import {
|
|
146
120
|
getPromptAcceptedAt,
|
|
@@ -153,12 +127,6 @@ import {
|
|
|
153
127
|
hasRepresentativeRow,
|
|
154
128
|
} from "./chat-signals.mjs"
|
|
155
129
|
import { cliKindForSessionName, sessionNameFor } from "./session-names.mjs"
|
|
156
|
-
import {
|
|
157
|
-
ReadinessTracker,
|
|
158
|
-
busyChildCount,
|
|
159
|
-
getPanePid,
|
|
160
|
-
snapshotProcs,
|
|
161
|
-
} from "./proc-introspect.mjs"
|
|
162
130
|
import { hydrateProcessEnvFromShell } from "./shell-env.mjs"
|
|
163
131
|
|
|
164
132
|
export function resolveCodexLoginCommand(configDir, { isDefault = false, _resolveCodexBinary = resolveCodexBinary } = {}) {
|
|
@@ -203,127 +171,6 @@ export function resolveClaudeAttachOptions(config, msg = {}) {
|
|
|
203
171
|
}
|
|
204
172
|
}
|
|
205
173
|
|
|
206
|
-
/**
|
|
207
|
-
* `~/.hub/agent.json` の Claude CLI 設定から claude コマンド文字列を組み立てる。
|
|
208
|
-
*
|
|
209
|
-
* - enrollment 時に Hub から取得した `claude_model` / `claude_permission_mode` が
|
|
210
|
-
* 入っていれば、それを使って `claude --continue --model ... || claude --model ...` を生成
|
|
211
|
-
* - 両方空なら `undefined` を返す (createSession 内で DEFAULT_CLAUDE_CMD =
|
|
212
|
-
* 環境変数 HUB_CLAUDE_CMD か最小デフォルト `claude --continue || claude` が使われる)
|
|
213
|
-
*
|
|
214
|
-
* cockpit 管理画面でユーザーが Hub 側で model / permission-mode を変更すると、
|
|
215
|
-
* 次の enrollment / 再起動時に新しい値で組み立てられる。
|
|
216
|
-
*/
|
|
217
|
-
function claudeCmdFromAgentConfig(config) {
|
|
218
|
-
const cmd = buildClaudeCmd({
|
|
219
|
-
model: config?.claude_model,
|
|
220
|
-
permissionMode: config?.claude_permission_mode,
|
|
221
|
-
})
|
|
222
|
-
// buildClaudeCmd は config も env も空なら最小デフォルトを返す。
|
|
223
|
-
// ここでは「明示的に Hub 側で何か指定されていた場合のみ上書き、なければ
|
|
224
|
-
// createSession 側の DEFAULT_CLAUDE_CMD に委ねる」挙動にするため、
|
|
225
|
-
// model も mode も空なら undefined を返す。
|
|
226
|
-
const model = (config?.claude_model || "").trim()
|
|
227
|
-
const mode = (config?.claude_permission_mode || "").trim()
|
|
228
|
-
if (!model && !mode) return undefined
|
|
229
|
-
return cmd
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* `~/.hub/agent.json` の Codex CLI 設定 (codex_model / codex_effort) から
|
|
234
|
-
* createTmuxSession の model/effort opts へのデフォルト値を取得する
|
|
235
|
-
* (claudeCmdFromAgentConfig の Codex 版)。
|
|
236
|
-
*
|
|
237
|
-
* tmux.create_session / worktree.create の browser 側 msg.model / msg.effort が
|
|
238
|
-
* 明示指定されていればそちらを優先し (呼び出し側で判定済み)、未指定のときだけ
|
|
239
|
-
* このデフォルトにフォールバックする。空文字は「未指定」として undefined を返す
|
|
240
|
-
* (buildCodexCmd 側は undefined を渡されても既定値へフォールバックするため安全)。
|
|
241
|
-
*/
|
|
242
|
-
function codexDefaultsFromAgentConfig(config) {
|
|
243
|
-
return {
|
|
244
|
-
model: (config?.codex_model || "").trim() || undefined,
|
|
245
|
-
effort: (config?.codex_effort || "").trim() || undefined,
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* プロファイル切替用: 全 tmux セッションを kill → 同名・同 cwd で再作成する。
|
|
251
|
-
*
|
|
252
|
-
* 再作成された pane は **既定では素のシェル** (createSession が CLI を起動しない)。
|
|
253
|
-
* アカウント切替は SDK チャット側の process.env と、tmux サーバーのグローバル環境
|
|
254
|
-
* (syncTmuxProfileEnv の `set-environment -g`) で追従する。
|
|
255
|
-
* `HUB_AGENT_TMUX_LAUNCH_CLI=1` で TUI を起動する運用の場合のみ、createSession 内の
|
|
256
|
-
* injectConfigDirEnv が新しい CLAUDE_CONFIG_DIR をコマンドへ焼き込む。
|
|
257
|
-
* initial prompt (leader の /orchestrate 等) までは復元しない — アカウントが
|
|
258
|
-
* 変わる以上、前アカウントの文脈でスキルを自動発火させない方が安全。
|
|
259
|
-
* cwd が取れないセッションは kill のみ行い、再作成はスキップして failed に積む。
|
|
260
|
-
* tmux サーバー未起動なら何もしない。
|
|
261
|
-
*/
|
|
262
|
-
export async function restartAllTmuxSessionsForProfileSwitch(ctx) {
|
|
263
|
-
const result = { killed: [], recreated: [], failed: [] }
|
|
264
|
-
const listSessionsForRestart = ctx._listTmuxSessions || listTmuxSessions
|
|
265
|
-
const killSessionsForRestart = ctx._killManySessions || killManySessions
|
|
266
|
-
const createSessionForRestart = ctx._createTmuxSession || createTmuxSession
|
|
267
|
-
let sessions = []
|
|
268
|
-
try {
|
|
269
|
-
sessions = await listSessionsForRestart({ plugins: ctx.plugins, logger: ctx.logger })
|
|
270
|
-
} catch (err) {
|
|
271
|
-
ctx.logger?.debug?.(
|
|
272
|
-
{ err: err?.message || String(err) },
|
|
273
|
-
"tmux list failed during profile switch (server not running?)",
|
|
274
|
-
)
|
|
275
|
-
return result
|
|
276
|
-
}
|
|
277
|
-
if (!sessions.length) return result
|
|
278
|
-
const codexDefaults = codexDefaultsFromAgentConfig(ctx.config)
|
|
279
|
-
let prebuiltCodexCmd
|
|
280
|
-
const codexSessions = sessions.filter((s) => cliKindForSessionName(s.name) === "codex")
|
|
281
|
-
let sessionsToRestart = sessions
|
|
282
|
-
if (shouldLaunchPaneCli() && codexSessions.length) {
|
|
283
|
-
try {
|
|
284
|
-
prebuiltCodexCmd = buildCodexCmd({
|
|
285
|
-
...codexDefaults,
|
|
286
|
-
_resolveCodexBinary: ctx._resolveCodexBinary,
|
|
287
|
-
})
|
|
288
|
-
} catch (err) {
|
|
289
|
-
const reason = err?.message || String(err)
|
|
290
|
-
result.failed.push(...codexSessions.map((s) => ({ name: s.name, reason })))
|
|
291
|
-
const blocked = new Set(codexSessions.map((s) => s.name))
|
|
292
|
-
sessionsToRestart = sessions.filter((s) => !blocked.has(s.name))
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
if (!sessionsToRestart.length) return result
|
|
296
|
-
const { killed } = await killSessionsForRestart(
|
|
297
|
-
sessionsToRestart.map((s) => s.name),
|
|
298
|
-
{ logger: ctx.logger },
|
|
299
|
-
)
|
|
300
|
-
result.killed = killed
|
|
301
|
-
for (const s of sessionsToRestart) {
|
|
302
|
-
if (!killed.includes(s.name)) continue
|
|
303
|
-
if (!s.cwd) {
|
|
304
|
-
result.failed.push({ name: s.name, reason: "cwd unknown" })
|
|
305
|
-
continue
|
|
306
|
-
}
|
|
307
|
-
try {
|
|
308
|
-
const cliKind = cliKindForSessionName(s.name)
|
|
309
|
-
await createSessionForRestart(s.name, s.cwd, {
|
|
310
|
-
// cliKind はセッション名 (`<dir>__codex` か否か) だけで決まる。渡し忘れると
|
|
311
|
-
// createSession が既定の claude として扱い、codex セッションの再生成でも
|
|
312
|
-
// AGENTS.md ではなく CLAUDE.md を同期対象にしてしまう。
|
|
313
|
-
cliKind,
|
|
314
|
-
claudeCmd: claudeCmdFromAgentConfig(ctx.config),
|
|
315
|
-
codexCmd: cliKind === "codex" ? prebuiltCodexCmd : undefined,
|
|
316
|
-
...codexDefaults,
|
|
317
|
-
logger: ctx.logger,
|
|
318
|
-
})
|
|
319
|
-
result.recreated.push(s.name)
|
|
320
|
-
} catch (err) {
|
|
321
|
-
result.failed.push({ name: s.name, reason: err?.message || String(err) })
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
return result
|
|
325
|
-
}
|
|
326
|
-
|
|
327
174
|
/**
|
|
328
175
|
* `~/.claude/scripts/manifest.json` から Hub AI bundle の version を読む。
|
|
329
176
|
* 未セットアップなら null を返す。
|
|
@@ -506,27 +353,6 @@ export function applyActiveProfileEnv(profile, log) {
|
|
|
506
353
|
)
|
|
507
354
|
}
|
|
508
355
|
|
|
509
|
-
/**
|
|
510
|
-
* active プロファイルの CLAUDE_CONFIG_DIR を tmux サーバーのグローバル環境にも同期する。
|
|
511
|
-
* tmux pane のシェルは hub-agent の process.env を見ないため、set-environment -g で流し込む。
|
|
512
|
-
* pane は既定で素のシェル (CLI を自動起動しない) なので、ユーザーが pane 内で直接
|
|
513
|
-
* `claude` を打つケースにはこれが主経路になる
|
|
514
|
-
* (`HUB_AGENT_TMUX_LAUNCH_CLI=1` で CLI を pane 起動するときだけ、createSession 内の
|
|
515
|
-
* injectConfigDirEnv によるコマンド焼き込みが併用される)。
|
|
516
|
-
* tmux サーバー未起動でも失敗しない。
|
|
517
|
-
*/
|
|
518
|
-
export async function syncTmuxProfileEnv(profile, log) {
|
|
519
|
-
const isDefault =
|
|
520
|
-
!profile ||
|
|
521
|
-
profile.id === DEFAULT_PROFILE_ID ||
|
|
522
|
-
profile.configDir === defaultConfigDir()
|
|
523
|
-
await setTmuxGlobalEnv(
|
|
524
|
-
"CLAUDE_CONFIG_DIR",
|
|
525
|
-
isDefault ? null : profile.configDir,
|
|
526
|
-
{ logger: log },
|
|
527
|
-
)
|
|
528
|
-
}
|
|
529
|
-
|
|
530
356
|
/**
|
|
531
357
|
* active Codex プロファイルの configDir を process.env.CODEX_HOME へ反映する。
|
|
532
358
|
* 既定 (`~/.codex`) のときは変数を消して Codex 既定パスに戻す。codex app-server は
|
|
@@ -552,20 +378,6 @@ export function applyActiveCodexEnv(profile, log) {
|
|
|
552
378
|
)
|
|
553
379
|
}
|
|
554
380
|
|
|
555
|
-
/**
|
|
556
|
-
* active Codex プロファイルの CODEX_HOME を tmux サーバーのグローバル環境へ同期する。
|
|
557
|
-
* tmux pane で直接 `codex` を打つケースや respawn の保険。tmux 未起動でも失敗しない。
|
|
558
|
-
*/
|
|
559
|
-
export async function syncTmuxCodexEnv(profile, log) {
|
|
560
|
-
const isDefault =
|
|
561
|
-
!profile ||
|
|
562
|
-
profile.id === codexProfiles.DEFAULT_CODEX_PROFILE_ID ||
|
|
563
|
-
profile.configDir === codexProfiles.defaultCodexDir()
|
|
564
|
-
await setTmuxGlobalEnv("CODEX_HOME", isDefault ? null : profile.configDir, {
|
|
565
|
-
logger: log,
|
|
566
|
-
})
|
|
567
|
-
}
|
|
568
|
-
|
|
569
381
|
/**
|
|
570
382
|
* `setup_hub_ai.py --silent` を best-effort でキックし、全プロファイルの settings.json へ
|
|
571
383
|
* bundle hooks (cockpit_permission_bridge 等) を反映させる。
|
|
@@ -930,7 +742,6 @@ export async function startDaemon({ version, claudeSdk } = {}) {
|
|
|
930
742
|
try {
|
|
931
743
|
const activeProfile = await getActiveProfile()
|
|
932
744
|
applyActiveProfileEnv(activeProfile, logger)
|
|
933
|
-
await syncTmuxProfileEnv(activeProfile, logger)
|
|
934
745
|
} catch (err) {
|
|
935
746
|
logger.warn(
|
|
936
747
|
{ err: err.message },
|
|
@@ -942,7 +753,6 @@ export async function startDaemon({ version, claudeSdk } = {}) {
|
|
|
942
753
|
try {
|
|
943
754
|
const activeCodex = await codexProfiles.getActiveProfile()
|
|
944
755
|
applyActiveCodexEnv(activeCodex, logger)
|
|
945
|
-
await syncTmuxCodexEnv(activeCodex, logger)
|
|
946
756
|
} catch (err) {
|
|
947
757
|
logger.warn(
|
|
948
758
|
{ err: err.message },
|
|
@@ -1213,58 +1023,15 @@ export async function startDaemon({ version, claudeSdk } = {}) {
|
|
|
1213
1023
|
// no-op. ref されていることだけが重要。
|
|
1214
1024
|
}, 60_000)
|
|
1215
1025
|
|
|
1216
|
-
//
|
|
1217
|
-
|
|
1218
|
-
// ready-for-input 判定: claude プロセスの子プロセスを内省し「Stop フック/ツール実行が
|
|
1219
|
-
// 終わって真に入力可能になった」エッジを検出する (proc-introspect.mjs)。capture-pane は
|
|
1220
|
-
// Stop フック実行中を観測できないため、これが「stop の早期消灯/誤フラッシュ」の根治。
|
|
1221
|
-
// state loop に渡す boundSessions (tmux session_name → session_id) の器。
|
|
1222
|
-
// ⚠️ 純 SDK チャット化 (2026-07) で唯一の writer だった claude.tui.bind が消えたため、
|
|
1223
|
-
// この Map は **常に空**。それでも state loop 起動より前に「空 Map として」用意する:
|
|
1224
|
-
// undefined を渡すと state loop がバックグラウンド時に cwd 最新 JSONL へフォールバックして
|
|
1225
|
-
// 別会話の context% を出す。空 Map なら boundSessionId が常に null を返し、
|
|
1226
|
-
// resolveStateLoopBoundSessionId の「SDK チャットは bind 台帳を持たないので claudeBridge の
|
|
1227
|
-
// ライブセッションへフォールバック」経路 (下記) が正しく効く。
|
|
1228
|
-
ctx.tuiReboundSessions = new Map()
|
|
1229
|
-
|
|
1230
|
-
const readinessTracker = new ReadinessTracker()
|
|
1231
|
-
ctx.readinessTracker = readinessTracker
|
|
1232
|
-
const stateLoop = startStateLoop({
|
|
1233
|
-
client,
|
|
1234
|
-
plugins,
|
|
1235
|
-
logger,
|
|
1236
|
-
intervalMs: 5_000,
|
|
1237
|
-
claudeBridge,
|
|
1238
|
-
// 背景 Codex チャットセッションの状態を state loop へ渡す。
|
|
1239
|
-
codexBridge,
|
|
1240
|
-
readinessTracker,
|
|
1241
|
-
// tmux session_name → bind 中の session_id。turn_active を「そのペインが実際に走らせている
|
|
1242
|
-
// セッションの jsonl」で判定するために渡す (cwd dir 内の別 jsonl 取り違え=メタ欠陥#1 の根治)。
|
|
1243
|
-
boundSessions: ctx.tuiReboundSessions,
|
|
1244
|
-
})
|
|
1245
|
-
// T1: tmux.list_sessions poll fallback で turn_active/proc_busy/child_busy/stalled を
|
|
1246
|
-
// 同梱できるよう、state loop の最新値スナップショット accessor を ctx に晒す。
|
|
1247
|
-
// push 専用の 4 信号を poll でも回復可能にする (生産点は state loop で単一のまま、
|
|
1248
|
-
// 取り出し口を 2 つに増やすだけ = §5 の用途別出し分けを侵さない)。
|
|
1249
|
-
ctx.getSessionStateSnapshot = stateLoop.getSnapshot
|
|
1026
|
+
// 会話の回収と常駐数制限を定期実行する。
|
|
1027
|
+
const stateLoop = startStateLoop({ client, logger, intervalMs: 5_000, claudeBridge, codexBridge })
|
|
1250
1028
|
// bundle hook (cockpit_session_event_hook.py) が /tmp/cockpit_session_events/<name>.json
|
|
1251
1029
|
// に書き出す UserPromptSubmit / Stop / ready の event を fs.watch で拾って WS push する。
|
|
1252
1030
|
// text マーカー判定 (detectStatusFromText) より精度が高い「ターン境界」の signal。
|
|
1253
|
-
// prompt_submit / stop で readinessTracker を arm し、ready の settle 検出に繋ぐ。
|
|
1254
1031
|
const sessionEventLoop = await startSessionEventWatcher({
|
|
1255
1032
|
client,
|
|
1256
1033
|
logger,
|
|
1257
|
-
readinessTracker,
|
|
1258
|
-
})
|
|
1259
|
-
// arm 済みセッションの子プロセスを高速 poll し、baseline 復帰で event ファイルに 'ready'
|
|
1260
|
-
// を書く (既存 watcher が session.event 'ready' を push)。アイドル時は ps を spawn しない。
|
|
1261
|
-
const readinessLoop = startReadinessLoop({
|
|
1262
|
-
logger,
|
|
1263
|
-
tracker: readinessTracker,
|
|
1264
|
-
intervalMs: 700,
|
|
1265
1034
|
})
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
1035
|
// sibling-inject (ask-sibling スキル, 2026-07-04): 同一 cwd の claude⇄codex
|
|
1269
1036
|
// 相互依頼のためのローカル IPC 受信口。cwd スコープ capability トークン
|
|
1270
1037
|
// (src/sibling-token.mjs) で検証した要求のみ claudeBridge/codexBridge.sendToCwd()
|
|
@@ -1315,7 +1082,6 @@ export async function startDaemon({ version, claudeSdk } = {}) {
|
|
|
1315
1082
|
logger.info({ signal }, "agent stop hooks completed")
|
|
1316
1083
|
stateLoop.stop()
|
|
1317
1084
|
sessionEventLoop?.stop?.()
|
|
1318
|
-
readinessLoop?.stop?.()
|
|
1319
1085
|
restartCoordinator?.stop?.()
|
|
1320
1086
|
claudeBridge?.shutdown?.()
|
|
1321
1087
|
codexBridge?.shutdown?.()
|
|
@@ -1773,11 +1539,6 @@ async function readAllSessionEvents() {
|
|
|
1773
1539
|
return out
|
|
1774
1540
|
}
|
|
1775
1541
|
|
|
1776
|
-
// 複数 Cockpit タブは各自 8 秒 poll を持つため、位相がずれると agent は同じ
|
|
1777
|
-
// tmux/worktree/jsonl スナップショットを数秒おきに再構築してしまう。5 秒だけ成功値を
|
|
1778
|
-
// 共有し、同時要求は single-flight に畳む。create/kill/worktree 変更時は明示 clear する。
|
|
1779
|
-
export const tmuxSessionsSnapshotCache = new AsyncTtlCache({ ttlMs: 5_000 })
|
|
1780
|
-
|
|
1781
1542
|
/**
|
|
1782
1543
|
* `/tmp/cockpit_session_events/<tmux_session>.json` を fs.watch して
|
|
1783
1544
|
* UserPromptSubmit / Stop の event を `session.event` 型で Hub に push する。
|
|
@@ -1880,7 +1641,6 @@ export async function publishSessionEvent(
|
|
|
1880
1641
|
sort_affecting: sortAffecting,
|
|
1881
1642
|
}, "session.event push")
|
|
1882
1643
|
}
|
|
1883
|
-
tmuxSessionsSnapshotCache.clear()
|
|
1884
1644
|
client.send({
|
|
1885
1645
|
type: "session.event",
|
|
1886
1646
|
session_name: sessionName,
|
|
@@ -1891,125 +1651,11 @@ export async function publishSessionEvent(
|
|
|
1891
1651
|
return { sortAffecting }
|
|
1892
1652
|
}
|
|
1893
1653
|
|
|
1894
|
-
|
|
1895
|
-
/**
|
|
1896
|
-
* arm 済みセッション (prompt_submit / stop を受けた or busy 観測中) の claude プロセスを
|
|
1897
|
-
* 高速 poll し、子プロセスが baseline へ復帰したエッジで event ファイルに 'ready' を書く。
|
|
1898
|
-
* 既存の startSessionEventWatcher がそれを `session.event` 'ready' として push し、frontend は
|
|
1899
|
-
* 「真に入力可能」になった瞬間として消灯 + キューフラッシュする (Stop フック中の誤フラッシュ
|
|
1900
|
-
* 根治)。tracker.getBusy() は state loop が proc_busy レベルとして相乗せする。
|
|
1901
|
-
* onReady(name): ready エッジで呼ぶコールバック (busy で defer した +新規 の実行に使う)。
|
|
1902
|
-
*
|
|
1903
|
-
* 負荷: アイドル (armed/busy なし) のときは ps を spawn しない。claude pid は TTL キャッシュ。
|
|
1904
|
-
*/
|
|
1905
|
-
function startReadinessLoop({ tracker, logger, intervalMs = 700, onReady }) {
|
|
1906
|
-
let stopped = false
|
|
1907
|
-
const PANE_PID_TTL_MS = 30_000
|
|
1908
|
-
const panePidByName = new Map() // name → {pid, at}
|
|
1909
|
-
const unresolvedStreak = new Map() // name → 連続解決失敗回数 (SDK チャット等の掃除用)
|
|
1910
|
-
|
|
1911
|
-
// busy 子数のカウント基点は claude 本体 PID ではなく tmux の **pane_pid** にする。
|
|
1912
|
-
// 理由: (1) クロスプラットフォーム安全 — claude を comm 名で同定する方式は Linux/WSL で
|
|
1913
|
-
// claude が `node .../cli.js` として起動し comm が 'node' になり同定に失敗する。pane_pid
|
|
1914
|
-
// 基点なら子孫を数えるだけで claude の同定が不要。(2) /clear 耐性 — claude PID は /clear で
|
|
1915
|
-
// 変わるが pane_pid (ペインの shell) は不変なので再解決が要らない。baseline (rolling-min) が
|
|
1916
|
-
// 「claude 本体 + 常駐 MCP」を吸収するので、基点が pane_pid でも busy=「一過性の子増加」で正しい。
|
|
1917
|
-
const resolvePanePid = async (name, procMap) => {
|
|
1918
|
-
const hit = panePidByName.get(name)
|
|
1919
|
-
if (hit && procMap.has(hit.pid) && Date.now() - hit.at < PANE_PID_TTL_MS) {
|
|
1920
|
-
return hit.pid
|
|
1921
|
-
}
|
|
1922
|
-
const panePid = await getPanePid(name)
|
|
1923
|
-
if (panePid && procMap.has(panePid)) {
|
|
1924
|
-
panePidByName.set(name, { pid: panePid, at: Date.now() })
|
|
1925
|
-
return panePid
|
|
1926
|
-
}
|
|
1927
|
-
panePidByName.delete(name)
|
|
1928
|
-
return null
|
|
1929
|
-
}
|
|
1930
|
-
|
|
1931
|
-
const tick = async () => {
|
|
1932
|
-
if (stopped) return
|
|
1933
|
-
try {
|
|
1934
|
-
const names = []
|
|
1935
|
-
for (const n of tracker.byName.keys()) {
|
|
1936
|
-
if (tracker.isActive(n)) names.push(n)
|
|
1937
|
-
}
|
|
1938
|
-
if (names.length === 0) return
|
|
1939
|
-
const procMap = await snapshotProcs()
|
|
1940
|
-
if (procMap.size === 0) return // ps 失敗時は据え置き (spurious ready 防止)
|
|
1941
|
-
for (const name of names) {
|
|
1942
|
-
const root = await resolvePanePid(name, procMap)
|
|
1943
|
-
if (!root) {
|
|
1944
|
-
// 解決失敗は observe しない (count=0 を注入すると誤って ready が出るため)。
|
|
1945
|
-
// SDK チャット等で tmux セッションが無いものは連続失敗で tracker から忘れる。
|
|
1946
|
-
const k = (unresolvedStreak.get(name) ?? 0) + 1
|
|
1947
|
-
if (k >= 8) {
|
|
1948
|
-
tracker.forget(name)
|
|
1949
|
-
unresolvedStreak.delete(name)
|
|
1950
|
-
panePidByName.delete(name)
|
|
1951
|
-
} else {
|
|
1952
|
-
unresolvedStreak.set(name, k)
|
|
1953
|
-
}
|
|
1954
|
-
continue
|
|
1955
|
-
}
|
|
1956
|
-
unresolvedStreak.delete(name)
|
|
1957
|
-
const count = busyChildCount(root, procMap)
|
|
1958
|
-
const { ready } = tracker.observe(name, count)
|
|
1959
|
-
if (ready) {
|
|
1960
|
-
await writeSessionEventFile(name, "ready", Date.now())
|
|
1961
|
-
// busy で defer した +新規 をターン終了 (ready) のここで 1 回だけ実行する。
|
|
1962
|
-
try {
|
|
1963
|
-
await onReady?.(name)
|
|
1964
|
-
} catch (err) {
|
|
1965
|
-
logger?.warn({ err: err?.message }, "readiness onReady callback failed")
|
|
1966
|
-
}
|
|
1967
|
-
}
|
|
1968
|
-
}
|
|
1969
|
-
} catch (err) {
|
|
1970
|
-
logger?.warn({ err: err?.message }, "readiness loop tick failed")
|
|
1971
|
-
}
|
|
1972
|
-
}
|
|
1973
|
-
|
|
1974
|
-
const ti = setInterval(tick, intervalMs)
|
|
1975
|
-
ti.unref?.()
|
|
1976
|
-
return {
|
|
1977
|
-
stop() {
|
|
1978
|
-
stopped = true
|
|
1979
|
-
clearInterval(ti)
|
|
1980
|
-
},
|
|
1981
|
-
}
|
|
1982
|
-
}
|
|
1983
|
-
|
|
1984
|
-
/**
|
|
1985
|
-
* 全 tmux session の状態を定期 capture し、変化したものだけ Hub に push する。
|
|
1986
|
-
*
|
|
1987
|
-
* - 前回値との比較で status / context_pct のどちらかが変化したら送る
|
|
1988
|
-
* - 新規 session は無条件 push、消えた session は無視 (browser 側は
|
|
1989
|
-
* セッション一覧の更新で消滅を検知する)
|
|
1990
|
-
* - tmux 自体が動いてない場合 (listSessionStates → []) は何も push しない
|
|
1991
|
-
*/
|
|
1992
|
-
// (i) 出力フロー検知の有効窓。ペイン出力領域の署名 (output_sig) が直近この時間内に変化して
|
|
1993
|
-
// いれば「出力ストリーミング中」とみなし proc_busy に OR する。state loop は 5s 周期なので、
|
|
1994
|
-
// 連続生成中は毎 tick 署名が変わり常時 true、出力が止まれば次 tick (≤5s) で署名不変になり
|
|
1995
|
-
// 窓経過後に false へ落ちる (ターン終了後のスピナー固着=症状A を悪化させない短さ)。
|
|
1996
|
-
// 出力フロー検知窓。state loop tick(5000ms)の整数倍にして、出力が止まった後の outputActive が
|
|
1997
|
-
// 5s tick 境界で true↔false フラッピングするのを防ぐ(8000 は非整数倍で proc_busy 単独反転 push が
|
|
1998
|
-
// 多発していた=perf監査。10000=2tick で安定側、影響は『出力停止とみなす猶予 8s→10s』のみ)。
|
|
1999
|
-
const OUTPUT_ACTIVE_MS = Number(process.env.HUB_AGENT_OUTPUT_ACTIVE_MS ?? 10000)
|
|
2000
|
-
|
|
2001
1654
|
// 状態信号の計装ログ(session.state/event push)を出すか。既定 OFF。障害解析時に
|
|
2002
1655
|
// HUB_AGENT_STATE_TRACE=1 で一時的に有効化する(常時 info 出力はログ肥大の主因=perf監査で
|
|
2003
1656
|
// 直近1h ログの約半分を占めると実測。挙動には一切影響しない純ログ)。
|
|
2004
1657
|
const STATE_TRACE = process.env.HUB_AGENT_STATE_TRACE === "1"
|
|
2005
1658
|
|
|
2006
|
-
// 権威的な「応答停止 (ハング)」判定の無進捗しきい値。ターン進行中 (armed) なのに、実際の
|
|
2007
|
-
// 進捗 (子プロセス実行中 / 出力フロー / ペインのライブ processing) がこの時間継続して観測
|
|
2008
|
-
// されなければ stalled とする。frontend 側の jsonl 無音タイマー (長時間ツールで誤検知し、
|
|
2009
|
-
// 本当のハングでは turnActive クリアにより見逃す) を置き換える正本。frontend の
|
|
2010
|
-
// TURN_STALL_WARN_MS と揃える。
|
|
2011
|
-
const STALL_WARN_MS = Number(process.env.HUB_AGENT_STALL_WARN_MS ?? 90000)
|
|
2012
|
-
|
|
2013
1659
|
/**
|
|
2014
1660
|
* tmux.list_sessions 応答に同梱する session オブジェクトを組み立てる純関数。
|
|
2015
1661
|
*
|
|
@@ -2086,14 +1732,6 @@ export function applyChatOverlay(paneStatus, paneContextPct, chat) {
|
|
|
2086
1732
|
return { status, contextPct }
|
|
2087
1733
|
}
|
|
2088
1734
|
|
|
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
1735
|
|
|
2098
1736
|
/**
|
|
2099
1737
|
* codex 行の使用率を、行の代表会話の live 値で上書きするかを決める。
|
|
@@ -2235,116 +1873,6 @@ export function filterCodexSessionsByAuthority(codexSessions, producerByCwd, seq
|
|
|
2235
1873
|
return codexSessions.filter((e) => seqForCwd(e.cwd) === producerByCwd.get(e.cwd))
|
|
2236
1874
|
}
|
|
2237
1875
|
|
|
2238
|
-
/**
|
|
2239
|
-
* S1 (2026-07): claude.attach / codex.attach の直後に、その cwd に対応する session.state を
|
|
2240
|
-
* 1 回だけ即時 push する。startStateLoop の周期 (既定 5s) を待つと、エンジン切替や新規
|
|
2241
|
-
* セッション選択の直後もサイドバーのドーナツ/ステータスドットが前セッションの値のまま
|
|
2242
|
-
* 数秒間残ってしまう (症状③④)。claude.tui.bind が既に持つ「即時ステータススナップショット」
|
|
2243
|
-
* (main.mjs 内 detectSessionState 直後の push) と同じ発想で、チャット (SDK/app-server) 側にも
|
|
2244
|
-
* 対称の即時 push を用意する。
|
|
2245
|
-
*
|
|
2246
|
-
* cwd → session_name の解決: claude.attach / codex.attach は tmux の session_name を知らない
|
|
2247
|
-
* (browser は cwd/stream_id しか送らない) ため、listSessionStates で全 tmux session を走査し
|
|
2248
|
-
* cwd が一致するものへ push する。tmux 自体が無い / 一致が無ければ何もしない (安全側、次の
|
|
2249
|
-
* poll 系フォールバックに委ねる)。
|
|
2250
|
-
*
|
|
2251
|
-
* ⚠️ T05945: 同一 cwd に claude 行 (`<dir>`) と codex 行 (`<dir>__codex`) が並ぶため、cwd 一致
|
|
2252
|
-
* だけで push すると **attach した CLI のゼロ値/実値が反対側の CLI 行にも書き込まれる**
|
|
2253
|
-
* (codex を新規 attach した瞬間に claude 行のドーナツが 0% に落ちる)。行のセッション名から
|
|
2254
|
-
* 導いた cli_kind が attach 中の CLI と一致する行にだけ push する。
|
|
2255
|
-
*
|
|
2256
|
-
* `listSessionStatesFn` は単体テスト用の DI ポイント (既定は実 `listSessionStates`、tmux を
|
|
2257
|
-
* 起動する)。実運用では省略してよい。
|
|
2258
|
-
*
|
|
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
|
|
2260
|
-
*/
|
|
2261
|
-
export async function pushImmediateChatState({
|
|
2262
|
-
client,
|
|
2263
|
-
plugins,
|
|
2264
|
-
logger,
|
|
2265
|
-
cwd,
|
|
2266
|
-
cliKind,
|
|
2267
|
-
sessionId,
|
|
2268
|
-
streamId,
|
|
2269
|
-
contextPct = 0,
|
|
2270
|
-
listSessionStatesFn = listSessionStates,
|
|
2271
|
-
}) {
|
|
2272
|
-
if (!cwd || !client) return
|
|
2273
|
-
try {
|
|
2274
|
-
const states = await listSessionStatesFn({ plugins, logger })
|
|
2275
|
-
for (const s of states) {
|
|
2276
|
-
if (s.cwd !== cwd) continue
|
|
2277
|
-
if (cliKindForSessionName(s.session_name) !== cliKind) continue
|
|
2278
|
-
const status = "waiting"
|
|
2279
|
-
// 代表行は直後の applyRowChatState が代表会話の実 ID で上書きする。
|
|
2280
|
-
const contextSessionId = sessionId ?? null
|
|
2281
|
-
tmuxSessionsSnapshotCache.clear()
|
|
2282
|
-
client.send(applyRowChatState({
|
|
2283
|
-
type: "session.state",
|
|
2284
|
-
session_name: s.session_name,
|
|
2285
|
-
status,
|
|
2286
|
-
context_pct: typeof contextPct === "number" ? contextPct : null,
|
|
2287
|
-
stable: status !== "processing",
|
|
2288
|
-
proc_busy: false,
|
|
2289
|
-
child_busy: false,
|
|
2290
|
-
stalled: false,
|
|
2291
|
-
turn_active: status == null ? null : status === "processing",
|
|
2292
|
-
cli_kind: cliKind,
|
|
2293
|
-
// context_pct がどの会話の値かを明示する。frontend は ID が変わったときだけ
|
|
2294
|
-
// 0% を採用し、同一会話の一時的な 0/null は直近の確定値を保持する。
|
|
2295
|
-
context_session_id: contextSessionId,
|
|
2296
|
-
chat_session_id: contextSessionId,
|
|
2297
|
-
}, cwd, cliKind))
|
|
2298
|
-
}
|
|
2299
|
-
} catch (err) {
|
|
2300
|
-
logger?.warn?.({ err: err?.message, cwd }, "pushImmediateChatState failed")
|
|
2301
|
-
}
|
|
2302
|
-
}
|
|
2303
|
-
|
|
2304
|
-
/**
|
|
2305
|
-
* チャット入力の受付時刻を、対応する tmux 行の prompt_submit として **in-process で** publish する。
|
|
2306
|
-
*
|
|
2307
|
-
* state loop の 5 秒 tick だけでは、5 秒未満で完了するターンや、終了と次の開始の間に tick が
|
|
2308
|
-
* 入らない連続ターンを取りこぼし、観測できても送信時刻ではなく assistant event の時刻になる。
|
|
2309
|
-
* そこで handler が入力を受け付けた瞬間 (dispatch 前) に、cwd と CLI 種別が一致する tmux 行へ
|
|
2310
|
-
* publishSessionEvent(prompt_submit, 受付時刻, fresh) を流す。
|
|
2311
|
-
*
|
|
2312
|
-
* ファイルは書かない。並び順の正本は ingestSessionEventForSort の sidecar (poll の
|
|
2313
|
-
* sort_affecting_at で復元される) で、event ファイルは status 復元用に state loop が従来どおり
|
|
2314
|
-
* 書く (その prompt_submit の at は chatSessionEventForTick が受付時刻へ揃える)。直接経路が
|
|
2315
|
-
* ファイルを書かないので、fs.watch の読み落とし・並行 rename・kill 中の保留書き込みといった
|
|
2316
|
-
* ファイル由来の競合そのものが無い。
|
|
2317
|
-
*
|
|
2318
|
-
* bridge が入力を拒否した場合 (empty / queue dead 等) も publish 済みのまま取り消さない:
|
|
2319
|
-
* 並び順の定義は「最後に人間が送った順」で、送信操作自体は起きているため (仕様)。
|
|
2320
|
-
*/
|
|
2321
|
-
export async function notifyChatPromptSubmit({
|
|
2322
|
-
cwd,
|
|
2323
|
-
cliKind,
|
|
2324
|
-
at,
|
|
2325
|
-
client,
|
|
2326
|
-
readinessTracker,
|
|
2327
|
-
plugins,
|
|
2328
|
-
logger,
|
|
2329
|
-
listSessionStatesFn = listSessionStates,
|
|
2330
|
-
publishFn = publishSessionEvent,
|
|
2331
|
-
}) {
|
|
2332
|
-
if (!cwd || !client) return
|
|
2333
|
-
try {
|
|
2334
|
-
const states = await listSessionStatesFn({ plugins, logger })
|
|
2335
|
-
for (const s of states) {
|
|
2336
|
-
if (s.cwd !== cwd || cliKindForSessionName(s.session_name) !== cliKind) continue
|
|
2337
|
-
try {
|
|
2338
|
-
await publishFn({ client, readinessTracker, logger }, s.session_name, "prompt_submit", at, { fresh: true })
|
|
2339
|
-
} catch (err) {
|
|
2340
|
-
logger?.warn?.({ err: err?.message, cwd, cliKind, session_name: s.session_name }, "notifyChatPromptSubmit publish failed")
|
|
2341
|
-
}
|
|
2342
|
-
}
|
|
2343
|
-
} catch (err) {
|
|
2344
|
-
logger?.warn?.({ err: err?.message, cwd, cliKind }, "notifyChatPromptSubmit failed")
|
|
2345
|
-
}
|
|
2346
|
-
}
|
|
2347
|
-
|
|
2348
1876
|
/**
|
|
2349
1877
|
* chat signal から、この tick で書く session-event を導出する。
|
|
2350
1878
|
* FIFO で前ターンの stop 後に次ターンの processing を観測しても、prompt_submit の at は
|
|
@@ -2432,17 +1960,7 @@ export function decideTurnActiveSource({
|
|
|
2432
1960
|
return "none"
|
|
2433
1961
|
}
|
|
2434
1962
|
|
|
2435
|
-
export function startStateLoop({ client,
|
|
2436
|
-
const lastByName = new Map() // session_name → {status, context_pct}
|
|
2437
|
-
const lastTurnAtByName = new Map() // session_name → 最後に event ファイル化した turnAt
|
|
2438
|
-
// session_name → 最後に書いた chat 由来 event 種別。エッジ駆動 (種別遷移時のみ書く) にして
|
|
2439
|
-
// 生成中の prompt_submit 連打を防ぐ (並行生成でサイドバーが揺れる原因)。
|
|
2440
|
-
const lastChatEventByName = new Map()
|
|
2441
|
-
// (i) session_name → {sig, changedAt}: 出力領域署名の最終変化時刻 (出力フロー検知用)。
|
|
2442
|
-
const outputFlowByName = new Map()
|
|
2443
|
-
// 権威的 stall 判定 (arm 非依存・cap 非依存)。state loop が観測する signal だけで turn の開閉と
|
|
2444
|
-
// 無進捗を判定する。旧 lastProgressAtByName + armed ゲートの「フック依存 + cap 140s で消える」死角を解消。
|
|
2445
|
-
const stallTracker = new StallTracker({ stallWarnMs: STALL_WARN_MS })
|
|
1963
|
+
export function startStateLoop({ client, logger, intervalMs, claudeBridge, codexBridge }) {
|
|
2446
1964
|
// 総 live 上限 M のログ抑制状態。⚠️ **この state loop インスタンスが所有する**(module-global に
|
|
2447
1965
|
// 置くと、複数インスタンスや並行テストが互いのログを抑制してしまう)。「超過しているが全部
|
|
2448
1966
|
// 保護されていて刈れない」tick は 5 秒ごとに同じ行を出し続けるので、状態が変わったときと
|
|
@@ -2450,17 +1968,6 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
|
|
|
2450
1968
|
const warmBoundsLogState = { lastUnresolved: 0, lastLoggedAtMs: 0 }
|
|
2451
1969
|
let stopped = false
|
|
2452
1970
|
|
|
2453
|
-
// RC-8: WS 再接続のたびに差分送信の基準 (lastByName) をクリアし、次 tick で全 session.state を
|
|
2454
|
-
// 再 push する。差分送信のままだと、切断中に起きた status 遷移 (processing→stop 等) の push が
|
|
2455
|
-
// 失われ、再接続後も frontend が古い processing に固着する (zombie WS の検知が遅れると最大数十秒)。
|
|
2456
|
-
// client は (再)接続ごとに "open" を emit する。フロントの tmux.list_sessions poll でも回復するが、
|
|
2457
|
-
// これで再接続直後の次 tick (≤intervalMs) に短縮する。初回接続時は空 Map の clear で no-op。
|
|
2458
|
-
const onReopen = () => {
|
|
2459
|
-
lastByName.clear()
|
|
2460
|
-
tmuxSessionsSnapshotCache.clear()
|
|
2461
|
-
}
|
|
2462
|
-
client.on("open", onReopen)
|
|
2463
|
-
|
|
2464
1971
|
const tick = async () => {
|
|
2465
1972
|
if (stopped) return
|
|
2466
1973
|
try {
|
|
@@ -2505,232 +2012,6 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
|
|
|
2505
2012
|
// 実コンテキスト窓サイズ (1M ベータ等) を反映。contextPctFromUsage の分母を
|
|
2506
2013
|
// 5s ごとに最新化し、ドーナツが 200k 固定で振り切れるのを防ぐ。
|
|
2507
2014
|
await refreshContextWindow()
|
|
2508
|
-
const states = await listSessionStatesFn({ plugins, logger })
|
|
2509
|
-
for (const s of states) {
|
|
2510
|
-
// T05945: この行がどの CLI のセッションかは **セッション名から構造的に決まる**
|
|
2511
|
-
// (`<dir>` = claude / `<dir>__codex` = codex)。chat 信号の読み出し・turn_active の
|
|
2512
|
-
// 判定源・session.state に載せる cli_kind はすべてこの 1 値から導く (正本一本化)。
|
|
2513
|
-
const sessionCliKind = cliKindForSessionName(s.session_name)
|
|
2514
|
-
// チャットモード補完: tmux ペインのスクレイプは TUI 前提で効かないため、
|
|
2515
|
-
// session の cwd に一致する新鮮なチャット信号があれば status/context% を
|
|
2516
|
-
// 上書きする (Hub ホスト型 Cockpit のステータスドット/各行ドーナツ用)。
|
|
2517
|
-
// 代表が設定済みなら、その会話の値だけを使い、欠けた値は null とする。
|
|
2518
|
-
// 代表未設定の行は従来の applyChatOverlay / pane processing ガードを使う。
|
|
2519
|
-
// (cwd, cliKind) キーで自 CLI のチャット信号だけを読む。同一 cwd の別 CLI 行が
|
|
2520
|
-
// 立てた status/context_pct を拾わない (交差汚染の遮断)。
|
|
2521
|
-
const representative = hasRepresentativeRow(s.cwd, sessionCliKind)
|
|
2522
|
-
const chat = s.cwd ? readRowChatState(s.cwd, sessionCliKind) : null
|
|
2523
|
-
const overlay = applyChatOverlay(s.status, s.context_pct, chat)
|
|
2524
|
-
let status = representative ? chat?.status ?? null : overlay.status
|
|
2525
|
-
let contextPct = representative ? chat?.context_pct ?? null : overlay.contextPct
|
|
2526
|
-
// 内容安定で「確実に停止」と判定できたか (frontend が 2 サンプル消灯確認を省いて
|
|
2527
|
-
// 即消灯するためのフラグ)。chat 信号で processing へ上書きされた場合は false。
|
|
2528
|
-
let stable = status !== "processing" && s.stable === true
|
|
2529
|
-
// プロセス内省由来の busy レベル (Stop フック/ツール実行中=true)。frontend は
|
|
2530
|
-
// これでペイン由来の早期消灯を抑止し、tool/Stop フック中もスピナーを保つ。
|
|
2531
|
-
// 高速 readiness loop が維持する値をそのまま相乗せする (この loop では計算しない)。
|
|
2532
|
-
const childBusy = readinessTracker ? readinessTracker.getBusy(s.session_name) : false
|
|
2533
|
-
// (i) 出力フロー検知: proc 内省は pane_pid 子孫の「プロセス数」で busy を見るため、
|
|
2534
|
-
// ツールを起動しない純テキスト生成 (モデルが出力を流すだけ) を busy として検出できない。
|
|
2535
|
-
// その間に capture-pane のスピナー検出が 1 フレーム取りこぼすと frontend がアイドルと
|
|
2536
|
-
// 誤読してスピナーを消す (生成中なのに消える=症状B)。出力領域の署名が直近変化していれば
|
|
2537
|
-
// 「出力中」とみなし proc_busy に OR する。childBusy=false の純テキスト生成中も
|
|
2538
|
-
// proc_busy=true となり frontend のペイン由来消灯が抑止される。出力が止まれば署名が
|
|
2539
|
-
// 不変になり窓経過後に false へ落ちるので、ターン終了後のスピナー固着は悪化させない。
|
|
2540
|
-
let outputActive = false
|
|
2541
|
-
if (typeof s.output_sig === "string") {
|
|
2542
|
-
const prevFlow = outputFlowByName.get(s.session_name)
|
|
2543
|
-
let changedAt = prevFlow ? prevFlow.changedAt : 0
|
|
2544
|
-
if (!prevFlow || prevFlow.sig !== s.output_sig) changedAt = Date.now()
|
|
2545
|
-
outputFlowByName.set(s.session_name, { sig: s.output_sig, changedAt })
|
|
2546
|
-
outputActive = Date.now() - changedAt < OUTPUT_ACTIVE_MS
|
|
2547
|
-
}
|
|
2548
|
-
const procBusy = childBusy || outputActive
|
|
2549
|
-
// 権威的 stall 判定 (StallTracker, arm 非依存・cap 非依存)。armed (バンドルフックが効く
|
|
2550
|
-
// セッション) は OR で常に turn 開扱い (従来挙動の上位互換)。フック未発火/旧世代hookセッション
|
|
2551
|
-
// でも観測した processing で turnOpen を立て、frozen スピナー (spinner_present) を維持して proc 子も
|
|
2552
|
-
// 出力も止まったハングを拾う。proc 内省の cap(140s) disarm に依存しないので 140s 以後も消えない。
|
|
2553
|
-
// 旧実装の「armed 依存 + cap で消える」二重死角を解消。frontend はこれを応答停止バナーの正本に使う。
|
|
2554
|
-
const armed = readinessTracker
|
|
2555
|
-
? readinessTracker.isArmed(s.session_name)
|
|
2556
|
-
: false
|
|
2557
|
-
const nowMs = Date.now()
|
|
2558
|
-
const { stalled } = stallTracker.observe(s.session_name, {
|
|
2559
|
-
armed,
|
|
2560
|
-
processing: status === "processing",
|
|
2561
|
-
procBusy,
|
|
2562
|
-
spinnerPresent: s.spinner_present === true,
|
|
2563
|
-
stable,
|
|
2564
|
-
now: nowMs,
|
|
2565
|
-
})
|
|
2566
|
-
|
|
2567
|
-
// 代表行は下の同期読み出しが turn_active も確定する。
|
|
2568
|
-
// 代表未設定の行だけ、従来の履歴・pane probe を使う。
|
|
2569
|
-
let turnActive = null
|
|
2570
|
-
// codex 行のときだけ rollout ベース判定の材料を引く (claude 行では常に null)。
|
|
2571
|
-
// 行の**代表会話の session** を 1 度だけ引き、threadId(turn_active 判定用・会話 ID) と
|
|
2572
|
-
// seq(context% の live source 参照キー) の両方を同一 session から取る。⚠️ 別々に引くと
|
|
2573
|
-
// 「背景会話の使用率に代表会話の ID が付く」不整合になる。usage.response と同じ
|
|
2574
|
-
// codexRowSession を使い、代表が live でなければ null(別会話の値で埋めない)。
|
|
2575
|
-
const codexAuthSession =
|
|
2576
|
-
s.cwd && sessionCliKind === "codex"
|
|
2577
|
-
? codexRowSession(codexBridge, s.cwd)
|
|
2578
|
-
: null
|
|
2579
|
-
const codexThreadId = codexAuthSession?.threadId || null
|
|
2580
|
-
// T06407 増分2 (axis3): codex 行の context%(ドーナツ) は TTL 非依存の live store
|
|
2581
|
-
// (thread/tokenUsage/updated 由来) を権威 producer(seq) で引き、chat overlay より優先して
|
|
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
|
-
})
|
|
2591
|
-
}
|
|
2592
|
-
// fresh bind は実 session_id がまだ確定していない。tmux.list の statusLine / cwd 最新
|
|
2593
|
-
// JSONL は直前会話の値を返し得るため、この間は必ず 0% を正本にする。これをしないと
|
|
2594
|
-
// bind の即時 0% snapshot の直後に state loop が旧値を同じ fresh:<id> で再送して
|
|
2595
|
-
// ドーナツを復活させる。
|
|
2596
|
-
const freshUnbound =
|
|
2597
|
-
sessionCliKind === "claude" && isFreshUnboundBind(boundSessions?.get(s.session_name))
|
|
2598
|
-
if (freshUnbound && !representative) contextPct = 0
|
|
2599
|
-
const turnActiveSource = representative ? "representative" : decideTurnActiveSource({
|
|
2600
|
-
chatStatus: chat?.status,
|
|
2601
|
-
cwd: s.cwd,
|
|
2602
|
-
isFreshUnbound: freshUnbound && !representative,
|
|
2603
|
-
codexThreadId,
|
|
2604
|
-
sessionCliKind,
|
|
2605
|
-
})
|
|
2606
|
-
switch (turnActiveSource) {
|
|
2607
|
-
case "chat-active":
|
|
2608
|
-
turnActive = true
|
|
2609
|
-
break
|
|
2610
|
-
case "chat-inactive":
|
|
2611
|
-
turnActive = false
|
|
2612
|
-
break
|
|
2613
|
-
case "fresh-unbound":
|
|
2614
|
-
// 症状2b 根治: fresh bind プレースホルダの間は、別アクティビティ jsonl を mtime 最新で
|
|
2615
|
-
// 取り違える汚染を避けて turn_active を出さない (null=不明)。実セッション id へ adopt
|
|
2616
|
-
// されれば下の通常経路に乗る。青を出さない安全側にのみ倒すため固着青は生まない。
|
|
2617
|
-
turnActive = null
|
|
2618
|
-
break
|
|
2619
|
-
case "codex":
|
|
2620
|
-
turnActive = await codexTurnActiveForThread(codexThreadId)
|
|
2621
|
-
break
|
|
2622
|
-
case "claude": {
|
|
2623
|
-
// P5 (T06570 症状3): チャットセッションは、そのセッションの SDK query ライフサイクル由来の
|
|
2624
|
-
// 権威フラグ _busy を最優先する (Codex の rollout flag 優先と対称)。これにより browser を
|
|
2625
|
-
// 離れて detach され this.sessions から外れた warm セッションでも、稼働中ターンの turn_active
|
|
2626
|
-
// が維持され、非選択行のドットが赤へ誤落ちしない。加えて bare-shell pane を dead 判定して
|
|
2627
|
-
// 赤へ倒す paneAliveProbe 経路 (チャットは実体が SDK 子プロセスで pane は素のシェル) も回避する。
|
|
2628
|
-
// 代表があれば、その会話の busy だけを読む。不在なら null のままにする。
|
|
2629
|
-
// 代表未設定の行だけ、従来の cwd 最新 / jsonl / pane probe を使う。
|
|
2630
|
-
const chatActive = claudeBridge?.chatTurnActiveForCwd?.(s.cwd)
|
|
2631
|
-
if (representative || chatActive === true || chatActive === false) {
|
|
2632
|
-
turnActive = chatActive
|
|
2633
|
-
} else {
|
|
2634
|
-
turnActive = await turnActiveForCwd(
|
|
2635
|
-
s.cwd,
|
|
2636
|
-
resolveStateLoopBoundSessionId(boundSessions, s.session_name, s.cwd, claudeBridge),
|
|
2637
|
-
s.session_name,
|
|
2638
|
-
{ paneAliveProbe: paneClaudeAliveOrUnknown },
|
|
2639
|
-
)
|
|
2640
|
-
}
|
|
2641
|
-
break
|
|
2642
|
-
}
|
|
2643
|
-
default:
|
|
2644
|
-
turnActive = null
|
|
2645
|
-
}
|
|
2646
|
-
|
|
2647
|
-
// cli_kind (T05945): **セッション名から導いた構造的な値**を常に載せる。旧実装は
|
|
2648
|
-
// 「チャット信号があればその cliKind、無ければ null」だったため、TUI 専用行や
|
|
2649
|
-
// チャット未接続行で null になり、frontend が行の CLI を知る術が無かった。
|
|
2650
|
-
// (cwd, cliKind) キー化により「チャット信号の cliKind == 行の cliKind」が恒真に
|
|
2651
|
-
// なったので、常に非 null の構造値を送る方が真に厳しい (= 情報量が多い) 上位互換。
|
|
2652
|
-
//
|
|
2653
|
-
// context_session_id は「この context_pct がどの会話の値か」。チャットの session/thread を
|
|
2654
|
-
// 優先し、Claude TUI 専用行では bind 済みの session_id(fresh は fresh:<request_id>)を使う。
|
|
2655
|
-
// frontend はこれを境界として、新規会話の 0% と同一会話の一時的な 0/null を区別する。
|
|
2656
|
-
const chatCliKind = sessionCliKind
|
|
2657
|
-
const contextSessionId =
|
|
2658
|
-
chat?.sessionId ??
|
|
2659
|
-
(sessionCliKind === "claude"
|
|
2660
|
-
? boundSessions?.get(s.session_name) ?? `unbound:${s.session_name}`
|
|
2661
|
-
: codexThreadId)
|
|
2662
|
-
|
|
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,
|
|
2689
|
-
})
|
|
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(() => {})
|
|
2695
|
-
}
|
|
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")
|
|
2701
|
-
tmuxSessionsSnapshotCache.clear()
|
|
2702
|
-
client.send({ type: "session.state", session_name: s.session_name, ...payload })
|
|
2703
|
-
}
|
|
2704
|
-
// 消滅セッションの GC (perf監査/メモリリーク対策): tmux から消えたセッションの状態 Map を
|
|
2705
|
-
// まとめて解放する。clear() は再接続時(onReopen)のみのため、worktree 案件の生成/破棄で
|
|
2706
|
-
// 各 per-session Map が線形増加し RSS が無入力時も成長していた。tmux が一時的に空応答
|
|
2707
|
-
// (states=[]) のときは全消去しないよう states.length>0 をガードにする。
|
|
2708
|
-
if (states.length > 0) {
|
|
2709
|
-
const liveNames = new Set(states.map((s) => s.session_name))
|
|
2710
|
-
for (const name of [...lastByName.keys()]) {
|
|
2711
|
-
if (liveNames.has(name)) continue
|
|
2712
|
-
// 終端を能動 broadcast (症状3 根治): tmux から消えた = crash/外部 kill/別経路
|
|
2713
|
-
// 終了。explicit kill は kill_session ハンドラが session.gone を出すが、それ以外
|
|
2714
|
-
// の消滅はここが唯一の検知点。frontend が states/lastEvent を忘れてドットを
|
|
2715
|
-
// 'down' に倒せるよう、ローカル Map を forget する前に通知する。
|
|
2716
|
-
try {
|
|
2717
|
-
tmuxSessionsSnapshotCache.clear()
|
|
2718
|
-
client.send({ type: "session.gone", session_name: name })
|
|
2719
|
-
} catch {
|
|
2720
|
-
/* ignore */
|
|
2721
|
-
}
|
|
2722
|
-
invalidateSessionCache(name) // capture/cwd/statusGate/spinnerFreeze/stability (state.mjs)
|
|
2723
|
-
stallTracker.forget(name) // StallTracker.byName
|
|
2724
|
-
readinessTracker?.forget?.(name) // ReadinessTracker.byName
|
|
2725
|
-
forgetSortAffectingState(name) // sawUserTurn / sort_affecting_at / lastEventAt (リーク防止)
|
|
2726
|
-
lastByName.delete(name)
|
|
2727
|
-
outputFlowByName.delete(name)
|
|
2728
|
-
lastTurnAtByName.delete(name)
|
|
2729
|
-
// 死んだセッションの bind 記録も掃除する (同名再作成時は claude.tui.bind が再登録)。
|
|
2730
|
-
boundSessions?.delete(name)
|
|
2731
|
-
}
|
|
2732
|
-
}
|
|
2733
|
-
}
|
|
2734
2015
|
} catch (err) {
|
|
2735
2016
|
logger?.warn({ err: err.message }, "state loop tick failed")
|
|
2736
2017
|
}
|
|
@@ -2748,24 +2029,6 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
|
|
|
2748
2029
|
stopped = true
|
|
2749
2030
|
clearInterval(ti)
|
|
2750
2031
|
clearTimeout(t0)
|
|
2751
|
-
client.off?.("open", onReopen)
|
|
2752
|
-
},
|
|
2753
|
-
/**
|
|
2754
|
-
* tmux.list_sessions poll fallback 用の session.state snapshot を返す。
|
|
2755
|
-
*
|
|
2756
|
-
* 背景: session.state push は差分送信(prev 比較で変化時のみ送信)のため、WS 再接続
|
|
2757
|
-
* 直後や push 取りこぼし時にドット権威信号(turn_active/proc_busy/child_busy/stalled)が
|
|
2758
|
-
* 古値固着し得る(2026-06-28 ワークフローで検出した「事故的非対称」)。tmux.list_sessions
|
|
2759
|
-
* poll の応答にこれら 4 信号を同梱することで、status/context_pct と同じ精度で poll 経路
|
|
2760
|
-
* からも最新化できるようにする(T1)。
|
|
2761
|
-
*
|
|
2762
|
-
* shallow copy で返却して呼び出し側の mutate が lastByName を汚染するのを防ぐ
|
|
2763
|
-
* (state loop の差分送信ロジックが prev 比較で誤判定するのを避ける = 症状A/B 再発防止)。
|
|
2764
|
-
* 名前が未知 or まだ tick が走っていなければ null(frontend は legacy フォールバックへ)。
|
|
2765
|
-
*/
|
|
2766
|
-
getSnapshot(name) {
|
|
2767
|
-
const entry = lastByName.get(name)
|
|
2768
|
-
return entry ? { ...entry } : null
|
|
2769
2032
|
},
|
|
2770
2033
|
}
|
|
2771
2034
|
}
|
|
@@ -3030,25 +2293,10 @@ export function handleClaudeChatInput(msg, ctx) {
|
|
|
3030
2293
|
}
|
|
3031
2294
|
return undefined
|
|
3032
2295
|
}
|
|
3033
|
-
//
|
|
3034
|
-
// された場合に別行へ記録する)。受付時刻の記録と並び順の publish も dispatch **前** に行う:
|
|
3035
|
-
// Claude の resident reconcile / Codex の turn/start RPC が未解決の間に assistant / turn/started
|
|
3036
|
-
// が先に届いて state loop が processing を観測しても、記録済みなら観測時刻ではなく受付時刻で
|
|
3037
|
-
// 書かれる。bridge が拒否しても取り消さない (仕様。notifyChatPromptSubmit の JSDoc)。
|
|
2296
|
+
// 非同期の入力処理へ渡す前に cwd と受付時刻を確定する。
|
|
3038
2297
|
const cwdSnapshot = ctx.claudeBridge.getCwd?.(stream_id) ?? null
|
|
3039
2298
|
if (cwdSnapshot) {
|
|
3040
2299
|
recordPromptAccepted(cwdSnapshot, "claude", acceptedAt, ctx.claudeBridge.sessions?.get(stream_id), ctx.claudeBridge)
|
|
3041
|
-
notifyChatPromptSubmit({
|
|
3042
|
-
cwd: cwdSnapshot,
|
|
3043
|
-
cliKind: "claude",
|
|
3044
|
-
at: acceptedAt,
|
|
3045
|
-
plugins: ctx.plugins,
|
|
3046
|
-
logger: ctx.logger,
|
|
3047
|
-
client: ctx.client,
|
|
3048
|
-
readinessTracker: ctx.readinessTracker,
|
|
3049
|
-
listSessionStatesFn: ctx._listSessionStates || listSessionStates,
|
|
3050
|
-
publishFn: ctx._publishSessionEvent || publishSessionEvent,
|
|
3051
|
-
}).catch(() => {})
|
|
3052
2300
|
}
|
|
3053
2301
|
const p = ctx.claudeBridge
|
|
3054
2302
|
.input({
|
|
@@ -3264,17 +2512,6 @@ export function handleCodexChatInput(msg, ctx) {
|
|
|
3264
2512
|
const cwdSnapshot = ctx.codexBridge.getCwd?.(stream_id) ?? null
|
|
3265
2513
|
if (cwdSnapshot) {
|
|
3266
2514
|
recordPromptAccepted(cwdSnapshot, "codex", acceptedAt, ctx.codexBridge.sessions?.get(stream_id), ctx.codexBridge)
|
|
3267
|
-
notifyChatPromptSubmit({
|
|
3268
|
-
cwd: cwdSnapshot,
|
|
3269
|
-
cliKind: "codex",
|
|
3270
|
-
at: acceptedAt,
|
|
3271
|
-
plugins: ctx.plugins,
|
|
3272
|
-
logger: ctx.logger,
|
|
3273
|
-
client: ctx.client,
|
|
3274
|
-
readinessTracker: ctx.readinessTracker,
|
|
3275
|
-
listSessionStatesFn: ctx._listSessionStates || listSessionStates,
|
|
3276
|
-
publishFn: ctx._publishSessionEvent || publishSessionEvent,
|
|
3277
|
-
}).catch(() => {})
|
|
3278
2515
|
}
|
|
3279
2516
|
const p = ctx.codexBridge
|
|
3280
2517
|
.input({
|
|
@@ -3384,72 +2621,6 @@ export async function handleCodexInterrupt(msg, ctx) {
|
|
|
3384
2621
|
})
|
|
3385
2622
|
}
|
|
3386
2623
|
|
|
3387
|
-
/**
|
|
3388
|
-
* tmux.kill_session の処理 (症状3 根治)。要求された session を kill し、要求元へ
|
|
3389
|
-
* tmux.kill_session.result を返したうえで、kill できた各 session について
|
|
3390
|
-
* **session.gone を能動 broadcast** する。
|
|
3391
|
-
*
|
|
3392
|
-
* 不変条件: session の終端 (kill/crash/外部終了) は、同一ホストで実体を観測できる
|
|
3393
|
-
* agent が正本として能動 push する。これが無いと frontend は kill 時点の turn_active=true
|
|
3394
|
-
* を keep-last したままステータスドットが青で固着する。最後の 1 セッションを kill した
|
|
3395
|
-
* 場合 state loop の GC は states.length>0 ガードで発火しないため、explicit kill 経路の
|
|
3396
|
-
* ここから session.gone を出すのが確実 (GC は crash/外部 kill の補完)。
|
|
3397
|
-
*
|
|
3398
|
-
* テストから直接呼べるよう export。kill 実体 (killManySessions) は ctx で差し替え可能。
|
|
3399
|
-
*/
|
|
3400
|
-
export async function handleKillSession(msg, ctx) {
|
|
3401
|
-
const killFn = ctx.killManySessions || killManySessions
|
|
3402
|
-
const names = Array.isArray(msg.session_names)
|
|
3403
|
-
? msg.session_names
|
|
3404
|
-
: msg.session_name
|
|
3405
|
-
? [msg.session_name]
|
|
3406
|
-
: []
|
|
3407
|
-
if (names.length === 0) {
|
|
3408
|
-
ctx.client.send({
|
|
3409
|
-
type: "tmux.kill_session.result",
|
|
3410
|
-
request_id: msg.request_id,
|
|
3411
|
-
killed: [],
|
|
3412
|
-
failed: [{ name: "", reason: "session_name(s) required" }],
|
|
3413
|
-
})
|
|
3414
|
-
return
|
|
3415
|
-
}
|
|
3416
|
-
// ⚠️ kill の受信・完了は必ずログに残す (2026-07-31 追加)。
|
|
3417
|
-
// 「停止に 10 秒経っても応答が返らない」の調査で、agent 側に kill の痕跡が
|
|
3418
|
-
// 1 行も無く、遅いのか・届いていないのか・返事が捨てられたのかを**判別できなかった**。
|
|
3419
|
-
// kill は低頻度なのでログ量の懸念は無い。速かった場合も記録する
|
|
3420
|
-
// (速いことの記録が無いと「agent は詰まっていない」を示せない)。
|
|
3421
|
-
ctx.logger?.info?.({ names, request_id: msg.request_id }, "tmux kill_session received")
|
|
3422
|
-
const killStartedAt = Date.now()
|
|
3423
|
-
const r = await killFn(names)
|
|
3424
|
-
const killMs = Date.now() - killStartedAt
|
|
3425
|
-
const sent = ctx.client.send({
|
|
3426
|
-
type: "tmux.kill_session.result",
|
|
3427
|
-
request_id: msg.request_id,
|
|
3428
|
-
killed: r.killed,
|
|
3429
|
-
failed: r.failed,
|
|
3430
|
-
})
|
|
3431
|
-
// 送信できたか自体を残す。ws が閉じていると result は黙って捨てられるため、
|
|
3432
|
-
// 「kill は成功したのに要求元へ返っていない」を後から特定できるようにする。
|
|
3433
|
-
ctx.logger?.info?.(
|
|
3434
|
-
{
|
|
3435
|
-
request_id: msg.request_id,
|
|
3436
|
-
killed: r.killed.length,
|
|
3437
|
-
failed: r.failed.length,
|
|
3438
|
-
kill_ms: killMs,
|
|
3439
|
-
result_sent: sent === true,
|
|
3440
|
-
},
|
|
3441
|
-
"tmux kill_session done",
|
|
3442
|
-
)
|
|
3443
|
-
for (const name of r.killed) {
|
|
3444
|
-
try {
|
|
3445
|
-
ctx.client.send({ type: "session.gone", session_name: name })
|
|
3446
|
-
} catch {
|
|
3447
|
-
/* ignore */
|
|
3448
|
-
}
|
|
3449
|
-
forgetSortAffectingState(name) // sort_affecting 状態のリーク防止 (kill 経路)
|
|
3450
|
-
}
|
|
3451
|
-
}
|
|
3452
|
-
|
|
3453
2624
|
/** usage.response の中身を共有する TTL (ms)。frontend の poll は既定 60 秒周期。 */
|
|
3454
2625
|
export const USAGE_RESPONSE_TTL_MS = 4_000
|
|
3455
2626
|
|
|
@@ -3704,78 +2875,12 @@ export async function handleUsageRequest(msg, ctx, deps = {}) {
|
|
|
3704
2875
|
}
|
|
3705
2876
|
|
|
3706
2877
|
/**
|
|
3707
|
-
* `
|
|
3708
|
-
*
|
|
3709
|
-
* ⚠️ pane に CLI (claude / codex) を起動するかは **createSession 内の
|
|
3710
|
-
* shouldLaunchPaneCli() が一元判定**する (既定は起動しない)。ここで渡す cliKind /
|
|
3711
|
-
* claudeCmd / codexCmd / initialPrompt 等は、`HUB_AGENT_TMUX_LAUNCH_CLI=1` で
|
|
3712
|
-
* CLI を起動するときにだけ使われる。呼び出し元ごとに既定を変えないため、
|
|
3713
|
-
* このハンドラ側で起動可否を判断しないこと。
|
|
3714
|
-
*
|
|
3715
|
-
* dispatch から切り出して export しているのは、実際に tmux へ渡る引数を
|
|
3716
|
-
* テストから検査できるようにするため (test/tmux-pane-cli-launch.test.mjs)。
|
|
3717
|
-
*/
|
|
3718
|
-
export async function handleTmuxCreateSession(msg, ctx) {
|
|
3719
|
-
const name = (msg.session_name || "").trim()
|
|
3720
|
-
const cwd = (msg.cwd || process.env.HOME || "/").trim()
|
|
3721
|
-
if (!name) {
|
|
3722
|
-
ctx.client.send({
|
|
3723
|
-
type: "tmux.create_session.result",
|
|
3724
|
-
request_id: msg.request_id,
|
|
3725
|
-
ok: false,
|
|
3726
|
-
error: "session_name is required",
|
|
3727
|
-
})
|
|
3728
|
-
return
|
|
3729
|
-
}
|
|
3730
|
-
try {
|
|
3731
|
-
await createTmuxSession(name, cwd, {
|
|
3732
|
-
// cli_kind: 'codex' なら codex を、それ以外は claude を pane 起動する (既定 claude)。
|
|
3733
|
-
cliKind: msg.cli_kind === "codex" ? "codex" : "claude",
|
|
3734
|
-
claudeCmd:
|
|
3735
|
-
typeof msg.claude_cmd === "string"
|
|
3736
|
-
? msg.claude_cmd
|
|
3737
|
-
: claudeCmdFromAgentConfig(ctx.config),
|
|
3738
|
-
codexCmd: typeof msg.codex_cmd === "string" ? msg.codex_cmd : undefined,
|
|
3739
|
-
// codex 用設定 (claude 経路では claudeCmd を直接使うため無視される)。
|
|
3740
|
-
model:
|
|
3741
|
-
typeof msg.model === "string"
|
|
3742
|
-
? msg.model
|
|
3743
|
-
: codexDefaultsFromAgentConfig(ctx.config).model,
|
|
3744
|
-
sandbox: typeof msg.sandbox === "string" ? msg.sandbox : undefined,
|
|
3745
|
-
approvalPolicy:
|
|
3746
|
-
typeof msg.approval_policy === "string" ? msg.approval_policy : undefined,
|
|
3747
|
-
effort:
|
|
3748
|
-
typeof msg.effort === "string"
|
|
3749
|
-
? msg.effort
|
|
3750
|
-
: codexDefaultsFromAgentConfig(ctx.config).effort,
|
|
3751
|
-
initialPrompt:
|
|
3752
|
-
typeof msg.initial_prompt === "string" ? msg.initial_prompt : undefined,
|
|
3753
|
-
logger: ctx.logger,
|
|
3754
|
-
})
|
|
3755
|
-
tmuxSessionsSnapshotCache.clear()
|
|
3756
|
-
ctx.client.send({
|
|
3757
|
-
type: "tmux.create_session.result",
|
|
3758
|
-
request_id: msg.request_id,
|
|
3759
|
-
ok: true,
|
|
3760
|
-
session_name: name,
|
|
3761
|
-
})
|
|
3762
|
-
} catch (err) {
|
|
3763
|
-
ctx.client.send({
|
|
3764
|
-
type: "tmux.create_session.result",
|
|
3765
|
-
request_id: msg.request_id,
|
|
3766
|
-
ok: false,
|
|
3767
|
-
error: err.message,
|
|
3768
|
-
})
|
|
3769
|
-
}
|
|
3770
|
-
}
|
|
3771
|
-
|
|
3772
|
-
/**
|
|
3773
|
-
* `worktree.create` ハンドラ。git worktree を作ってから、その dir で tmux セッションを作る。
|
|
2878
|
+
* `worktree.create` ハンドラ。git worktree とチャット行の識別子を作る。
|
|
3774
2879
|
* body: { request_id, dir, branch, cli_kind?, claude_cmd?, codex_cmd?, model?, sandbox?,
|
|
3775
2880
|
* approval_policy?, effort?, initial_prompt? }
|
|
3776
2881
|
*
|
|
3777
|
-
*
|
|
3778
|
-
*
|
|
2882
|
+
* worktree 作成後にワークスペースの準備・同期を待ってから応答する。
|
|
2883
|
+
* CLI 起動用の旧フィールドは互換のため受け付けるが使用しない。
|
|
3779
2884
|
*/
|
|
3780
2885
|
export async function handleWorktreeCreate(msg, ctx) {
|
|
3781
2886
|
const dir = (msg.dir || "").trim()
|
|
@@ -3800,53 +2905,23 @@ export async function handleWorktreeCreate(msg, ctx) {
|
|
|
3800
2905
|
}
|
|
3801
2906
|
try {
|
|
3802
2907
|
const worktreeCreate = ctx._createWorktreeDir || createWorktreeDir
|
|
3803
|
-
const tmuxCreate = ctx._createTmuxSession || createTmuxSession
|
|
3804
2908
|
const wtCliKind = msg.cli_kind === "codex" ? "codex" : "claude"
|
|
3805
|
-
const launchPaneCli = shouldLaunchPaneCli()
|
|
3806
|
-
const codexDefaults = codexDefaultsFromAgentConfig(ctx.config)
|
|
3807
|
-
const tmuxOptions = {
|
|
3808
|
-
cliKind: wtCliKind,
|
|
3809
|
-
claudeCmd:
|
|
3810
|
-
typeof msg.claude_cmd === "string"
|
|
3811
|
-
? msg.claude_cmd
|
|
3812
|
-
: claudeCmdFromAgentConfig(ctx.config),
|
|
3813
|
-
codexCmd: typeof msg.codex_cmd === "string" ? msg.codex_cmd : undefined,
|
|
3814
|
-
model: typeof msg.model === "string" ? msg.model : codexDefaults.model,
|
|
3815
|
-
sandbox: typeof msg.sandbox === "string" ? msg.sandbox : undefined,
|
|
3816
|
-
approvalPolicy:
|
|
3817
|
-
typeof msg.approval_policy === "string" ? msg.approval_policy : undefined,
|
|
3818
|
-
effort: typeof msg.effort === "string" ? msg.effort : codexDefaults.effort,
|
|
3819
|
-
initialPrompt:
|
|
3820
|
-
typeof msg.initial_prompt === "string" ? msg.initial_prompt : undefined,
|
|
3821
|
-
logger: ctx.logger,
|
|
3822
|
-
_resolveCodexBinary: ctx._resolveCodexBinary,
|
|
3823
|
-
}
|
|
3824
|
-
if (launchPaneCli) validateInitialPrompt(tmuxOptions.initialPrompt)
|
|
3825
|
-
// 下流 createSession と同じ options・優先順位で command を1回だけ生成する。
|
|
3826
|
-
// 失敗はこの try の catch へ流れ、worktree / tmux の副作用前に ok:false となる。
|
|
3827
|
-
if (launchPaneCli && wtCliKind === "codex") {
|
|
3828
|
-
tmuxOptions.codexCmd = tmuxOptions.codexCmd ?? buildCodexCmd(tmuxOptions)
|
|
3829
|
-
}
|
|
3830
2909
|
const { wtName, wtPath, createdBranch } = await worktreeCreate(dir, branch)
|
|
3831
|
-
// ⚠️ 宛先一覧の無効化を tmux 作成の成否に依存させない。この一覧の正本はローカルの
|
|
3832
|
-
// ディレクトリ実体なので、worktree ができた時点で既に古い。後続の tmux 作成が失敗しても
|
|
3833
|
-
// worktree dir は残るため、ここで捨てないと残り TTL のあいだ実在する宛先が一覧から欠ける。
|
|
3834
2910
|
chatTargetsCache.clear()
|
|
3835
|
-
// worktree dir で tmux session を作成。tmux.create_session と同じく cli_kind を
|
|
3836
|
-
// 解釈し、codex なら codex を pane 起動する(既定 claude)。
|
|
3837
|
-
// T05945: セッション名は cli_kind に応じて `<wt>` / `<wt>__codex` へ分ける。これで
|
|
3838
|
-
// 同一 worktree でも claude と codex を独立に起動/終了できる (旧実装は cli_kind に
|
|
3839
|
-
// 関わらず `<wt>` 固定だったため、片方しか立てられなかった)。
|
|
3840
2911
|
const wtSessionName = sessionNameFor(wtName, wtCliKind)
|
|
3841
|
-
await
|
|
3842
|
-
|
|
2912
|
+
await prepareChatWorkspace({
|
|
2913
|
+
cwd: wtPath,
|
|
2914
|
+
session: wtSessionName,
|
|
2915
|
+
cliKind: wtCliKind,
|
|
2916
|
+
logger: ctx.logger,
|
|
2917
|
+
})
|
|
3843
2918
|
ctx.client.send({
|
|
3844
2919
|
type: "worktree.create.result",
|
|
3845
2920
|
request_id: msg.request_id,
|
|
3846
2921
|
ok: true,
|
|
3847
2922
|
// wt_name は従来どおり **ベース名** (dir 名)。frontend のツリー構築キーに使う。
|
|
3848
2923
|
wt_name: wtName,
|
|
3849
|
-
// session_name
|
|
2924
|
+
// session_name は画面が選択するチャット行の名前 (codex なら __codex 付き)。
|
|
3850
2925
|
// 旧 frontend はこのフィールドを見ないため後方互換 (wt_name で選択する)。
|
|
3851
2926
|
session_name: wtSessionName,
|
|
3852
2927
|
wt_path: wtPath,
|
|
@@ -4076,12 +3151,9 @@ export async function dispatch(msg, ctx) {
|
|
|
4076
3151
|
})
|
|
4077
3152
|
const session = ctx.claudeBridge.sessions?.get(stream_id)
|
|
4078
3153
|
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
3154
|
if (session && resumeSessionId) {
|
|
4082
|
-
void loadResumeChatContext(session, "claude").
|
|
3155
|
+
void loadResumeChatContext(session, "claude").catch(() => {})
|
|
4083
3156
|
}
|
|
4084
|
-
void push().catch(() => {})
|
|
4085
3157
|
|
|
4086
3158
|
} catch (err) {
|
|
4087
3159
|
ctx.client.send({
|
|
@@ -4468,12 +3540,9 @@ export async function dispatch(msg, ctx) {
|
|
|
4468
3540
|
ctx.logger?.warn?.({ err: err?.message }, "failed to replay review events")
|
|
4469
3541
|
}
|
|
4470
3542
|
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
3543
|
if (session && info.resuming) {
|
|
4474
|
-
void loadResumeChatContext(session, "codex").
|
|
3544
|
+
void loadResumeChatContext(session, "codex").catch(() => {})
|
|
4475
3545
|
}
|
|
4476
|
-
void push().catch(() => {})
|
|
4477
3546
|
|
|
4478
3547
|
} catch (err) {
|
|
4479
3548
|
ctx.client.send({
|
|
@@ -4907,8 +3976,8 @@ export async function dispatch(msg, ctx) {
|
|
|
4907
3976
|
return
|
|
4908
3977
|
}
|
|
4909
3978
|
case "profile.switch": {
|
|
4910
|
-
// アカウント切替。active 永続化 → env 反映 (process
|
|
4911
|
-
// 強制再起動 (SDK チャット
|
|
3979
|
+
// アカウント切替。active 永続化 → env 反映 (process) → 全 Claude セッション
|
|
3980
|
+
// 強制再起動 (SDK チャット)。走行中ターンも中断する。
|
|
4912
3981
|
//
|
|
4913
3982
|
// WHY 認証スワップ廃止 (B案, 2026-07): Claude Code 2.1.x は OAuth トークンを
|
|
4914
3983
|
// `Claude Code-credentials-<sha256(CLAUDE_CONFIG_DIR)>` に config ディレクトリ別
|
|
@@ -4920,13 +3989,9 @@ export async function dispatch(msg, ctx) {
|
|
|
4920
3989
|
try {
|
|
4921
3990
|
const target = await setActiveProfile(id)
|
|
4922
3991
|
applyActiveProfileEnv(target, ctx.logger)
|
|
4923
|
-
await syncTmuxProfileEnv(target, ctx.logger)
|
|
4924
3992
|
// 全セッションを撤去 (frontend は exit 通知でセッション一覧をリロードする)。
|
|
4925
3993
|
const before = ctx.claudeBridge ? ctx.claudeBridge.list().length : 0
|
|
4926
3994
|
ctx.claudeBridge?.shutdown()
|
|
4927
|
-
// tmux TUI セッションも旧アカウントのまま残さない: kill → 同名・同 cwd で再作成。
|
|
4928
|
-
const tmuxResult = await restartAllTmuxSessionsForProfileSwitch(ctx)
|
|
4929
|
-
tmuxSessionsSnapshotCache.clear()
|
|
4930
3995
|
// アカウントが変わったので、旧アカウントの利用量・認証在否を TTL の残り時間だけ
|
|
4931
3996
|
// 見せ続けないよう明示的に捨てる (直後の needs_login 判定も実測値になる)。
|
|
4932
3997
|
usageResponseCache.clear()
|
|
@@ -4940,9 +4005,6 @@ export async function dispatch(msg, ctx) {
|
|
|
4940
4005
|
{
|
|
4941
4006
|
profile_id: target.id,
|
|
4942
4007
|
reaped: before,
|
|
4943
|
-
tmux_killed: tmuxResult.killed.length,
|
|
4944
|
-
tmux_recreated: tmuxResult.recreated.length,
|
|
4945
|
-
tmux_failed: tmuxResult.failed.length,
|
|
4946
4008
|
needs_login: needsLogin,
|
|
4947
4009
|
},
|
|
4948
4010
|
"claude profile switched; all sessions reaped",
|
|
@@ -4959,7 +4021,6 @@ export async function dispatch(msg, ctx) {
|
|
|
4959
4021
|
config_dir: target.configDir,
|
|
4960
4022
|
},
|
|
4961
4023
|
reaped: before,
|
|
4962
|
-
tmux_restarted: tmuxResult.recreated.length,
|
|
4963
4024
|
needs_login: needsLogin,
|
|
4964
4025
|
login_command: needsLogin
|
|
4965
4026
|
? isDefaultTarget
|
|
@@ -5083,7 +4144,7 @@ export async function dispatch(msg, ctx) {
|
|
|
5083
4144
|
return
|
|
5084
4145
|
}
|
|
5085
4146
|
case "codex.profile.switch": {
|
|
5086
|
-
// Codex アカウント切替。active 永続化 → CODEX_HOME を env
|
|
4147
|
+
// Codex アカウント切替。active 永続化 → CODEX_HOME を env に反映 → Codex
|
|
5087
4148
|
// app-server セッションのみ再起動 (Claude 側は独立なので触らない)。認証はファイル
|
|
5088
4149
|
// 分離のため CODEX_HOME 切替だけでアカウントが変わる (スワップ不要)。
|
|
5089
4150
|
const id = msg.profile_id || msg.id || ""
|
|
@@ -5091,11 +4152,9 @@ export async function dispatch(msg, ctx) {
|
|
|
5091
4152
|
const profileApi = ctx.codexProfiles || codexProfiles
|
|
5092
4153
|
const target = await profileApi.setActiveProfile(id)
|
|
5093
4154
|
;(ctx.applyActiveCodexEnv || applyActiveCodexEnv)(target, ctx.logger)
|
|
5094
|
-
await (ctx.syncTmuxCodexEnv || syncTmuxCodexEnv)(target, ctx.logger)
|
|
5095
4155
|
// Codex セッションだけ撤去 (Claude セッションは残す = 独立切替)。
|
|
5096
4156
|
const before = ctx.codexBridge?.sessions?.size ?? 0
|
|
5097
4157
|
ctx.codexBridge?.shutdown()
|
|
5098
|
-
tmuxSessionsSnapshotCache.clear()
|
|
5099
4158
|
// Codex アカウントが変わったので、旧アカウントの 5h/7d を TTL の残り時間だけ
|
|
5100
4159
|
// 見せ続けない (rate limits 自体は profileId 別キャッシュなので混ざらないが、
|
|
5101
4160
|
// usage.response の中身は両アカウントを束ねているため明示的に捨てる)。
|
|
@@ -5301,85 +4360,6 @@ export async function dispatch(msg, ctx) {
|
|
|
5301
4360
|
}
|
|
5302
4361
|
return
|
|
5303
4362
|
}
|
|
5304
|
-
case "tmux.list_sessions": {
|
|
5305
|
-
try {
|
|
5306
|
-
const snapshot = await tmuxSessionsSnapshotCache.get(async () => {
|
|
5307
|
-
// worktree index は listSessions の親判定と stub 列挙で共用する。従来は同じ
|
|
5308
|
-
// 全 workspace + git branch 走査を 1 応答内で 2 回直列実行していた。
|
|
5309
|
-
const worktreeIndex = await buildWorktreeIndex()
|
|
5310
|
-
// boundSessions: 各行の context% (ドーナツ) を bound session の jsonl から引く
|
|
5311
|
-
// (cwd dir 内の別セッション jsonl を最新と取り違える=メタ欠陥#1 の根治)。
|
|
5312
|
-
const sessions = await listTmuxSessions({
|
|
5313
|
-
plugins: ctx.plugins,
|
|
5314
|
-
logger: ctx.logger,
|
|
5315
|
-
boundSessions: ctx.tuiReboundSessions,
|
|
5316
|
-
claudeBridge: ctx.claudeBridge,
|
|
5317
|
-
worktreeIndex,
|
|
5318
|
-
})
|
|
5319
|
-
const lastEventByName = await readAllSessionEvents()
|
|
5320
|
-
const enriched = enrichTmuxSessionsForListResponse(
|
|
5321
|
-
sessions,
|
|
5322
|
-
lastEventByName,
|
|
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
|
-
},
|
|
5328
|
-
// warm = このセッションへ依頼 (cockpit_orch.py send) を送れるか。sibling-inject の
|
|
5329
|
-
// 宛先候補列挙と同一関数 (live-sessions.mjs) を通す。
|
|
5330
|
-
(row) =>
|
|
5331
|
-
isSessionWarm(
|
|
5332
|
-
{ claudeBridge: ctx.claudeBridge, codexBridge: ctx.codexBridge },
|
|
5333
|
-
row,
|
|
5334
|
-
),
|
|
5335
|
-
)
|
|
5336
|
-
const liveNames = new Set(enriched.map((s) => s.name))
|
|
5337
|
-
let worktreeStubs = []
|
|
5338
|
-
try {
|
|
5339
|
-
worktreeStubs = await listWorktreeStubs(liveNames, { worktreeIndex })
|
|
5340
|
-
} catch (err) {
|
|
5341
|
-
ctx.logger?.warn?.({ err: err?.message }, "listWorktreeStubs failed")
|
|
5342
|
-
}
|
|
5343
|
-
return { sessions: enriched, worktree_stubs: worktreeStubs }
|
|
5344
|
-
})
|
|
5345
|
-
// 行ごとの「次に開く会話」の context% (任意)。宛先 (context_targets) は要求ごとに
|
|
5346
|
-
// 違うため、共有される snapshot キャッシュの**外**で解決し、触る行だけ複製して
|
|
5347
|
-
// 載せる (他クライアントの応答を汚さない)。context_targets が無ければ従来どおりの
|
|
5348
|
-
// 応答 (後方互換)。
|
|
5349
|
-
let sessions = snapshot.sessions
|
|
5350
|
-
if (Array.isArray(msg.context_targets) && msg.context_targets.length > 0) {
|
|
5351
|
-
try {
|
|
5352
|
-
const pointerByName = await resolvePointerContexts(
|
|
5353
|
-
msg.context_targets,
|
|
5354
|
-
sessions,
|
|
5355
|
-
{ logger: ctx.logger },
|
|
5356
|
-
)
|
|
5357
|
-
sessions = applyPointerContexts(sessions, pointerByName)
|
|
5358
|
-
} catch (err) {
|
|
5359
|
-
// pointer context はあくまで付加情報。失敗してもセッション一覧は返す。
|
|
5360
|
-
ctx.logger?.warn?.(
|
|
5361
|
-
{ err: err?.message },
|
|
5362
|
-
"resolvePointerContexts failed",
|
|
5363
|
-
)
|
|
5364
|
-
}
|
|
5365
|
-
}
|
|
5366
|
-
ctx.client.send({
|
|
5367
|
-
type: "tmux.sessions",
|
|
5368
|
-
request_id: msg.request_id,
|
|
5369
|
-
...snapshot,
|
|
5370
|
-
sessions: sessions.map((row) => applyRowChatState(row, row.cwd, cliKindForSessionName(row.name))),
|
|
5371
|
-
})
|
|
5372
|
-
} catch (err) {
|
|
5373
|
-
ctx.client.send({
|
|
5374
|
-
type: "tmux.sessions",
|
|
5375
|
-
request_id: msg.request_id,
|
|
5376
|
-
sessions: [],
|
|
5377
|
-
worktree_stubs: [],
|
|
5378
|
-
error: err.message,
|
|
5379
|
-
})
|
|
5380
|
-
}
|
|
5381
|
-
return
|
|
5382
|
-
}
|
|
5383
4363
|
case "chat.sidebar_snapshot.request": {
|
|
5384
4364
|
try {
|
|
5385
4365
|
const messages = await readSidebarMessageSnapshot(msg.targets, {
|
|
@@ -5403,10 +4383,6 @@ export async function dispatch(msg, ctx) {
|
|
|
5403
4383
|
}
|
|
5404
4384
|
return
|
|
5405
4385
|
}
|
|
5406
|
-
case "tmux.create_session": {
|
|
5407
|
-
await handleTmuxCreateSession(msg, ctx)
|
|
5408
|
-
return
|
|
5409
|
-
}
|
|
5410
4386
|
case "worktree.create": {
|
|
5411
4387
|
await handleWorktreeCreate(msg, ctx)
|
|
5412
4388
|
return
|
|
@@ -5485,7 +4461,6 @@ export async function dispatch(msg, ctx) {
|
|
|
5485
4461
|
releaseRemoval?.()
|
|
5486
4462
|
}
|
|
5487
4463
|
if (result.dirty !== true) {
|
|
5488
|
-
tmuxSessionsSnapshotCache.clear()
|
|
5489
4464
|
chatTargetsCache.clear()
|
|
5490
4465
|
}
|
|
5491
4466
|
ctx.client.send({
|
|
@@ -5513,11 +4488,6 @@ export async function dispatch(msg, ctx) {
|
|
|
5513
4488
|
}
|
|
5514
4489
|
return
|
|
5515
4490
|
}
|
|
5516
|
-
case "tmux.kill_session": {
|
|
5517
|
-
await handleKillSession(msg, ctx)
|
|
5518
|
-
tmuxSessionsSnapshotCache.clear()
|
|
5519
|
-
return
|
|
5520
|
-
}
|
|
5521
4491
|
case "skills.request": {
|
|
5522
4492
|
try {
|
|
5523
4493
|
// Cockpit Codex 両対応 (Stream A3, 2026-07-02): browser が cli_kind を
|