@cocorograph/hub-agent 0.8.17 → 0.8.18
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/live-sessions.mjs +61 -0
- package/src/main.mjs +183 -133
- package/src/sibling-inject.mjs +21 -12
- package/src/tmux.mjs +60 -9
package/package.json
CHANGED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 「そのセッションに依頼を送れるか (warm)」判定の **正本 (SoT)** (2026-08-01)。
|
|
3
|
+
*
|
|
4
|
+
* ## 背景 (WHY)
|
|
5
|
+
* セッション間の依頼 (`cockpit_orch.py send`) は sibling-inject が宛先を解決するが、その
|
|
6
|
+
* 対象は **claudeBridge / codexBridge の `describeLiveSessions()` に載るセッションだけ**で
|
|
7
|
+
* ある (載らない宛先は `sendToStream` / `sendToCwd` が `not_found` を返す)。一方 Cockpit の
|
|
8
|
+
* サイドバーは「tmux のセッション行が存在するか」しか見ていないため、**依頼を送れない
|
|
9
|
+
* セッションが「起動中」と表示される**食い違いがあった。
|
|
10
|
+
*
|
|
11
|
+
* そこで `tmux.list_sessions` 応答に `warm` を載せて表示と配送の判定を揃えるが、その際に
|
|
12
|
+
* **同じ判定を別実装で書き直すと再び食い違う** (グレー表示なのに宛先一覧に出ない、等)。
|
|
13
|
+
* 判定はこのモジュール 1 箇所に集約し、sibling-inject の候補列挙 (op:list / op:list-all /
|
|
14
|
+
* 失敗時の診断) と `warm` の算出が**必ず同じ関数**を通るようにする。
|
|
15
|
+
*
|
|
16
|
+
* ## 不変条件
|
|
17
|
+
* - `isSessionWarm(bridges, row) === true`
|
|
18
|
+
* ⇔ `describeLiveSessionsUnion(bridges, {cwd: row.cwd, cliKind: 行の cli_kind}).length > 0`
|
|
19
|
+
* ⇔ sibling-inject の `op:"list"` が同じ cwd / kind で 1 本以上返す
|
|
20
|
+
* - 行の cli_kind は `session-names.mjs` の命名規約 (`<dir>` = claude /
|
|
21
|
+
* `<dir>__codex` = codex) からのみ導く。tmux / ブリッジの状態は参照しない。
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { cliKindForSessionName } from "./session-names.mjs"
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* claude / codex ブリッジの live セッションを union で返す (宛先解決の候補集合)。
|
|
28
|
+
*
|
|
29
|
+
* `cwd` を渡すとその cwd のみ、`cliKind` に 'claude' / 'codex' を渡すとその CLI のみへ
|
|
30
|
+
* 絞り込む。どちらも省略すると全 cwd・両 CLI (op:"list-all" 用)。`cliKind` が既知値以外
|
|
31
|
+
* なら絞り込まない (両方返す) — 呼び出し側で既に検証済みの前提。
|
|
32
|
+
*
|
|
33
|
+
* @param {{claudeBridge?: object, codexBridge?: object}} bridges
|
|
34
|
+
* @param {{cwd?: string, cliKind?: 'claude'|'codex'}} [filter]
|
|
35
|
+
* @returns {Array<object>} describeLiveSessions の記述子配列 (claude → codex の順)
|
|
36
|
+
*/
|
|
37
|
+
export function describeLiveSessionsUnion(bridges, { cwd, cliKind } = {}) {
|
|
38
|
+
const { claudeBridge, codexBridge } = bridges || {}
|
|
39
|
+
const out = []
|
|
40
|
+
if (cliKind !== "codex") out.push(...(claudeBridge?.describeLiveSessions?.(cwd) ?? []))
|
|
41
|
+
if (cliKind !== "claude") out.push(...(codexBridge?.describeLiveSessions?.(cwd) ?? []))
|
|
42
|
+
return out
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* tmux セッション行が「依頼を受け取れる (warm)」かを判定する。
|
|
47
|
+
*
|
|
48
|
+
* 判定は上記 union に一本化する。`cwd` が取れていない行 (tmux の cwd 解決失敗など) は
|
|
49
|
+
* 宛先解決のスコープを作れないため false (fail-closed。送れないものを送れると見せない)。
|
|
50
|
+
*
|
|
51
|
+
* @param {{claudeBridge?: object, codexBridge?: object}} bridges
|
|
52
|
+
* @param {{name?: string, cwd?: string|null}} row tmux.list_sessions の session 行
|
|
53
|
+
* @returns {boolean}
|
|
54
|
+
*/
|
|
55
|
+
export function isSessionWarm(bridges, { name, cwd } = {}) {
|
|
56
|
+
if (!cwd) return false
|
|
57
|
+
const cliKind = cliKindForSessionName(name)
|
|
58
|
+
return describeLiveSessionsUnion(bridges, { cwd, cliKind }).length > 0
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default { describeLiveSessionsUnion, isSessionWarm }
|
package/src/main.mjs
CHANGED
|
@@ -110,6 +110,7 @@ import {
|
|
|
110
110
|
stopTerminalMirror,
|
|
111
111
|
} from "./tmux.mjs"
|
|
112
112
|
import { SiblingInjectBridge } from "./sibling-inject.mjs"
|
|
113
|
+
import { isSessionWarm } from "./live-sessions.mjs"
|
|
113
114
|
import {
|
|
114
115
|
boundSessionId,
|
|
115
116
|
codexContextPercentFromTokenUsage,
|
|
@@ -199,14 +200,17 @@ function codexDefaultsFromAgentConfig(config) {
|
|
|
199
200
|
/**
|
|
200
201
|
* プロファイル切替用: 全 tmux セッションを kill → 同名・同 cwd で再作成する。
|
|
201
202
|
*
|
|
202
|
-
* 再作成された pane
|
|
203
|
-
*
|
|
203
|
+
* 再作成された pane は **既定では素のシェル** (createSession が CLI を起動しない)。
|
|
204
|
+
* アカウント切替は SDK チャット側の process.env と、tmux サーバーのグローバル環境
|
|
205
|
+
* (syncTmuxProfileEnv の `set-environment -g`) で追従する。
|
|
206
|
+
* `HUB_AGENT_TMUX_LAUNCH_CLI=1` で TUI を起動する運用の場合のみ、createSession 内の
|
|
207
|
+
* injectConfigDirEnv が新しい CLAUDE_CONFIG_DIR をコマンドへ焼き込む。
|
|
204
208
|
* initial prompt (leader の /orchestrate 等) までは復元しない — アカウントが
|
|
205
209
|
* 変わる以上、前アカウントの文脈でスキルを自動発火させない方が安全。
|
|
206
210
|
* cwd が取れないセッションは kill のみ行い、再作成はスキップして failed に積む。
|
|
207
211
|
* tmux サーバー未起動なら何もしない。
|
|
208
212
|
*/
|
|
209
|
-
async function restartAllTmuxSessionsForProfileSwitch(ctx) {
|
|
213
|
+
export async function restartAllTmuxSessionsForProfileSwitch(ctx) {
|
|
210
214
|
const result = { killed: [], recreated: [], failed: [] }
|
|
211
215
|
let sessions = []
|
|
212
216
|
try {
|
|
@@ -410,9 +414,11 @@ export function applyActiveProfileEnv(profile, log) {
|
|
|
410
414
|
|
|
411
415
|
/**
|
|
412
416
|
* active プロファイルの CLAUDE_CONFIG_DIR を tmux サーバーのグローバル環境にも同期する。
|
|
413
|
-
* tmux pane のシェルは hub-agent の process.env
|
|
414
|
-
*
|
|
415
|
-
*
|
|
417
|
+
* tmux pane のシェルは hub-agent の process.env を見ないため、set-environment -g で流し込む。
|
|
418
|
+
* pane は既定で素のシェル (CLI を自動起動しない) なので、ユーザーが pane 内で直接
|
|
419
|
+
* `claude` を打つケースにはこれが主経路になる
|
|
420
|
+
* (`HUB_AGENT_TMUX_LAUNCH_CLI=1` で CLI を pane 起動するときだけ、createSession 内の
|
|
421
|
+
* injectConfigDirEnv によるコマンド焼き込みが併用される)。
|
|
416
422
|
* tmux サーバー未起動でも失敗しない。
|
|
417
423
|
*/
|
|
418
424
|
export async function syncTmuxProfileEnv(profile, log) {
|
|
@@ -2013,17 +2019,27 @@ const STALL_WARN_MS = Number(process.env.HUB_AGENT_STALL_WARN_MS ?? 90000)
|
|
|
2013
2019
|
* 純関数として export することでテスト容易性を上げる(getSessionStateSnapshot を mock で
|
|
2014
2020
|
* 注入し、snapshot 有/無/null/欠落フィールドの各ケースで挙動を検証可能)。
|
|
2015
2021
|
*
|
|
2022
|
+
* `warm` (2026-08-01): 「このセッションに依頼 (cockpit_orch.py send) を送れるか」。frontend は
|
|
2023
|
+
* 従来「tmux のセッション行が存在するか」だけで起動中を判定していたため、**依頼を送れない
|
|
2024
|
+
* セッションが起動中と表示される**食い違いがあった。判定は live-sessions.mjs の
|
|
2025
|
+
* `isSessionWarm` (sibling-inject の宛先候補列挙と同一関数) に一本化する。accessor 未指定
|
|
2026
|
+
* (=旧呼び出し) では `warm` を付与しない(推測で false を配らない)。
|
|
2027
|
+
*
|
|
2016
2028
|
* @param {Array} sessions tmux.list_sessions が返した素の session 行
|
|
2017
2029
|
* @param {Map<string, object>} lastEventByName session_name → 最新 hook event
|
|
2018
2030
|
* @param {((name: string) => object | null) | undefined} getSnap state loop の getSnapshot
|
|
2019
2031
|
* accessor。undefined のときは 4 信号を一切付与しない(snapshot 経路 OFF と等価)
|
|
2032
|
+
* @param {((row: {name: string, cwd?: string|null}) => boolean) | undefined} isWarm
|
|
2033
|
+
* warm 判定 accessor (既定は live-sessions.mjs の isSessionWarm を bind したもの)。
|
|
2034
|
+
* undefined のときは `warm` を付与しない
|
|
2020
2035
|
*/
|
|
2021
|
-
export function enrichTmuxSessionsForListResponse(sessions, lastEventByName, getSnap) {
|
|
2036
|
+
export function enrichTmuxSessionsForListResponse(sessions, lastEventByName, getSnap, isWarm) {
|
|
2022
2037
|
return sessions.map((s) => {
|
|
2023
2038
|
const base = {
|
|
2024
2039
|
...s,
|
|
2025
2040
|
last_event: lastEventByName.get(s.name) || null,
|
|
2026
2041
|
}
|
|
2042
|
+
if (isWarm) base.warm = !!isWarm(s)
|
|
2027
2043
|
if (!getSnap) return base
|
|
2028
2044
|
const snap = getSnap(s.name)
|
|
2029
2045
|
if (!snap) return base
|
|
@@ -3542,6 +3558,157 @@ export async function handleUsageRequest(msg, ctx, deps = {}) {
|
|
|
3542
3558
|
}
|
|
3543
3559
|
}
|
|
3544
3560
|
|
|
3561
|
+
/**
|
|
3562
|
+
* `tmux.create_session` ハンドラ。指定名・cwd で detached の tmux セッションを作る。
|
|
3563
|
+
*
|
|
3564
|
+
* ⚠️ pane に CLI (claude / codex) を起動するかは **createSession 内の
|
|
3565
|
+
* shouldLaunchPaneCli() が一元判定**する (既定は起動しない)。ここで渡す cliKind /
|
|
3566
|
+
* claudeCmd / codexCmd / initialPrompt 等は、`HUB_AGENT_TMUX_LAUNCH_CLI=1` で
|
|
3567
|
+
* CLI を起動するときにだけ使われる。呼び出し元ごとに既定を変えないため、
|
|
3568
|
+
* このハンドラ側で起動可否を判断しないこと。
|
|
3569
|
+
*
|
|
3570
|
+
* dispatch から切り出して export しているのは、実際に tmux へ渡る引数を
|
|
3571
|
+
* テストから検査できるようにするため (test/tmux-pane-cli-launch.test.mjs)。
|
|
3572
|
+
*/
|
|
3573
|
+
export async function handleTmuxCreateSession(msg, ctx) {
|
|
3574
|
+
const name = (msg.session_name || "").trim()
|
|
3575
|
+
const cwd = (msg.cwd || process.env.HOME || "/").trim()
|
|
3576
|
+
if (!name) {
|
|
3577
|
+
ctx.client.send({
|
|
3578
|
+
type: "tmux.create_session.result",
|
|
3579
|
+
request_id: msg.request_id,
|
|
3580
|
+
ok: false,
|
|
3581
|
+
error: "session_name is required",
|
|
3582
|
+
})
|
|
3583
|
+
return
|
|
3584
|
+
}
|
|
3585
|
+
try {
|
|
3586
|
+
await createTmuxSession(name, cwd, {
|
|
3587
|
+
// cli_kind: 'codex' なら codex を、それ以外は claude を pane 起動する (既定 claude)。
|
|
3588
|
+
cliKind: msg.cli_kind === "codex" ? "codex" : "claude",
|
|
3589
|
+
claudeCmd:
|
|
3590
|
+
typeof msg.claude_cmd === "string"
|
|
3591
|
+
? msg.claude_cmd
|
|
3592
|
+
: claudeCmdFromAgentConfig(ctx.config),
|
|
3593
|
+
codexCmd: typeof msg.codex_cmd === "string" ? msg.codex_cmd : undefined,
|
|
3594
|
+
// codex 用設定 (claude 経路では claudeCmd を直接使うため無視される)。
|
|
3595
|
+
model:
|
|
3596
|
+
typeof msg.model === "string"
|
|
3597
|
+
? msg.model
|
|
3598
|
+
: codexDefaultsFromAgentConfig(ctx.config).model,
|
|
3599
|
+
sandbox: typeof msg.sandbox === "string" ? msg.sandbox : undefined,
|
|
3600
|
+
approvalPolicy:
|
|
3601
|
+
typeof msg.approval_policy === "string" ? msg.approval_policy : undefined,
|
|
3602
|
+
effort:
|
|
3603
|
+
typeof msg.effort === "string"
|
|
3604
|
+
? msg.effort
|
|
3605
|
+
: codexDefaultsFromAgentConfig(ctx.config).effort,
|
|
3606
|
+
initialPrompt:
|
|
3607
|
+
typeof msg.initial_prompt === "string" ? msg.initial_prompt : undefined,
|
|
3608
|
+
logger: ctx.logger,
|
|
3609
|
+
})
|
|
3610
|
+
tmuxSessionsSnapshotCache.clear()
|
|
3611
|
+
ctx.client.send({
|
|
3612
|
+
type: "tmux.create_session.result",
|
|
3613
|
+
request_id: msg.request_id,
|
|
3614
|
+
ok: true,
|
|
3615
|
+
session_name: name,
|
|
3616
|
+
})
|
|
3617
|
+
} catch (err) {
|
|
3618
|
+
ctx.client.send({
|
|
3619
|
+
type: "tmux.create_session.result",
|
|
3620
|
+
request_id: msg.request_id,
|
|
3621
|
+
ok: false,
|
|
3622
|
+
error: err.message,
|
|
3623
|
+
})
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
|
|
3627
|
+
/**
|
|
3628
|
+
* `worktree.create` ハンドラ。git worktree を作ってから、その dir で tmux セッションを作る。
|
|
3629
|
+
* body: { request_id, dir, branch, cli_kind?, claude_cmd?, codex_cmd?, model?, sandbox?,
|
|
3630
|
+
* approval_policy?, effort?, initial_prompt? }
|
|
3631
|
+
*
|
|
3632
|
+
* ⚠️ pane の CLI 起動可否は handleTmuxCreateSession と同じく createSession 側の
|
|
3633
|
+
* 一元判定に従う (既定は起動しない)。
|
|
3634
|
+
*/
|
|
3635
|
+
export async function handleWorktreeCreate(msg, ctx) {
|
|
3636
|
+
const dir = (msg.dir || "").trim()
|
|
3637
|
+
const branch = (msg.branch || "").trim()
|
|
3638
|
+
if (!dir || !branch) {
|
|
3639
|
+
ctx.client.send({
|
|
3640
|
+
type: "worktree.create.result",
|
|
3641
|
+
request_id: msg.request_id,
|
|
3642
|
+
ok: false,
|
|
3643
|
+
error: "dir and branch required",
|
|
3644
|
+
})
|
|
3645
|
+
return
|
|
3646
|
+
}
|
|
3647
|
+
if (!/^[A-Za-z0-9._\-/]+$/.test(branch) || branch.startsWith("-")) {
|
|
3648
|
+
ctx.client.send({
|
|
3649
|
+
type: "worktree.create.result",
|
|
3650
|
+
request_id: msg.request_id,
|
|
3651
|
+
ok: false,
|
|
3652
|
+
error: "invalid branch name",
|
|
3653
|
+
})
|
|
3654
|
+
return
|
|
3655
|
+
}
|
|
3656
|
+
try {
|
|
3657
|
+
const { wtName, wtPath, createdBranch } = await createWorktreeDir(dir, branch)
|
|
3658
|
+
// worktree dir で tmux session を作成。tmux.create_session と同じく cli_kind を
|
|
3659
|
+
// 解釈し、codex なら codex を pane 起動する(既定 claude)。
|
|
3660
|
+
// T05945: セッション名は cli_kind に応じて `<wt>` / `<wt>__codex` へ分ける。これで
|
|
3661
|
+
// 同一 worktree でも claude と codex を独立に起動/終了できる (旧実装は cli_kind に
|
|
3662
|
+
// 関わらず `<wt>` 固定だったため、片方しか立てられなかった)。
|
|
3663
|
+
const wtCliKind = msg.cli_kind === "codex" ? "codex" : "claude"
|
|
3664
|
+
const wtSessionName = sessionNameFor(wtName, wtCliKind)
|
|
3665
|
+
await createTmuxSession(wtSessionName, wtPath, {
|
|
3666
|
+
cliKind: wtCliKind,
|
|
3667
|
+
claudeCmd:
|
|
3668
|
+
typeof msg.claude_cmd === "string"
|
|
3669
|
+
? msg.claude_cmd
|
|
3670
|
+
: claudeCmdFromAgentConfig(ctx.config),
|
|
3671
|
+
codexCmd: typeof msg.codex_cmd === "string" ? msg.codex_cmd : undefined,
|
|
3672
|
+
// codex 用設定 (claude 経路では無視される)。
|
|
3673
|
+
model:
|
|
3674
|
+
typeof msg.model === "string"
|
|
3675
|
+
? msg.model
|
|
3676
|
+
: codexDefaultsFromAgentConfig(ctx.config).model,
|
|
3677
|
+
sandbox: typeof msg.sandbox === "string" ? msg.sandbox : undefined,
|
|
3678
|
+
approvalPolicy:
|
|
3679
|
+
typeof msg.approval_policy === "string" ? msg.approval_policy : undefined,
|
|
3680
|
+
effort:
|
|
3681
|
+
typeof msg.effort === "string"
|
|
3682
|
+
? msg.effort
|
|
3683
|
+
: codexDefaultsFromAgentConfig(ctx.config).effort,
|
|
3684
|
+
initialPrompt:
|
|
3685
|
+
typeof msg.initial_prompt === "string" ? msg.initial_prompt : undefined,
|
|
3686
|
+
logger: ctx.logger,
|
|
3687
|
+
})
|
|
3688
|
+
tmuxSessionsSnapshotCache.clear()
|
|
3689
|
+
ctx.client.send({
|
|
3690
|
+
type: "worktree.create.result",
|
|
3691
|
+
request_id: msg.request_id,
|
|
3692
|
+
ok: true,
|
|
3693
|
+
// wt_name は従来どおり **ベース名** (dir 名)。frontend のツリー構築キーに使う。
|
|
3694
|
+
wt_name: wtName,
|
|
3695
|
+
// session_name は実際に作った tmux セッション名 (codex なら __codex 付き)。
|
|
3696
|
+
// 旧 frontend はこのフィールドを見ないため後方互換 (wt_name で選択する)。
|
|
3697
|
+
session_name: wtSessionName,
|
|
3698
|
+
wt_path: wtPath,
|
|
3699
|
+
branch,
|
|
3700
|
+
created_branch: createdBranch,
|
|
3701
|
+
})
|
|
3702
|
+
} catch (err) {
|
|
3703
|
+
ctx.client.send({
|
|
3704
|
+
type: "worktree.create.result",
|
|
3705
|
+
request_id: msg.request_id,
|
|
3706
|
+
ok: false,
|
|
3707
|
+
error: err.message,
|
|
3708
|
+
})
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
|
|
3545
3712
|
async function dispatch(msg, ctx) {
|
|
3546
3713
|
const t = msg?.type || ""
|
|
3547
3714
|
try {
|
|
@@ -4889,6 +5056,13 @@ async function dispatch(msg, ctx) {
|
|
|
4889
5056
|
sessions,
|
|
4890
5057
|
lastEventByName,
|
|
4891
5058
|
ctx.getSessionStateSnapshot,
|
|
5059
|
+
// warm = このセッションへ依頼 (cockpit_orch.py send) を送れるか。sibling-inject の
|
|
5060
|
+
// 宛先候補列挙と同一関数 (live-sessions.mjs) を通す。
|
|
5061
|
+
(row) =>
|
|
5062
|
+
isSessionWarm(
|
|
5063
|
+
{ claudeBridge: ctx.claudeBridge, codexBridge: ctx.codexBridge },
|
|
5064
|
+
row,
|
|
5065
|
+
),
|
|
4892
5066
|
)
|
|
4893
5067
|
const liveNames = new Set(enriched.map((s) => s.name))
|
|
4894
5068
|
let worktreeStubs = []
|
|
@@ -4939,135 +5113,11 @@ async function dispatch(msg, ctx) {
|
|
|
4939
5113
|
return
|
|
4940
5114
|
}
|
|
4941
5115
|
case "tmux.create_session": {
|
|
4942
|
-
|
|
4943
|
-
const cwd = (msg.cwd || process.env.HOME || "/").trim()
|
|
4944
|
-
if (!name) {
|
|
4945
|
-
ctx.client.send({
|
|
4946
|
-
type: "tmux.create_session.result",
|
|
4947
|
-
request_id: msg.request_id,
|
|
4948
|
-
ok: false,
|
|
4949
|
-
error: "session_name is required",
|
|
4950
|
-
})
|
|
4951
|
-
return
|
|
4952
|
-
}
|
|
4953
|
-
try {
|
|
4954
|
-
await createTmuxSession(name, cwd, {
|
|
4955
|
-
// cli_kind: 'codex' なら codex を、それ以外は claude を pane 起動する (既定 claude)。
|
|
4956
|
-
cliKind: msg.cli_kind === "codex" ? "codex" : "claude",
|
|
4957
|
-
claudeCmd:
|
|
4958
|
-
typeof msg.claude_cmd === "string"
|
|
4959
|
-
? msg.claude_cmd
|
|
4960
|
-
: claudeCmdFromAgentConfig(ctx.config),
|
|
4961
|
-
codexCmd: typeof msg.codex_cmd === "string" ? msg.codex_cmd : undefined,
|
|
4962
|
-
// codex 用設定 (claude 経路では claudeCmd を直接使うため無視される)。
|
|
4963
|
-
model:
|
|
4964
|
-
typeof msg.model === "string"
|
|
4965
|
-
? msg.model
|
|
4966
|
-
: codexDefaultsFromAgentConfig(ctx.config).model,
|
|
4967
|
-
sandbox: typeof msg.sandbox === "string" ? msg.sandbox : undefined,
|
|
4968
|
-
approvalPolicy:
|
|
4969
|
-
typeof msg.approval_policy === "string" ? msg.approval_policy : undefined,
|
|
4970
|
-
effort:
|
|
4971
|
-
typeof msg.effort === "string"
|
|
4972
|
-
? msg.effort
|
|
4973
|
-
: codexDefaultsFromAgentConfig(ctx.config).effort,
|
|
4974
|
-
initialPrompt:
|
|
4975
|
-
typeof msg.initial_prompt === "string" ? msg.initial_prompt : undefined,
|
|
4976
|
-
logger: ctx.logger,
|
|
4977
|
-
})
|
|
4978
|
-
tmuxSessionsSnapshotCache.clear()
|
|
4979
|
-
ctx.client.send({
|
|
4980
|
-
type: "tmux.create_session.result",
|
|
4981
|
-
request_id: msg.request_id,
|
|
4982
|
-
ok: true,
|
|
4983
|
-
session_name: name,
|
|
4984
|
-
})
|
|
4985
|
-
} catch (err) {
|
|
4986
|
-
ctx.client.send({
|
|
4987
|
-
type: "tmux.create_session.result",
|
|
4988
|
-
request_id: msg.request_id,
|
|
4989
|
-
ok: false,
|
|
4990
|
-
error: err.message,
|
|
4991
|
-
})
|
|
4992
|
-
}
|
|
5116
|
+
await handleTmuxCreateSession(msg, ctx)
|
|
4993
5117
|
return
|
|
4994
5118
|
}
|
|
4995
5119
|
case "worktree.create": {
|
|
4996
|
-
|
|
4997
|
-
const dir = (msg.dir || "").trim()
|
|
4998
|
-
const branch = (msg.branch || "").trim()
|
|
4999
|
-
if (!dir || !branch) {
|
|
5000
|
-
ctx.client.send({
|
|
5001
|
-
type: "worktree.create.result",
|
|
5002
|
-
request_id: msg.request_id,
|
|
5003
|
-
ok: false,
|
|
5004
|
-
error: "dir and branch required",
|
|
5005
|
-
})
|
|
5006
|
-
return
|
|
5007
|
-
}
|
|
5008
|
-
if (!/^[A-Za-z0-9._\-/]+$/.test(branch) || branch.startsWith("-")) {
|
|
5009
|
-
ctx.client.send({
|
|
5010
|
-
type: "worktree.create.result",
|
|
5011
|
-
request_id: msg.request_id,
|
|
5012
|
-
ok: false,
|
|
5013
|
-
error: "invalid branch name",
|
|
5014
|
-
})
|
|
5015
|
-
return
|
|
5016
|
-
}
|
|
5017
|
-
try {
|
|
5018
|
-
const { wtName, wtPath, createdBranch } = await createWorktreeDir(dir, branch)
|
|
5019
|
-
// worktree dir で tmux session を作成。tmux.create_session と同じく cli_kind を
|
|
5020
|
-
// 解釈し、codex なら codex を pane 起動する(既定 claude)。
|
|
5021
|
-
// T05945: セッション名は cli_kind に応じて `<wt>` / `<wt>__codex` へ分ける。これで
|
|
5022
|
-
// 同一 worktree でも claude と codex を独立に起動/終了できる (旧実装は cli_kind に
|
|
5023
|
-
// 関わらず `<wt>` 固定だったため、片方しか立てられなかった)。
|
|
5024
|
-
const wtCliKind = msg.cli_kind === "codex" ? "codex" : "claude"
|
|
5025
|
-
const wtSessionName = sessionNameFor(wtName, wtCliKind)
|
|
5026
|
-
await createTmuxSession(wtSessionName, wtPath, {
|
|
5027
|
-
cliKind: wtCliKind,
|
|
5028
|
-
claudeCmd:
|
|
5029
|
-
typeof msg.claude_cmd === "string"
|
|
5030
|
-
? msg.claude_cmd
|
|
5031
|
-
: claudeCmdFromAgentConfig(ctx.config),
|
|
5032
|
-
codexCmd: typeof msg.codex_cmd === "string" ? msg.codex_cmd : undefined,
|
|
5033
|
-
// codex 用設定 (claude 経路では無視される)。
|
|
5034
|
-
model:
|
|
5035
|
-
typeof msg.model === "string"
|
|
5036
|
-
? msg.model
|
|
5037
|
-
: codexDefaultsFromAgentConfig(ctx.config).model,
|
|
5038
|
-
sandbox: typeof msg.sandbox === "string" ? msg.sandbox : undefined,
|
|
5039
|
-
approvalPolicy:
|
|
5040
|
-
typeof msg.approval_policy === "string" ? msg.approval_policy : undefined,
|
|
5041
|
-
effort:
|
|
5042
|
-
typeof msg.effort === "string"
|
|
5043
|
-
? msg.effort
|
|
5044
|
-
: codexDefaultsFromAgentConfig(ctx.config).effort,
|
|
5045
|
-
initialPrompt:
|
|
5046
|
-
typeof msg.initial_prompt === "string" ? msg.initial_prompt : undefined,
|
|
5047
|
-
logger: ctx.logger,
|
|
5048
|
-
})
|
|
5049
|
-
tmuxSessionsSnapshotCache.clear()
|
|
5050
|
-
ctx.client.send({
|
|
5051
|
-
type: "worktree.create.result",
|
|
5052
|
-
request_id: msg.request_id,
|
|
5053
|
-
ok: true,
|
|
5054
|
-
// wt_name は従来どおり **ベース名** (dir 名)。frontend のツリー構築キーに使う。
|
|
5055
|
-
wt_name: wtName,
|
|
5056
|
-
// session_name は実際に作った tmux セッション名 (codex なら __codex 付き)。
|
|
5057
|
-
// 旧 frontend はこのフィールドを見ないため後方互換 (wt_name で選択する)。
|
|
5058
|
-
session_name: wtSessionName,
|
|
5059
|
-
wt_path: wtPath,
|
|
5060
|
-
branch,
|
|
5061
|
-
created_branch: createdBranch,
|
|
5062
|
-
})
|
|
5063
|
-
} catch (err) {
|
|
5064
|
-
ctx.client.send({
|
|
5065
|
-
type: "worktree.create.result",
|
|
5066
|
-
request_id: msg.request_id,
|
|
5067
|
-
ok: false,
|
|
5068
|
-
error: err.message,
|
|
5069
|
-
})
|
|
5070
|
-
}
|
|
5120
|
+
await handleWorktreeCreate(msg, ctx)
|
|
5071
5121
|
return
|
|
5072
5122
|
}
|
|
5073
5123
|
case "worktree.list_history": {
|
package/src/sibling-inject.mjs
CHANGED
|
@@ -41,6 +41,7 @@ import { mkdir, readFile, writeFile, rename, unlink, readdir } from "node:fs/pro
|
|
|
41
41
|
import path from "node:path"
|
|
42
42
|
|
|
43
43
|
import { verifySiblingTokenAny, siblingTokenCwd } from "./sibling-token.mjs"
|
|
44
|
+
import { describeLiveSessionsUnion } from "./live-sessions.mjs"
|
|
44
45
|
|
|
45
46
|
export const SIBLING_REQUESTS_DIR =
|
|
46
47
|
process.env.COCKPIT_SIBLING_REQUESTS_DIR || "/tmp/cockpit_sibling_requests"
|
|
@@ -69,6 +70,14 @@ export class SiblingInjectBridge {
|
|
|
69
70
|
this._watcher = null
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
/**
|
|
74
|
+
* live-sessions.mjs (warm 判定の正本) へ渡すブリッジ束。宛先候補の列挙は必ずここを
|
|
75
|
+
* 経由し、`tmux.list_sessions` の `warm` と同じ判定を共有する。
|
|
76
|
+
*/
|
|
77
|
+
_bridges() {
|
|
78
|
+
return { claudeBridge: this.claudeBridge, codexBridge: this.codexBridge }
|
|
79
|
+
}
|
|
80
|
+
|
|
72
81
|
/** 監視ディレクトリを作成し、既存要求を sweep してから fs.watch を張る。 */
|
|
73
82
|
async start() {
|
|
74
83
|
await mkdir(this.dir, { recursive: true })
|
|
@@ -159,13 +168,8 @@ export class SiblingInjectBridge {
|
|
|
159
168
|
this.logger?.warn({ request_id }, "sibling-inject: invalid sibling token (list-all)")
|
|
160
169
|
result = { ok: false, error: "invalid_token" }
|
|
161
170
|
} else {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
live_sessions: [
|
|
165
|
-
...(this.claudeBridge?.describeLiveSessions?.() ?? []),
|
|
166
|
-
...(this.codexBridge?.describeLiveSessions?.() ?? []),
|
|
167
|
-
],
|
|
168
|
-
}
|
|
171
|
+
// 判定は live-sessions.mjs に一本化する (tmux.list_sessions の warm と同じ関数)。
|
|
172
|
+
result = { ok: true, live_sessions: describeLiveSessionsUnion(this._bridges()) }
|
|
169
173
|
}
|
|
170
174
|
} else if (!cwd || badKind || badBody) {
|
|
171
175
|
result = { ok: false, error: "invalid_request" }
|
|
@@ -192,7 +196,15 @@ export class SiblingInjectBridge {
|
|
|
192
196
|
const bridge = target_cli_kind === "codex" ? this.codexBridge : this.claudeBridge
|
|
193
197
|
if (isList) {
|
|
194
198
|
// 宛先候補の列挙 (cockpit_orch.py --list 用)。この cwd の同種セッションを返す。
|
|
195
|
-
|
|
199
|
+
// ⚠️ tmux.list_sessions の `warm` と同じ関数を通す (二重定義にすると「グレー表示
|
|
200
|
+
// なのに宛先一覧に出ない」食い違いが再発する)。
|
|
201
|
+
result = {
|
|
202
|
+
ok: true,
|
|
203
|
+
live_sessions: describeLiveSessionsUnion(this._bridges(), {
|
|
204
|
+
cwd,
|
|
205
|
+
cliKind: target_cli_kind,
|
|
206
|
+
}),
|
|
207
|
+
}
|
|
196
208
|
} else if (isExplicit) {
|
|
197
209
|
// 明示宛先: session_id (claude) / stream_id|thread_id (codex) で cwd 一致下に確定解決。
|
|
198
210
|
// sendToStream は {status:"delivered"|"not_found"|"ambiguous"} を返す。
|
|
@@ -361,10 +373,7 @@ export class SiblingInjectBridge {
|
|
|
361
373
|
* @param {string} targetKind
|
|
362
374
|
*/
|
|
363
375
|
_diagnose(cwd, targetKind) {
|
|
364
|
-
const sameCwd =
|
|
365
|
-
...(this.claudeBridge?.describeLiveSessions?.(cwd) ?? []),
|
|
366
|
-
...(this.codexBridge?.describeLiveSessions?.(cwd) ?? []),
|
|
367
|
-
]
|
|
376
|
+
const sameCwd = describeLiveSessionsUnion(this._bridges(), { cwd })
|
|
368
377
|
const hint = sameCwd.length
|
|
369
378
|
? `この cwd には ${sameCwd.map((s) => s.kind).join("/")} のチャットセッションはあるが ${targetKind} は無い。`
|
|
370
379
|
: `この cwd には hub-agent 管理下のチャットセッションが 1 本も無い。` +
|
package/src/tmux.mjs
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* - `listSessions()`: 構造化された TmuxSession[] を返す。各 session に
|
|
6
6
|
* status (state.mjs の detectSessionState) と cwd (active pane の current_path)
|
|
7
7
|
* を並列で付与
|
|
8
|
-
* - `createSession(name, cwd, opts)`: detached new-session
|
|
9
|
-
*
|
|
8
|
+
* - `createSession(name, cwd, opts)`: detached new-session。既定では pane に CLI
|
|
9
|
+
* (claude / codex) を起動せず素のシェルのまま置く。`HUB_AGENT_TMUX_LAUNCH_CLI=1`
|
|
10
|
+
* のときだけ従来どおり CLAUDE_CMD / CODEX_CMD を send-keys する。既存ならエラー throw
|
|
10
11
|
* - `killSession(name)`: 1 session を kill。存在しない場合は 'session not found' throw
|
|
11
12
|
* - `killManySessions(names)`: 複数 kill、存在しないものは無視
|
|
12
13
|
*
|
|
@@ -509,6 +510,26 @@ export function composeClaudeCmdFromOptions(opts = {}) {
|
|
|
509
510
|
|
|
510
511
|
const DEFAULT_CLAUDE_CMD = buildClaudeCmd()
|
|
511
512
|
|
|
513
|
+
/**
|
|
514
|
+
* tmux pane で CLI (claude / codex) の TUI を起動してよいかを判定する。
|
|
515
|
+
*
|
|
516
|
+
* **既定は false**(pane は素のシェルのまま)。Cockpit のターミナルモードは
|
|
517
|
+
* 2026-07-03 に封印済みで、通常運用は常にチャットモード (Agent SDK / codex
|
|
518
|
+
* app-server) になった。それでも createSession は pane で TUI を起動し続けていたため、
|
|
519
|
+
* **誰も見ていない CLI が常駐**する。実測 (2026-08-01 中村さん端末): tmux セッション
|
|
520
|
+
* 33 個に対して pane の `claude` プロセス 31 個・合計 RSS 1.7 GB。
|
|
521
|
+
*
|
|
522
|
+
* `HUB_AGENT_TMUX_LAUNCH_CLI=1` のときだけ従来どおり起動する(デバッグ・TUI を
|
|
523
|
+
* 意図的に使いたいときの逃げ道)。呼び出し元ごとに挙動を変えないため、判定は
|
|
524
|
+
* この 1 箇所 (createSession 内) だけで行い、opts では上書きさせない。
|
|
525
|
+
*
|
|
526
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
527
|
+
* @returns {boolean}
|
|
528
|
+
*/
|
|
529
|
+
export function shouldLaunchPaneCli(env = process.env) {
|
|
530
|
+
return env?.HUB_AGENT_TMUX_LAUNCH_CLI === "1"
|
|
531
|
+
}
|
|
532
|
+
|
|
512
533
|
function tmuxBin(opts = {}) {
|
|
513
534
|
return opts.tmuxBin || DEFAULT_TMUX_BIN
|
|
514
535
|
}
|
|
@@ -546,6 +567,10 @@ export function injectInitialPrompt(claudeCmd, initialPrompt) {
|
|
|
546
567
|
* しか効かないため、injectInitialPrompt と同様に `||` で split して両側に付ける。
|
|
547
568
|
*
|
|
548
569
|
* configDir が空 (= 既定プロファイル) のときは何もしない。
|
|
570
|
+
*
|
|
571
|
+
* ⚠️ この関数は **CLI を pane 起動するときだけ** 使われる
|
|
572
|
+
* (`HUB_AGENT_TMUX_LAUNCH_CLI=1`)。素のシェルの pane へは setTmuxGlobalEnv の
|
|
573
|
+
* `set-environment -g` 経由で CLAUDE_CONFIG_DIR が渡る。
|
|
549
574
|
*/
|
|
550
575
|
export function injectConfigDirEnv(claudeCmd, configDir) {
|
|
551
576
|
if (!configDir || typeof configDir !== "string") return claudeCmd
|
|
@@ -559,8 +584,10 @@ export function injectConfigDirEnv(claudeCmd, configDir) {
|
|
|
559
584
|
|
|
560
585
|
/**
|
|
561
586
|
* tmux サーバーのグローバル環境変数を設定/解除する。
|
|
562
|
-
*
|
|
563
|
-
*
|
|
587
|
+
* pane は既定で素のシェルなので、ユーザーが pane 内で手動起動する claude / codex に
|
|
588
|
+
* CLAUDE_CONFIG_DIR (CODEX_HOME) を届ける **主経路** がこれ。
|
|
589
|
+
* (CLI を pane 起動する `HUB_AGENT_TMUX_LAUNCH_CLI=1` のときだけ、
|
|
590
|
+
* injectConfigDirEnv のコマンド焼き込みが併用される)。
|
|
564
591
|
* value が null/undefined なら unset (-gu)。tmux サーバー未起動などの失敗は握り潰す。
|
|
565
592
|
*/
|
|
566
593
|
export async function setTmuxGlobalEnv(name, value, opts = {}) {
|
|
@@ -766,12 +793,21 @@ export async function killManySessions(names, opts = {}) {
|
|
|
766
793
|
}
|
|
767
794
|
|
|
768
795
|
/**
|
|
769
|
-
* 新規 session を detached
|
|
796
|
+
* 新規 session を detached で作成する。
|
|
797
|
+
*
|
|
798
|
+
* ⚠️ **既定では pane に CLI (claude / codex) を起動しない**(pane は素のシェルのまま)。
|
|
799
|
+
* Cockpit の通常運用はチャットモードで、pane の TUI は誰も見ないまま常駐して
|
|
800
|
+
* メモリを食うだけだったため (詳細は shouldLaunchPaneCli の JSDoc)。
|
|
801
|
+
* `HUB_AGENT_TMUX_LAUNCH_CLI=1` のときだけ従来どおり CLI を send-keys する。
|
|
802
|
+
*
|
|
770
803
|
* - 同名 session が既にあれば 'duplicate session' throw
|
|
771
|
-
* -
|
|
772
|
-
*
|
|
773
|
-
*
|
|
774
|
-
*
|
|
804
|
+
* - CLAUDE.md / AGENTS.md の生成・Hub 同期、cwd 自動作成、mouse mode 設定は
|
|
805
|
+
* **CLI を起動するかどうかに関わらず**常に行う(素のシェルでも必要なため)
|
|
806
|
+
* - 以下は CLI を起動するときだけ意味を持つので、起動時のみ評価される:
|
|
807
|
+
* opts.claudeCmd / opts.codexCmd / opts.cliKind / opts.claudeConfigDir /
|
|
808
|
+
* opts.codexHome / opts.initialPrompt / model・sandbox 等の codex フラグ
|
|
809
|
+
* - opts.initialPrompt は CLI 起動時に位置引数として末尾へ shell-safe に埋め込まれ、
|
|
810
|
+
* TUI の初回プロンプトとして実行される (`/orchestrate` 等のスキル発火に利用)
|
|
775
811
|
*/
|
|
776
812
|
export async function createSession(name, cwd, opts = {}) {
|
|
777
813
|
// tmux は `-c` をシェル展開しないため、ここで `~` を絶対パスに展開する。
|
|
@@ -918,6 +954,21 @@ export async function createSession(name, cwd, opts = {}) {
|
|
|
918
954
|
// display-message 失敗は致命的でないので飲み込む
|
|
919
955
|
}
|
|
920
956
|
}
|
|
957
|
+
// 既定では pane を素のシェルのまま残す (CLI は起動しない)。ここから下は
|
|
958
|
+
// HUB_AGENT_TMUX_LAUNCH_CLI=1 のときだけ走る。
|
|
959
|
+
//
|
|
960
|
+
// 素のシェルでも必要な処理 (cwd 解決・CLAUDE.md 生成/同期・mouse mode) は上で
|
|
961
|
+
// 済ませてある。CLAUDE_CONFIG_DIR / CODEX_HOME も、pane で手動起動したときに
|
|
962
|
+
// 効くよう main.mjs の syncTmuxProfileEnv / syncTmuxCodexEnv が
|
|
963
|
+
// `tmux set-environment -g` でサーバーのグローバル環境へ流し込んでいるため、
|
|
964
|
+
// ここでコマンド文字列に焼き込む必要があるのは CLI を起動する場合だけ。
|
|
965
|
+
if (!shouldLaunchPaneCli()) {
|
|
966
|
+
opts.logger?.debug?.(
|
|
967
|
+
{ session: name, cwd: resolvedCwd },
|
|
968
|
+
"pane CLI launch skipped (set HUB_AGENT_TMUX_LAUNCH_CLI=1 to enable)",
|
|
969
|
+
)
|
|
970
|
+
return
|
|
971
|
+
}
|
|
921
972
|
// cliKind に応じて claude / codex いずれの CLI を pane で起動するか分岐する。
|
|
922
973
|
// 既定は claude(後方互換)。initialPrompt はどちらも位置引数として末尾注入する
|
|
923
974
|
// (claude "<p>" / codex "<p>" / codex resume <id> "<p>" いずれも [PROMPT] を受ける)。
|