@cocorograph/hub-agent 0.6.46 → 0.6.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/main.mjs +22 -6
- package/src/tmux.mjs +25 -45
- package/src/tui-permission-bridge.mjs +11 -2
package/package.json
CHANGED
package/src/main.mjs
CHANGED
|
@@ -452,6 +452,11 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
|
|
|
452
452
|
jsonlLiveWatchers.start()
|
|
453
453
|
ctx.jsonlLiveWatchers = jsonlLiveWatchers
|
|
454
454
|
|
|
455
|
+
// T04784 会話継続バインディングの冪等性ガード: tmux セッション名 → 最後に --resume で
|
|
456
|
+
// 載せ替えた Claude session_id。TUI ビューが remount のたびに claude.tui.bind を送っても、
|
|
457
|
+
// 同じ id への再 respawn (= claude 再起動) を抑止する。
|
|
458
|
+
ctx.tuiReboundSessions = new Map()
|
|
459
|
+
|
|
455
460
|
const shutdown = async (signal) => {
|
|
456
461
|
logger.info({ signal }, "shutting down")
|
|
457
462
|
await runHookBroadcast(plugins, "onAgentStop", ctx)
|
|
@@ -1023,6 +1028,7 @@ async function dispatch(msg, ctx) {
|
|
|
1023
1028
|
msg.request_id,
|
|
1024
1029
|
!!msg.allow,
|
|
1025
1030
|
msg.deny_message,
|
|
1031
|
+
msg.updated_input,
|
|
1026
1032
|
)
|
|
1027
1033
|
: false
|
|
1028
1034
|
if (handledByTui) return
|
|
@@ -1100,18 +1106,28 @@ async function dispatch(msg, ctx) {
|
|
|
1100
1106
|
})
|
|
1101
1107
|
}
|
|
1102
1108
|
// 3) tmux claude を --resume で載せ替え (targetId があるときだけ)。
|
|
1103
|
-
|
|
1109
|
+
// 冪等性: 同じ session を同じ id へ既に載せ替え済みなら respawn しない
|
|
1110
|
+
// (TUI ビューは remount のたびに bind を送るため、毎回 respawn すると
|
|
1111
|
+
// claude が再起動ループになり送信不能になる。2026-06-06 leader 実機)。
|
|
1112
|
+
let rebind = { ok: false, skipped: false }
|
|
1104
1113
|
if (targetId && sessionName) {
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1114
|
+
if (ctx.tuiReboundSessions.get(sessionName) === targetId) {
|
|
1115
|
+
rebind = { ok: true, skipped: true }
|
|
1116
|
+
} else {
|
|
1117
|
+
rebind = await rebindClaudeSession(sessionName, targetId, {
|
|
1118
|
+
cwd,
|
|
1119
|
+
model: ctx.config?.claude_model || "",
|
|
1120
|
+
permissionMode: ctx.config?.claude_permission_mode || "",
|
|
1121
|
+
logger,
|
|
1122
|
+
})
|
|
1123
|
+
if (rebind.ok) ctx.tuiReboundSessions.set(sessionName, targetId)
|
|
1124
|
+
}
|
|
1110
1125
|
}
|
|
1111
1126
|
reply({
|
|
1112
1127
|
session_id: targetId,
|
|
1113
1128
|
stopped_sdk: stoppedSdk,
|
|
1114
1129
|
rebound: !!rebind.ok,
|
|
1130
|
+
skipped: !!rebind.skipped,
|
|
1115
1131
|
error: rebind.ok ? undefined : rebind.error,
|
|
1116
1132
|
})
|
|
1117
1133
|
} catch (err) {
|
package/src/tmux.mjs
CHANGED
|
@@ -716,45 +716,29 @@ export function buildResumeCmd(sessionId, opts = {}) {
|
|
|
716
716
|
|
|
717
717
|
const _delay = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
718
718
|
|
|
719
|
-
/** pane_current_command が claude でなくなる (= シェルに戻った) のを待つ。 */
|
|
720
|
-
async function _waitPaneLeftClaude(name, opts = {}, timeoutMs = 4000) {
|
|
721
|
-
const bin = tmuxBin(opts)
|
|
722
|
-
const start = Date.now()
|
|
723
|
-
while (Date.now() - start < timeoutMs) {
|
|
724
|
-
try {
|
|
725
|
-
const { stdout } = await execFileP(bin, [
|
|
726
|
-
"display-message",
|
|
727
|
-
"-p",
|
|
728
|
-
"-t",
|
|
729
|
-
name,
|
|
730
|
-
"#{pane_current_command}",
|
|
731
|
-
])
|
|
732
|
-
const cmd = (stdout || "").trim().toLowerCase()
|
|
733
|
-
if (cmd && !cmd.includes("claude")) return true
|
|
734
|
-
} catch {
|
|
735
|
-
/* ignore */
|
|
736
|
-
}
|
|
737
|
-
await _delay(200)
|
|
738
|
-
}
|
|
739
|
-
return false
|
|
740
|
-
}
|
|
741
|
-
|
|
742
719
|
/**
|
|
743
720
|
* tmux セッションの対話 claude を、指定 session_id に `--resume` で載せ替える
|
|
744
721
|
* (T04784 TUI resume binding)。
|
|
745
722
|
*
|
|
746
|
-
*
|
|
747
|
-
*
|
|
748
|
-
*
|
|
749
|
-
*
|
|
750
|
-
*
|
|
723
|
+
* 旧実装 (Ctrl-C 連打で claude REPL を抜ける) は不安定だった: `claude --continue || claude`
|
|
724
|
+
* の `|| claude` フォールバックや生成中の状態で REPL を抜けきれず、resume コマンドが
|
|
725
|
+
* **動作中の claude の入力欄に文字列として打ち込まれて会話に混入**する事故が出た
|
|
726
|
+
* (2026-06-06 finance 実機 / leftClaude:false でも送ってしまっていた)。
|
|
727
|
+
*
|
|
728
|
+
* 新実装は `tmux respawn-pane -k` でペインのプロセスを**原子的に kill → 既定シェルへ置換**し、
|
|
729
|
+
* クリーンなシェルに `claude --resume <id> [flags]` を送る。文字列混入が起きず決定的。
|
|
730
|
+
* respawn はペイン自体を残すので browser の隠し端末 (tmux attach) は繋がったまま、
|
|
731
|
+
* かつ claude 終了後もシェルが残る (ペイン消滅でセッションが落ちるのを防ぐ)。
|
|
732
|
+
*
|
|
733
|
+
* ⚠️ 冪等性 (remount のたびに再起動しない) は呼び出し側 (main.mjs の
|
|
734
|
+
* tuiReboundSessions マップ) で担保する。本関数は呼ばれたら必ず respawn する。
|
|
751
735
|
*
|
|
752
736
|
* ベストエフォート。失敗しても throw せず {ok:false} を返し、上位は newest 表示に degrade する。
|
|
753
737
|
*
|
|
754
738
|
* @param {string} name tmux セッション名
|
|
755
739
|
* @param {string} sessionId 載せ替え先 Claude session_id
|
|
756
|
-
* @param {{model?:string,permissionMode?:string,logger?:object,tmuxBin?:string}} [opts]
|
|
757
|
-
* @returns {Promise<{ok:boolean,
|
|
740
|
+
* @param {{cwd?:string,model?:string,permissionMode?:string,logger?:object,tmuxBin?:string}} [opts]
|
|
741
|
+
* @returns {Promise<{ok:boolean, cmd?:string, error?:string}>}
|
|
758
742
|
*/
|
|
759
743
|
export async function rebindClaudeSession(name, sessionId, opts = {}) {
|
|
760
744
|
let cmd
|
|
@@ -764,25 +748,21 @@ export async function rebindClaudeSession(name, sessionId, opts = {}) {
|
|
|
764
748
|
return { ok: false, error: err?.message || String(err) }
|
|
765
749
|
}
|
|
766
750
|
const bin = tmuxBin(opts)
|
|
767
|
-
const send = (...keys) =>
|
|
768
|
-
execFileP(bin, ["send-keys", "-t", name, ...keys]).catch(() => {})
|
|
769
751
|
try {
|
|
770
|
-
// 1)
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
// 2)
|
|
776
|
-
|
|
777
|
-
// 3)
|
|
778
|
-
await send("C-u")
|
|
779
|
-
await _delay(80)
|
|
752
|
+
// 1) ペインを既定シェルへ原子置換 (-k で現 claude を強制 kill)。コマンド未指定なので
|
|
753
|
+
// クリーンなシェルが起動する。-c でセッションの cwd を維持。
|
|
754
|
+
const respArgs = ["respawn-pane", "-k", "-t", name]
|
|
755
|
+
if (opts.cwd) respArgs.push("-c", opts.cwd)
|
|
756
|
+
await execFileP(bin, respArgs)
|
|
757
|
+
// 2) シェル準備待ち (rc 読み込み等)。send-keys は pty にバッファされるので過敏でなくてよい。
|
|
758
|
+
await _delay(300)
|
|
759
|
+
// 3) クリーンなシェルに resume コマンドを送って claude を起動。
|
|
780
760
|
await execFileP(bin, ["send-keys", "-t", name, cmd, "Enter"])
|
|
781
761
|
opts.logger?.info(
|
|
782
|
-
{ session: name, resume: sessionId
|
|
783
|
-
"tui rebind:
|
|
762
|
+
{ session: name, resume: sessionId },
|
|
763
|
+
"tui rebind: respawned pane shell + claude --resume",
|
|
784
764
|
)
|
|
785
|
-
return { ok: true,
|
|
765
|
+
return { ok: true, cmd }
|
|
786
766
|
} catch (err) {
|
|
787
767
|
opts.logger?.warn(
|
|
788
768
|
{ session: name, resume: sessionId, err: err?.message },
|
|
@@ -114,9 +114,12 @@ export class TuiPermissionBridge extends EventEmitter {
|
|
|
114
114
|
* @param {string} request_id
|
|
115
115
|
* @param {boolean} allow
|
|
116
116
|
* @param {string} [denyMessage]
|
|
117
|
+
* @param {unknown} [updatedInput] AskUserQuestion 等の回答 ({questions, answers})。
|
|
118
|
+
* フックが PreToolUse の hookSpecificOutput.updatedInput として注入し、claude が
|
|
119
|
+
* 対話 UI を出さず確定する。Bash 等の allow/deny だけで済むツールでは undefined。
|
|
117
120
|
* @returns {Promise<boolean>} この bridge の管理下だったか
|
|
118
121
|
*/
|
|
119
|
-
async resolve(request_id, allow, denyMessage) {
|
|
122
|
+
async resolve(request_id, allow, denyMessage, updatedInput) {
|
|
120
123
|
if (!this._pending.has(request_id)) return false
|
|
121
124
|
this._pending.delete(request_id)
|
|
122
125
|
const decision = allow ? "allow" : "deny"
|
|
@@ -125,7 +128,13 @@ export class TuiPermissionBridge extends EventEmitter {
|
|
|
125
128
|
try {
|
|
126
129
|
await writeFile(
|
|
127
130
|
tmp,
|
|
128
|
-
JSON.stringify({
|
|
131
|
+
JSON.stringify({
|
|
132
|
+
decision,
|
|
133
|
+
deny_message: denyMessage ?? null,
|
|
134
|
+
// 回答 (AskUserQuestion の {questions, answers} 等)。フックが
|
|
135
|
+
// updatedInput として注入する。無ければ null (Bash 等)。
|
|
136
|
+
updated_input: updatedInput ?? null,
|
|
137
|
+
}),
|
|
129
138
|
)
|
|
130
139
|
await rename(tmp, fp)
|
|
131
140
|
} catch (err) {
|