tmuxclihook 0.5.8 → 0.5.10
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/adapters/claude.sh +68 -0
- package/scripts/install-claude-hooks.sh +25 -2
- package/scripts/install-codex-hooks.sh +14 -1
- package/scripts/install-opencode-hooks.sh +14 -1
- package/scripts/lib-install-hooks.sh +74 -3
- package/scripts/lib-tmux-ai-log.sh +5 -2
- package/scripts/lib-tmux-ai-status.sh +92 -29
- package/scripts/tmux-ai-status +40 -26
package/package.json
CHANGED
|
@@ -255,6 +255,51 @@ adapter_check_integrity() {
|
|
|
255
255
|
return 0
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
+
# 已收敛判定(仅供 wrapper 的 auto 分支使用——⚠️ 不得被事件路径复用)。
|
|
259
|
+
# 四条件全过 = auto 可安全跳过重装:integrity 过 + 全部条目指向当前源(无漂移/
|
|
260
|
+
# 死条目)+ 每事件本插件条目 ≤1(无重复)。
|
|
261
|
+
# 参照系:wrapper 的 _LIB_DIR 是 pwd -P 物理路径;写入(install/auto/修复全部经
|
|
262
|
+
# wrapper)与比较同参照系自洽。事件路径的 _LIB_DIR 是 pwd 逻辑路径(软链场景
|
|
263
|
+
# 两者不等),事件路径复用会误判漂移。
|
|
264
|
+
# 取源逻辑与 adapter_check_integrity 的三模式 case 保持同步(不抽共享以控制
|
|
265
|
+
# integrity 的回归面)。
|
|
266
|
+
adapter_check_converged() {
|
|
267
|
+
adapter_check_integrity || return 1
|
|
268
|
+
local mode src="" tmp="" stats
|
|
269
|
+
mode=$(_ccswitch_mode)
|
|
270
|
+
case "$mode" in
|
|
271
|
+
live)
|
|
272
|
+
src=$(_ccswitch_live_file)
|
|
273
|
+
[ -f "$src" ] || return 1
|
|
274
|
+
;;
|
|
275
|
+
legacy)
|
|
276
|
+
_ccswitch_dump_common tmp || return 1
|
|
277
|
+
src="$tmp"
|
|
278
|
+
;;
|
|
279
|
+
*)
|
|
280
|
+
[ -f "$ADAPTER_HOOKS_FILE" ] || return 1
|
|
281
|
+
src="$ADAPTER_HOOKS_FILE"
|
|
282
|
+
;;
|
|
283
|
+
esac
|
|
284
|
+
# 一次 jq 同时产出漂移计数(≠ 当前源,含死路径)与重复计数(每事件条目 >1)
|
|
285
|
+
stats=$(jq -r --arg cur "${_LIB_DIR}/tmux-ai-status" '
|
|
286
|
+
.hooks as $h |
|
|
287
|
+
["SessionStart","SessionEnd","UserPromptSubmit","PreToolUse","PostToolUse",
|
|
288
|
+
"PostToolUseFailure","PermissionRequest","Notification","Stop","StopFailure"] as $events |
|
|
289
|
+
[$events[] as $e |
|
|
290
|
+
[$h[$e][]?.hooks[]?.command // empty
|
|
291
|
+
| select(contains("tmux-ai-status")) | split(" ")[0]] as $scripts |
|
|
292
|
+
select(($scripts | length) > 0) |
|
|
293
|
+
{bad: ([$scripts[] | select(. != $cur)] | length),
|
|
294
|
+
n: ($scripts | length)}]
|
|
295
|
+
| "bad=\([.[].bad] | add // 0) dup=\([.[].n | select(. > 1)] | length)"' "$src" 2>/dev/null)
|
|
296
|
+
[ -z "$tmp" ] || rm -f "$tmp"
|
|
297
|
+
case "$stats" in
|
|
298
|
+
"bad=0 dup=0") return 0 ;;
|
|
299
|
+
*) return 1 ;;
|
|
300
|
+
esac
|
|
301
|
+
}
|
|
302
|
+
|
|
258
303
|
# 明细检查(仅 install wrapper 的 check --json 调用;事件热路径不触达)。
|
|
259
304
|
# stdout 输出单行紧凑 JSON 片段 {"mode":...,"events":{...},"plugin":null,"registeredPaths":[...]},
|
|
260
305
|
# 由 lib-install-hooks.sh 的 _wrapper_check_json 拼装 tool/status/suppress/drift。
|
|
@@ -342,6 +387,16 @@ adapter_install_hooks() {
|
|
|
342
387
|
_ccswitch_warn_gui
|
|
343
388
|
_ccswitch_dump_common tmp || return 1
|
|
344
389
|
merged=$(_claude_apply_merge "$tmp")
|
|
390
|
+
# 语义不变跳写:merge 只动 .hooks,顶层其他字段天然继承 tmp——
|
|
391
|
+
# 两边 jq -S 规范化对比,相同则跳过 upsert(GUI 冲掉 hooks 时
|
|
392
|
+
# 库内现值 ≠ merged,恒不跳,对抗不弱化;行缺失时 tmp={},
|
|
393
|
+
# merged 恒含 10 事件,同样恒不跳)
|
|
394
|
+
if [ "$(jq -S . "$tmp" 2>/dev/null)" = "$(jq -S . <<<"$merged" 2>/dev/null)" ]; then
|
|
395
|
+
rm -f "$tmp"
|
|
396
|
+
echo "Claude hooks already up to date in cc-switch common config (skip write)"
|
|
397
|
+
echo "Events: $_CLAUDE_EVENTS"
|
|
398
|
+
return 0
|
|
399
|
+
fi
|
|
345
400
|
rm -f "$tmp"
|
|
346
401
|
hooks_patch=$(jq -c '{hooks: (.hooks // {})}' <<<"$merged")
|
|
347
402
|
full_doc=$(jq -c . <<<"$merged")
|
|
@@ -360,6 +415,14 @@ adapter_install_hooks() {
|
|
|
360
415
|
esac
|
|
361
416
|
[ -f "$target" ] || _install_atomic_write '{}' "$target"
|
|
362
417
|
updated=$(_claude_apply_merge "$target")
|
|
418
|
+
# 语义不变跳写:jq 输出格式与手写文件不同,字节 cmp 不可用——两边 jq -S
|
|
419
|
+
# 规范化后比较(命令替换对称剥尾换行)。跳写不触碰 mtime,避免触发 Claude
|
|
420
|
+
# Code file watcher 的无谓 hooks 重载。
|
|
421
|
+
if [ "$(jq -S . "$target" 2>/dev/null)" = "$(jq -S . <<<"$updated" 2>/dev/null)" ]; then
|
|
422
|
+
echo "Claude hooks already up to date at $target (skip write)"
|
|
423
|
+
echo "Events: $_CLAUDE_EVENTS"
|
|
424
|
+
return 0
|
|
425
|
+
fi
|
|
363
426
|
_install_atomic_write "$updated" "$target"
|
|
364
427
|
echo "Claude hooks installed to $target"
|
|
365
428
|
echo "Events: $_CLAUDE_EVENTS"
|
|
@@ -412,6 +475,11 @@ adapter_uninstall_hooks() {
|
|
|
412
475
|
esac
|
|
413
476
|
[ -f "$target" ] || { echo "Claude hooks: nothing to uninstall"; return 0; }
|
|
414
477
|
updated=$(_claude_apply_strip "$target")
|
|
478
|
+
# 无本插件条目时 strip 无变化 → 跳写(不触碰 mtime)
|
|
479
|
+
if [ "$(jq -S . "$target" 2>/dev/null)" = "$(jq -S . <<<"$updated" 2>/dev/null)" ]; then
|
|
480
|
+
echo "Claude hooks: nothing to uninstall from $target (skip write)"
|
|
481
|
+
return 0
|
|
482
|
+
fi
|
|
415
483
|
_install_atomic_write "$updated" "$target"
|
|
416
484
|
echo "Claude hooks uninstalled from $target"
|
|
417
485
|
}
|
|
@@ -18,6 +18,21 @@ _LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
|
|
18
18
|
source "${_LIB_DIR}/lib-install-hooks.sh"
|
|
19
19
|
source "${_LIB_DIR}/adapters/claude.sh"
|
|
20
20
|
|
|
21
|
+
# 写路径互斥(check 只读不锁):临界区覆盖 adapter 调用 + suppress 标记读-改-写,
|
|
22
|
+
# 串行化 auto 与显式 uninstall 的标记 TOCTOU。失败语义见 _install_acquire_lock:
|
|
23
|
+
# 降级无锁(rc=2)继续;等待超时(rc=1)退出。
|
|
24
|
+
case "${1:-install}" in
|
|
25
|
+
check) ;;
|
|
26
|
+
*)
|
|
27
|
+
_lock_rc=0
|
|
28
|
+
_install_acquire_lock claude || _lock_rc=$?
|
|
29
|
+
if [ "$_lock_rc" = 1 ]; then exit 1; fi
|
|
30
|
+
if [ "$_lock_rc" = 0 ]; then
|
|
31
|
+
trap '_install_release_lock claude' EXIT
|
|
32
|
+
fi
|
|
33
|
+
;;
|
|
34
|
+
esac
|
|
35
|
+
|
|
21
36
|
case "${1:-install}" in
|
|
22
37
|
uninstall)
|
|
23
38
|
adapter_uninstall_hooks
|
|
@@ -37,11 +52,19 @@ case "${1:-install}" in
|
|
|
37
52
|
if [ -f "$(_hooks_suppress_file claude)" ]; then
|
|
38
53
|
exit 0
|
|
39
54
|
fi
|
|
55
|
+
# 已收敛(integrity + 无漂移/死条目/重复)→ 跳过重装:tmux 每次 reload
|
|
56
|
+
# 不再全量重写真源。强收敛保留——漂移/死条目/缺失/重复任一存在都走
|
|
57
|
+
# install 收敛。结构漂移(async/matcher 被外部改)不在判定内,auto 跳过
|
|
58
|
+
# 不修复,显式 install 仍强收敛。
|
|
59
|
+
if adapter_check_converged; then
|
|
60
|
+
echo "claude hooks already converged (skip install)"
|
|
61
|
+
exit 0
|
|
62
|
+
fi
|
|
40
63
|
adapter_install_hooks
|
|
41
|
-
_hooks_suppress_clear claude
|
|
64
|
+
_hooks_suppress_clear claude || true
|
|
42
65
|
;;
|
|
43
66
|
*)
|
|
44
67
|
adapter_install_hooks
|
|
45
|
-
_hooks_suppress_clear claude
|
|
68
|
+
_hooks_suppress_clear claude || true
|
|
46
69
|
;;
|
|
47
70
|
esac
|
|
@@ -15,6 +15,19 @@ _LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
|
|
15
15
|
source "${_LIB_DIR}/lib-install-hooks.sh"
|
|
16
16
|
source "${_LIB_DIR}/adapters/codex.sh"
|
|
17
17
|
|
|
18
|
+
# 写路径互斥(check 只读不锁):见 _install_acquire_lock 头注释。
|
|
19
|
+
case "${1:-install}" in
|
|
20
|
+
check) ;;
|
|
21
|
+
*)
|
|
22
|
+
_lock_rc=0
|
|
23
|
+
_install_acquire_lock codex || _lock_rc=$?
|
|
24
|
+
if [ "$_lock_rc" = 1 ]; then exit 1; fi
|
|
25
|
+
if [ "$_lock_rc" = 0 ]; then
|
|
26
|
+
trap '_install_release_lock codex' EXIT
|
|
27
|
+
fi
|
|
28
|
+
;;
|
|
29
|
+
esac
|
|
30
|
+
|
|
18
31
|
case "${1:-install}" in
|
|
19
32
|
uninstall)
|
|
20
33
|
adapter_uninstall_hooks
|
|
@@ -32,6 +45,6 @@ case "${1:-install}" in
|
|
|
32
45
|
;;
|
|
33
46
|
*)
|
|
34
47
|
adapter_install_hooks
|
|
35
|
-
_hooks_suppress_clear codex
|
|
48
|
+
_hooks_suppress_clear codex || true
|
|
36
49
|
;;
|
|
37
50
|
esac
|
|
@@ -15,6 +15,19 @@ _LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
|
|
15
15
|
source "${_LIB_DIR}/lib-install-hooks.sh"
|
|
16
16
|
source "${_LIB_DIR}/adapters/opencode.sh"
|
|
17
17
|
|
|
18
|
+
# 写路径互斥(check 只读不锁):见 _install_acquire_lock 头注释。
|
|
19
|
+
case "${1:-install}" in
|
|
20
|
+
check) ;;
|
|
21
|
+
*)
|
|
22
|
+
_lock_rc=0
|
|
23
|
+
_install_acquire_lock opencode || _lock_rc=$?
|
|
24
|
+
if [ "$_lock_rc" = 1 ]; then exit 1; fi
|
|
25
|
+
if [ "$_lock_rc" = 0 ]; then
|
|
26
|
+
trap '_install_release_lock opencode' EXIT
|
|
27
|
+
fi
|
|
28
|
+
;;
|
|
29
|
+
esac
|
|
30
|
+
|
|
18
31
|
case "${1:-install}" in
|
|
19
32
|
uninstall)
|
|
20
33
|
adapter_uninstall_hooks
|
|
@@ -32,6 +45,6 @@ case "${1:-install}" in
|
|
|
32
45
|
;;
|
|
33
46
|
*)
|
|
34
47
|
adapter_install_hooks
|
|
35
|
-
_hooks_suppress_clear opencode
|
|
48
|
+
_hooks_suppress_clear opencode || true
|
|
36
49
|
;;
|
|
37
50
|
esac
|
|
@@ -29,14 +29,85 @@ _install_atomic_write() {
|
|
|
29
29
|
# 显式 uninstall 成功 → 写 suppressed-<tool>;显式 install 成功 → 清。
|
|
30
30
|
# 自动修复路径(_maybe_repair_hooks / 入口 auto / SessionStart 重装)见标记一律不装。
|
|
31
31
|
# ⚠️ _SUPPRESS_DIR 与 lib-tmux-ai-status.sh 中的定义保持一致(两处同步改)。
|
|
32
|
+
#
|
|
33
|
+
# 失败语义(两个方向都告警,不对称):
|
|
34
|
+
# - write 失败:hooks 已剥离但标记没落 → auto 会静默装回(用户显式卸载被反转)
|
|
35
|
+
# → return 1,wrapper uninstall 分支 errexit 使其以非 0 退出,用户可见。
|
|
36
|
+
# - clear 失败:install 已成功但残留标记会把 auto 永久挡住(比 write 失败更隐蔽)
|
|
37
|
+
# → 告警但 return 0(主操作已成功,调用方 wrapper 的 clear 调用需 || true 容忍)。
|
|
32
38
|
_SUPPRESS_DIR="${TMUX_AI_SUPPRESS_DIR:-${XDG_CONFIG_HOME:-${HOME}/.config}/tmuxclihook}"
|
|
33
39
|
|
|
34
40
|
_hooks_suppress_file() { printf '%s/suppressed-%s' "$_SUPPRESS_DIR" "$1"; }
|
|
35
41
|
_hooks_suppress_write() {
|
|
36
|
-
mkdir -p "$_SUPPRESS_DIR" 2>/dev/null
|
|
37
|
-
|
|
42
|
+
if ! mkdir -p "$_SUPPRESS_DIR" 2>/dev/null \
|
|
43
|
+
|| ! date +%s > "$(_hooks_suppress_file "$1")" 2>/dev/null; then
|
|
44
|
+
echo "WARN: 抑制标记写入失败——hooks 已卸载,但 tmux 重载/新会话可能自动装回;" \
|
|
45
|
+
"请检查 $_SUPPRESS_DIR 可写性" >&2
|
|
46
|
+
return 1
|
|
47
|
+
fi
|
|
48
|
+
}
|
|
49
|
+
_hooks_suppress_clear() {
|
|
50
|
+
if [ -e "$(_hooks_suppress_file "$1")" ] \
|
|
51
|
+
&& ! rm -f "$(_hooks_suppress_file "$1")" 2>/dev/null; then
|
|
52
|
+
echo "WARN: 卸载意图标记清除失败——tmux 自动安装将持续跳过;" \
|
|
53
|
+
"请检查 $_SUPPRESS_DIR 可写性" >&2
|
|
54
|
+
return 1
|
|
55
|
+
fi
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# --- 安装/卸载读-改-写互斥锁 ---
|
|
59
|
+
# 写路径的并发方:TUI 显式装 / tmux 入口 auto / SessionStart 自修复 / _maybe_repair_hooks /
|
|
60
|
+
# drift 收敛——五方都经 wrapper dispatch,锁在此层一处覆盖三 adapter。
|
|
61
|
+
# mkdir mutex(项目风格同 .lock/.event-lock;macOS 无 flock(1))。两种失败语义区分:
|
|
62
|
+
# - 锁目录建不出来(权限/ENOTDIR)→ 降级无锁继续 + 告警(历史行为是无锁写;install
|
|
63
|
+
# 真正写目标与锁目录不同权限域,锁失败不代表写不了)
|
|
64
|
+
# - 他人持锁 → 等待(0.2s × 50 ≈ 10s)→ stale(10s)强夺 → 超时报错 return 1(显式操作可见)。
|
|
65
|
+
# ⚠️ codex SessionStart 是同步 hook(timeout:5s):极端并发下锁等待可能超过 5s 使该次
|
|
66
|
+
# hook 超时——后果轻(该次 hook 不执行、codex 继续、无阻塞),概率极低(repair 60s 一次)。
|
|
67
|
+
# 边界:锁只收敛「我们自己的多方」;Claude Code 自身写 settings.json 不遵守本锁(外部
|
|
68
|
+
# 写者无法防)——这正是 60s 自修复存在的原因。写路径互斥由 wrapper 层负责,adapter
|
|
69
|
+
# 函数不含锁——直接 source adapter 调 install 会绕过锁。
|
|
70
|
+
_install_lock_dir() { printf '%s/.install-lock-%s' "$_SUPPRESS_DIR" "$1"; }
|
|
71
|
+
_install_acquire_lock() { # $1=tool id → 0=持锁 1=超时(2=降级无锁,调用方继续)
|
|
72
|
+
local lock; lock=$(_install_lock_dir "$1")
|
|
73
|
+
if ! mkdir -p "$_SUPPRESS_DIR" 2>/dev/null; then
|
|
74
|
+
echo "WARN: 锁目录不可创建($_SUPPRESS_DIR),降级无锁继续安装" >&2
|
|
75
|
+
return 2
|
|
76
|
+
fi
|
|
77
|
+
local _start=$SECONDS i
|
|
78
|
+
for i in {1..50}; do
|
|
79
|
+
if mkdir "$lock" 2>/dev/null; then
|
|
80
|
+
echo "$$" > "$lock/owner"
|
|
81
|
+
return 0
|
|
82
|
+
fi
|
|
83
|
+
# stale 强夺:持锁者进程已死 → 立即夺;锁 mtime 超 10s → 强夺。
|
|
84
|
+
# 外部命令(stat/sleep/rmdir)逐一防御退出码——ccswitch 用例 8 的 stubbin
|
|
85
|
+
# 是最小 PATH 沙箱(无 stat/rmdir/sleep),127 会在 errexit 下炸掉 install。
|
|
86
|
+
local owner mtime
|
|
87
|
+
owner=$(cat "$lock/owner" 2>/dev/null)
|
|
88
|
+
if [ -n "$owner" ] && ! kill -0 "$owner" 2>/dev/null; then
|
|
89
|
+
rm -f "$lock/owner" 2>/dev/null || true
|
|
90
|
+
rmdir "$lock" 2>/dev/null || true
|
|
91
|
+
continue
|
|
92
|
+
fi
|
|
93
|
+
mtime=""
|
|
94
|
+
mtime=$(stat -c %Y "$lock" 2>/dev/null || stat -f %m "$lock" 2>/dev/null || true)
|
|
95
|
+
if [ -n "$mtime" ] && [ $(( _start + SECONDS - mtime )) -ge 10 ]; then
|
|
96
|
+
echo "WARN: 强夺超时的安装锁(owner=${owner:-?})" >&2
|
|
97
|
+
rm -f "$lock/owner" 2>/dev/null || true
|
|
98
|
+
rmdir "$lock" 2>/dev/null || true
|
|
99
|
+
continue
|
|
100
|
+
fi
|
|
101
|
+
sleep 0.2 2>/dev/null || :
|
|
102
|
+
done
|
|
103
|
+
echo "ERROR: 安装锁获取超时($lock)——另一安装操作可能仍在进行,稍后重试" >&2
|
|
104
|
+
return 1
|
|
105
|
+
}
|
|
106
|
+
_install_release_lock() { # $1=tool id
|
|
107
|
+
local lock; lock=$(_install_lock_dir "$1")
|
|
108
|
+
rm -f "$lock/owner" 2>/dev/null || true
|
|
109
|
+
rmdir "$lock" 2>/dev/null || true
|
|
38
110
|
}
|
|
39
|
-
_hooks_suppress_clear() { rm -f "$(_hooks_suppress_file "$1")" 2>/dev/null || true; }
|
|
40
111
|
|
|
41
112
|
# --- check --json:完整性明细 JSON(wrapper 专用,事件热路径不触达)---
|
|
42
113
|
# stdout = 单行紧凑 JSON;exit code 与默认 check 一致(ok→0 / missing→1)。
|
|
@@ -69,9 +69,12 @@ _ai_log_event() {
|
|
|
69
69
|
[ "${fsize:-0}" -gt "$AI_LOG_MAX_SIZE" ] && _ai_log_rotate
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
# input
|
|
72
|
+
# input 超长截断:默认 >2000B 保留前 2000B 加 "…+Nbytes";$2 可传自定义上限。
|
|
73
|
+
# PermissionRequest 的 full 日志路径传 16384(保留调试价值同时限制体积)。
|
|
74
|
+
# 注意:即便 16384 也远超 PIPE_BUF(macOS 512 / Linux 4096),并发 hook 的
|
|
75
|
+
# 大 input 日志行可能交错撕裂——接受(日志观测用途,非结构化数据)。
|
|
73
76
|
_ai_log_truncate_input() {
|
|
74
|
-
local s="$1" max=2000
|
|
77
|
+
local s="$1" max="${2:-2000}"
|
|
75
78
|
local n=${#s}
|
|
76
79
|
if [ "$n" -gt "$max" ]; then
|
|
77
80
|
printf '%s…+%dbytes' "${s:0:$max}" "$((n - max))"
|
|
@@ -113,26 +113,51 @@ _snapshot_has_pane() { # $1=pane_id
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
# 从当前进程沿父链向上匹配 pane_pid;命中即返回该 pane(权威信号)。
|
|
116
|
+
# $1 可选进程快照(_ps_process_snapshot 输出):调用方(resolve_tmux_pane)
|
|
117
|
+
# 传入以复用同一份快照(ancestry 与进程树候选共用,每事件一次 ps);空参自取。
|
|
118
|
+
# 一次 awk 同时建 ppid 映射与 pane_pid→pane_id 表,从 $$ 内存爬链——
|
|
119
|
+
# depth<24 / pid>1 语义与旧逐层 `ps -o ppid=` 版一致;快照是瞬时一致视图,
|
|
120
|
+
# 还消除了逐层实时 ps 的爬链中途进程死亡问题。
|
|
116
121
|
_pane_from_ancestry() {
|
|
117
|
-
local
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
122
|
+
local snapshot="${1:-}"
|
|
123
|
+
[ -n "$snapshot" ] || snapshot=$(_ps_process_snapshot)
|
|
124
|
+
[ -n "$snapshot" ] || return 1
|
|
125
|
+
# ps 快照与 pane 快照均为变量内容(非文件),各经 process substitution 进 awk:
|
|
126
|
+
# 第一个文件(NR==FNR)建 ppid 映射,第二个建 pane_pid→pane_id 表,END 爬链。
|
|
127
|
+
# ⚠️ split 必须用单空格 " "(awk 特殊语义:忽略前导/尾部空白、折叠连续分隔),
|
|
128
|
+
# 不能用 /[ \t]+/——macOS ps 的 pid 列宽随快照最大 pid 位数浮动,4 位 pid 行
|
|
129
|
+
# 带前导空格,正则 split 的首字段会是空串(pid 全部错位/丢弃),爬链在低位
|
|
130
|
+
# pid 处断链——表现为偶发 PANE: unresolved(实测踩坑,2026-10-20)。
|
|
131
|
+
awk -F'|' -v self="$$" '
|
|
132
|
+
NR==FNR {
|
|
133
|
+
split($0, a, " ")
|
|
134
|
+
if (a[1] != "") ppid[a[1]] = a[2]
|
|
135
|
+
next
|
|
136
|
+
}
|
|
137
|
+
$4 != "" { pane_by_pid[$4] = $1 }
|
|
138
|
+
END {
|
|
139
|
+
pid = self
|
|
140
|
+
depth = 0
|
|
141
|
+
while (pid + 0 > 1 && depth < 24) {
|
|
142
|
+
if (pid in pane_by_pid) { print pane_by_pid[pid]; exit 0 }
|
|
143
|
+
if (!(pid in ppid)) exit 1
|
|
144
|
+
pid = ppid[pid] + 0
|
|
145
|
+
depth++
|
|
146
|
+
}
|
|
147
|
+
exit 1
|
|
148
|
+
}' <(printf '%s\n' "$snapshot") <(printf '%s\n' "${_PANE_SNAPSHOT:-}")
|
|
125
149
|
}
|
|
126
150
|
|
|
127
151
|
# 当前工具在 attached pane 中的候选集合(进程树含 ADAPTER_PROCESS_NAMES)。
|
|
152
|
+
# $1 可选进程快照:透传给 _pid_tree_has_names,避免每个候选 pane 各起一次 ps。
|
|
128
153
|
_pane_process_candidates() {
|
|
129
|
-
local names="${ADAPTER_PROCESS_NAMES:-}" id path cmd pid server attached title
|
|
154
|
+
local names="${ADAPTER_PROCESS_NAMES:-}" snapshot="${1:-}" id path cmd pid server attached title
|
|
130
155
|
[ -n "$names" ] || return 1
|
|
131
156
|
[ "$_PANE_SNAPSHOT_OK" = "1" ] || return 1
|
|
132
157
|
while IFS='|' read -r id path cmd pid server attached title; do
|
|
133
158
|
[ -n "$id" ] || continue
|
|
134
159
|
[ "${attached:-0}" -gt 0 ] || continue
|
|
135
|
-
if _pid_tree_has_names "$pid" "$names"; then printf '%s\n' "$id"; fi
|
|
160
|
+
if _pid_tree_has_names "$pid" "$names" "$snapshot"; then printf '%s\n' "$id"; fi
|
|
136
161
|
done <<EOF
|
|
137
162
|
$_PANE_SNAPSHOT
|
|
138
163
|
EOF
|
|
@@ -214,7 +239,10 @@ resolve_tmux_pane() {
|
|
|
214
239
|
|
|
215
240
|
# 1) 祖先链是权威信号(普通启动 / 旧 cc-switch / Claude 的 pane 内 hook)
|
|
216
241
|
_load_pane_snapshot || return 1
|
|
217
|
-
|
|
242
|
+
# 进程快照取一次,ancestry 爬链与进程树候选共用(每事件 1 次 ps 而非 4-6 次)
|
|
243
|
+
local ps_snap
|
|
244
|
+
ps_snap=$(_ps_process_snapshot)
|
|
245
|
+
pane=$(_pane_from_ancestry "$ps_snap")
|
|
218
246
|
if [ -n "$pane" ]; then
|
|
219
247
|
TMUX_PANE="$pane"
|
|
220
248
|
_TMUX_PANE_SOURCE="ancestry"
|
|
@@ -238,7 +266,7 @@ resolve_tmux_pane() {
|
|
|
238
266
|
fi
|
|
239
267
|
|
|
240
268
|
# 3) 同工具进程树候选 → adapter 提示 → hook cwd
|
|
241
|
-
candidates=$(_pane_process_candidates)
|
|
269
|
+
candidates=$(_pane_process_candidates "$ps_snap")
|
|
242
270
|
if [ -n "$candidates" ]; then
|
|
243
271
|
n=$(printf '%s\n' "$candidates" | grep -c .)
|
|
244
272
|
if [ "$n" -gt 1 ]; then
|
|
@@ -456,8 +484,13 @@ _acquire_event_lock() {
|
|
|
456
484
|
[ -n "$TMUX_PANE" ] || return 1
|
|
457
485
|
_ensure_pane_dir
|
|
458
486
|
local lock; lock=$(_event_lock_dir)
|
|
487
|
+
# 重试预算 ~2s。sleep 0.01 不被支持时退化为无 sleep 自旋(mkdir+stat 系统调用,
|
|
488
|
+
# ~几百 ms 耗尽预算)——绝不能回落 sleep 1:200×1s 病态忙等,而 sync hook 的
|
|
489
|
+
# 上游 timeout 是 600s,不会打断它(实测 macOS/Linux sleep 均支持小数,此为
|
|
490
|
+
# 其他平台兜底)。时间基准用 $SECONDS(builtin,零 fork;date +%s 每迭代一次 fork)。
|
|
491
|
+
local _start=$SECONDS
|
|
459
492
|
local i
|
|
460
|
-
for i in
|
|
493
|
+
for i in {1..200}; do
|
|
461
494
|
if mkdir "$lock" 2>/dev/null; then
|
|
462
495
|
echo "$$" > "$lock/owner"
|
|
463
496
|
return 0
|
|
@@ -470,11 +503,11 @@ _acquire_event_lock() {
|
|
|
470
503
|
continue
|
|
471
504
|
fi
|
|
472
505
|
mtime=$(stat -c %Y "$lock" 2>/dev/null || stat -f %m "$lock" 2>/dev/null)
|
|
473
|
-
if [ -n "$mtime" ] && [ $((
|
|
506
|
+
if [ -n "$mtime" ] && [ $(( _start + SECONDS - mtime )) -ge 10 ]; then
|
|
474
507
|
rm -f "$lock/owner" 2>/dev/null
|
|
475
508
|
rmdir "$lock" 2>/dev/null
|
|
476
509
|
fi
|
|
477
|
-
sleep 0.01 2>/dev/null ||
|
|
510
|
+
sleep 0.01 2>/dev/null || :
|
|
478
511
|
done
|
|
479
512
|
_ai_log "EVENT_LOCK: acquire timeout on $lock"
|
|
480
513
|
return 1
|
|
@@ -486,13 +519,34 @@ _release_event_lock() {
|
|
|
486
519
|
rmdir "$(_event_lock_dir)" 2>/dev/null
|
|
487
520
|
}
|
|
488
521
|
|
|
522
|
+
# 事件锁超时时 PermissionRequest 的无锁降级写入(仅 !/? 这类最强状态可无锁写,
|
|
523
|
+
# 调用点见 tmux-ai-status 锁段落的原则注释)。
|
|
524
|
+
# 最小动作集:一次 jq 取 tool_name → AskUserQuestion ? / 其他 ! → set-option →
|
|
525
|
+
# 日志标记 DOWNGRADE。刻意不碰 pending 文件(不加剧持锁者正在进行的文件竞争)、
|
|
526
|
+
# 不跑 _save_pane_id(锁外写 pane 映射有复活已清映射的风险)、不做 build_all_status
|
|
527
|
+
# (monitor 每秒重建聚合,降级值 ≤1s 内可见)。
|
|
528
|
+
_downgrade_permission_request() { # $1=原始 stdin JSON
|
|
529
|
+
local input="$1" tool status
|
|
530
|
+
tool=$(printf '%s' "$input" | jq -r '.tool_name // ""' 2>/dev/null)
|
|
531
|
+
if [ "$tool" = "AskUserQuestion" ]; then
|
|
532
|
+
status="?"
|
|
533
|
+
else
|
|
534
|
+
status="!"
|
|
535
|
+
fi
|
|
536
|
+
tmux set-option -pt "$TMUX_PANE" @ai_pane_status "$status" 2>/dev/null || true
|
|
537
|
+
_pane_loc=""
|
|
538
|
+
_ai_log "PermReq DOWNGRADE: event lock timeout, unlocked write '$status' (tool=${tool:-?})"
|
|
539
|
+
}
|
|
540
|
+
|
|
489
541
|
# Per-pane mkdir mutex。5s stale 强夺。retry 20 × 10ms ≈ 200ms 上限。
|
|
542
|
+
# sleep 回落策略同 _acquire_event_lock(不支持小数 sleep 时无 sleep 自旋,不回落 sleep 1)。
|
|
490
543
|
_acquire_lock() {
|
|
491
544
|
[ -n "$TMUX_PANE" ] || return 1
|
|
492
545
|
_ensure_pane_dir
|
|
493
546
|
local lock; lock=$(_lock_dir)
|
|
547
|
+
local _start=$SECONDS
|
|
494
548
|
local i
|
|
495
|
-
for i in
|
|
549
|
+
for i in {1..20}; do
|
|
496
550
|
if mkdir "$lock" 2>/dev/null; then
|
|
497
551
|
return 0
|
|
498
552
|
fi
|
|
@@ -500,13 +554,13 @@ _acquire_lock() {
|
|
|
500
554
|
local mtime
|
|
501
555
|
mtime=$(stat -c %Y "$lock" 2>/dev/null || stat -f %m "$lock" 2>/dev/null)
|
|
502
556
|
if [ -n "$mtime" ]; then
|
|
503
|
-
local age=$((
|
|
557
|
+
local age=$(( _start + SECONDS - mtime ))
|
|
504
558
|
if [ "$age" -ge 5 ]; then
|
|
505
559
|
rmdir "$lock" 2>/dev/null
|
|
506
560
|
mkdir "$lock" 2>/dev/null && return 0
|
|
507
561
|
fi
|
|
508
562
|
fi
|
|
509
|
-
sleep 0.01 2>/dev/null ||
|
|
563
|
+
sleep 0.01 2>/dev/null || :
|
|
510
564
|
done
|
|
511
565
|
_ai_log "LOCK: acquire timeout on $lock"
|
|
512
566
|
return 1
|
|
@@ -790,13 +844,15 @@ _pane_has_tool_process() { # $1=pane_id $2=工具名 [$3=进程快照]
|
|
|
790
844
|
}
|
|
791
845
|
|
|
792
846
|
# 所有 adapter 汇总的 AI 进程名
|
|
793
|
-
|
|
794
|
-
|
|
847
|
+
# $2 可选进程快照:调用方批量场景(_cleanup_stale_panes)传入以共用一次 ps
|
|
848
|
+
_pane_has_ai_process() { # $1=pane_id [$2=进程快照]
|
|
849
|
+
_pane_has_process_names "$1" "$(_collect_ai_process_names)" "${2:-}"
|
|
795
850
|
}
|
|
796
851
|
|
|
797
852
|
# 检查 pane 的所有 pane-id 文件是否过期(> 5 min),再验证进程树
|
|
853
|
+
# $2 可选进程快照(批量调用方传入,避免逐 pane 一次 ps)
|
|
798
854
|
_is_pane_session_dead() {
|
|
799
|
-
local pane_id="$1"
|
|
855
|
+
local pane_id="$1" snapshot="${2:-}"
|
|
800
856
|
local sanitized="${pane_id//[^a-zA-Z0-9]/_}"
|
|
801
857
|
local pane_dir="${_STATUS_DIR}/${sanitized}"
|
|
802
858
|
[ -d "$pane_dir" ] || return 0
|
|
@@ -809,7 +865,7 @@ _is_pane_session_dead() {
|
|
|
809
865
|
local age=$(( $(date +%s) - mtime ))
|
|
810
866
|
[ "$age" -lt 300 ] && return 1
|
|
811
867
|
done
|
|
812
|
-
! _pane_has_ai_process "$pane_id"
|
|
868
|
+
! _pane_has_ai_process "$pane_id" "$snapshot"
|
|
813
869
|
}
|
|
814
870
|
|
|
815
871
|
# --- 孤儿状态清理(_refresh 用)---
|
|
@@ -825,6 +881,11 @@ _cleanup_stale_panes() {
|
|
|
825
881
|
[ -n "$pane_id" ] && active_panes="$active_panes $pane_id"
|
|
826
882
|
done
|
|
827
883
|
|
|
884
|
+
# 进程快照取一次循环共用:✓/- 分支与死会话判定逐 pane 调 _pane_has_ai_process
|
|
885
|
+
# 时,旧实现每个 pane 各起一次 ps(pane 多时放大明显)
|
|
886
|
+
local ps_snap
|
|
887
|
+
ps_snap=$(_ps_process_snapshot)
|
|
888
|
+
|
|
828
889
|
while IFS='|' read -r pane_id pane_status; do
|
|
829
890
|
case "$pane_status" in
|
|
830
891
|
"!"|"?"|">")
|
|
@@ -834,14 +895,14 @@ _cleanup_stale_panes() {
|
|
|
834
895
|
_ai_log "STALE: clearing orphaned '$pane_status' on $pane_id"
|
|
835
896
|
tmux set-option -pt "$pane_id" @ai_pane_status "" 2>/dev/null || true
|
|
836
897
|
rm -rf "$pane_dir" 2>/dev/null
|
|
837
|
-
elif _is_pane_session_dead "$pane_id"; then
|
|
898
|
+
elif _is_pane_session_dead "$pane_id" "$ps_snap"; then
|
|
838
899
|
_ai_log "STALE: clearing dead session '$pane_status' on $pane_id"
|
|
839
900
|
tmux set-option -pt "$pane_id" @ai_pane_status "" 2>/dev/null || true
|
|
840
901
|
rm -rf "$pane_dir" 2>/dev/null
|
|
841
902
|
fi
|
|
842
903
|
;;
|
|
843
904
|
"✓"|"-")
|
|
844
|
-
if ! _pane_has_ai_process "$pane_id"; then
|
|
905
|
+
if ! _pane_has_ai_process "$pane_id" "$ps_snap"; then
|
|
845
906
|
_ai_log "STALE: clearing terminal state '$pane_status' on $pane_id (no ai process)"
|
|
846
907
|
tmux set-option -pt "$pane_id" @ai_pane_status "" 2>/dev/null || true
|
|
847
908
|
fi
|
|
@@ -901,12 +962,14 @@ _check_hooks_integrity() {
|
|
|
901
962
|
fi
|
|
902
963
|
}
|
|
903
964
|
|
|
904
|
-
# 兜底自修复:tmux
|
|
905
|
-
# 不依赖 SESSION_ID/EVENT,可被 _refresh 触发。60s
|
|
965
|
+
# 兜底自修复:tmux 端事件路径与 SessionStart hook 共用,配置被外部覆盖时自动重装。
|
|
966
|
+
# 不依赖 SESSION_ID/EVENT,可被 _refresh 与 SessionStart 触发。60s 节流退避
|
|
967
|
+
# (窗口内不重复 integrity 检查;修复失败 60s 后才重试)。
|
|
906
968
|
# 安装器由 adapter 声明(ADAPTER_INSTALLER)。
|
|
907
969
|
_maybe_repair_hooks() {
|
|
908
970
|
# 用户显式卸载意图(suppressed-<tool>)→ 不自动修复,尊重到显式安装为止。
|
|
909
971
|
if [ -f "${_SUPPRESS_DIR}/suppressed-${TOOL_ID:-claude}" ]; then
|
|
972
|
+
_ai_log "REPAIR: suppressed by uninstall marker, skip (${TOOL_ID:-claude})"
|
|
910
973
|
return 0
|
|
911
974
|
fi
|
|
912
975
|
local marker="${_STATUS_DIR}/.hooks-repair-${TOOL_ID:-claude}-ts"
|
|
@@ -922,9 +985,9 @@ _maybe_repair_hooks() {
|
|
|
922
985
|
local installer="${ADAPTER_INSTALLER}"
|
|
923
986
|
if ! _check_hooks_integrity; then
|
|
924
987
|
_ai_log "REPAIR: hooks integrity check failed, reinstalling (${TOOL_ID:-claude})"
|
|
925
|
-
#
|
|
926
|
-
#
|
|
927
|
-
#
|
|
988
|
+
# 浮出提示(成败区分):SessionStart(Claude Code hook 进程内)与
|
|
989
|
+
# _run_refresh(tmux run-shell)两个上下文都会到这里;hook 进程继承
|
|
990
|
+
# TMUX env,display-message 无 -p 写 client tty 而非调用方 stdout。
|
|
928
991
|
if [ -x "$installer" ] && "$installer" >/dev/null 2>&1; then
|
|
929
992
|
tmux display-message "✓ ${TOOL_ID:-claude} hooks 已自动修复(检测到缺失)" 2>/dev/null || true
|
|
930
993
|
else
|
package/scripts/tmux-ai-status
CHANGED
|
@@ -28,8 +28,22 @@ case "$EVENT" in
|
|
|
28
28
|
esac
|
|
29
29
|
# 同 pane 的并发 hooks 必须完整串行化,否则一个事件可能用过期派生结果覆盖另一个
|
|
30
30
|
# PermissionRequest 已写入的审批态。
|
|
31
|
+
#
|
|
32
|
+
# 锁超时(~2s 重试预算耗尽)时的降级原则——只有最强状态可无锁写:
|
|
33
|
+
# PermissionRequest 是 sync hook 且携带最高优先级审批态(!/?),丢失 = 状态栏
|
|
34
|
+
# 在用户审批前失明,因此降级为无锁最小写入(_downgrade_permission_request)。
|
|
35
|
+
# 其他事件(Pre/Post/Stop…)超时一律 exit 0 丢弃——无锁写弱状态(如 >)会
|
|
36
|
+
# 覆盖并发 PermReq 已早写的 !,正是 7448847 修掉的竞态,绝不能对它们降级。
|
|
37
|
+
# 降级最有价值的场景是「持锁者已死但未到 10s stale 强夺线」(此时无并发写入者,
|
|
38
|
+
# 降级写存活);反方向竞态(持锁者稍后的最终写入覆盖降级的 !)可接受——该
|
|
39
|
+
# 场景现状(超时丢弃)同样不显示 !,降级只多一瞬正确闪烁,终态不劣化。
|
|
40
|
+
# 恢复时间线:stale 强夺线(10s) > 重试预算(~2s),跨过 10s 线后的第一个事件
|
|
41
|
+
# 会强夺锁恢复正常路径。
|
|
31
42
|
if [ -n "$TMUX_PANE" ]; then
|
|
32
|
-
|
|
43
|
+
if ! _acquire_event_lock; then
|
|
44
|
+
[ "$EVENT" = "PermissionRequest" ] && _downgrade_permission_request "$INPUT"
|
|
45
|
+
exit 0
|
|
46
|
+
fi
|
|
33
47
|
trap '_release_event_lock' EXIT
|
|
34
48
|
fi
|
|
35
49
|
|
|
@@ -67,7 +81,10 @@ _current_pane_status() {
|
|
|
67
81
|
# 重建聚合状态(_refresh / _entered 共用)
|
|
68
82
|
_run_refresh() {
|
|
69
83
|
_maybe_repair_hooks
|
|
70
|
-
|
|
84
|
+
# 节流版清理(60s):tmux 生命周期 hook(pane-focus-in 等)高频触发,
|
|
85
|
+
# 全价 _cleanup_stale_panes 实测 ~455ms;卡死状态清理有 :37 的事件路径
|
|
86
|
+
# 节流兜底,此处无需每次全价。Stop/SessionEnd fallback 仍走全价精准清理。
|
|
87
|
+
_maybe_cleanup_stale
|
|
71
88
|
build_all_status
|
|
72
89
|
tmux set-option -g @ai_all_status "$ALL" 2>/dev/null || true
|
|
73
90
|
}
|
|
@@ -103,10 +120,12 @@ case "$EVENT" in
|
|
|
103
120
|
esac
|
|
104
121
|
;;
|
|
105
122
|
PermissionRequest)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
123
|
+
# 一次 @tsv 提取 tool_name/tool_use_id(digest 单独一次;stdin 只能读一次,
|
|
124
|
+
# 故从已缓存的 $INPUT 提取)。digest 只取 tool_name/tool_input——全量重序列化
|
|
125
|
+
# 会把 transcript_path 等无关字段落日志,且大 tool_input 全量展开拖慢 sync 路径。
|
|
126
|
+
_tool_fields=$(printf '%s' "$INPUT" | jq -r '[(.tool_name // ""), (.tool_use_id // "")] | @tsv' 2>/dev/null)
|
|
127
|
+
IFS=$'\t' read -r TOOL TOOL_USE_ID <<< "$_tool_fields"
|
|
128
|
+
INPUT_DIGEST=$(printf '%s' "$INPUT" | jq -c '{tool_name, tool_input}' 2>/dev/null)
|
|
110
129
|
if [ "$TOOL" = "AskUserQuestion" ]; then
|
|
111
130
|
STATUS="?"
|
|
112
131
|
else
|
|
@@ -135,9 +154,9 @@ case "$EVENT" in
|
|
|
135
154
|
fi
|
|
136
155
|
;;
|
|
137
156
|
PreToolUse)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
INPUT_DIGEST=$(
|
|
157
|
+
_tool_fields=$(printf '%s' "$INPUT" | jq -r '[(.tool_name // ""), (.tool_use_id // "")] | @tsv' 2>/dev/null)
|
|
158
|
+
IFS=$'\t' read -r TOOL TOOL_USE_ID <<< "$_tool_fields"
|
|
159
|
+
INPUT_DIGEST=$(printf '%s' "$INPUT" | jq -c '{tool_name, tool_input}' 2>/dev/null)
|
|
141
160
|
_add_pending_pre "$TOOL_USE_ID" "$TOOL"
|
|
142
161
|
_permission_pre_tool=0
|
|
143
162
|
for _permission_tool in ${ADAPTER_PERMISSION_PRE_TOOL_NAMES:-}; do
|
|
@@ -163,8 +182,8 @@ case "$EVENT" in
|
|
|
163
182
|
fi
|
|
164
183
|
;;
|
|
165
184
|
PostToolUse|PostToolUseFailure)
|
|
166
|
-
|
|
167
|
-
|
|
185
|
+
_tool_fields=$(printf '%s' "$INPUT" | jq -r '[(.tool_name // ""), (.tool_use_id // "")] | @tsv' 2>/dev/null)
|
|
186
|
+
IFS=$'\t' read -r TOOL TOOL_USE_ID <<< "$_tool_fields"
|
|
168
187
|
_remove_pending_id "$TOOL_USE_ID"
|
|
169
188
|
# 只解除当前工具对应项;其他权限请求仍 pending 时必须保持 !/?。
|
|
170
189
|
if _derive_status_from_pending; then
|
|
@@ -202,14 +221,12 @@ case "$EVENT" in
|
|
|
202
221
|
INPUT_DIGEST=$(echo "$INPUT" | jq -c '{source, model}' 2>/dev/null)
|
|
203
222
|
STATUS="-"
|
|
204
223
|
_clear_pending
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
fi
|
|
212
|
-
fi
|
|
224
|
+
# 自修复归并到 _maybe_repair_hooks:60s 节流退避(旧实现每次 SessionStart——
|
|
225
|
+
# 含 resume/clear/fork——都全量 integrity + 失败即重装且无 exit code 检查),
|
|
226
|
+
# 并统一 suppress 检查与成功/失败区分的 display-message。节流语义:窗口内
|
|
227
|
+
# 新会话不重复检查 integrity(外部改 settings 后最多延迟 60s 才被发现);
|
|
228
|
+
# 修复失败 60s 后由 _run_refresh 或下一次 SessionStart 重试。
|
|
229
|
+
_maybe_repair_hooks
|
|
213
230
|
;;
|
|
214
231
|
SessionEnd)
|
|
215
232
|
INPUT_DIGEST=$(echo "$INPUT" | jq -c '{reason}' 2>/dev/null)
|
|
@@ -260,11 +277,6 @@ case "$EVENT" in
|
|
|
260
277
|
;;
|
|
261
278
|
esac
|
|
262
279
|
|
|
263
|
-
# pane 解析失败时不写全局状态(避免把无归属事件当成有效状态)
|
|
264
|
-
if [ -n "$TMUX_PANE" ]; then
|
|
265
|
-
tmux set-option -g @ai_status "$STATUS" 2>/dev/null || true
|
|
266
|
-
fi
|
|
267
|
-
|
|
268
280
|
# 构造事件块详情行:
|
|
269
281
|
# tool: <tool_name> id=<short tool_use_id>
|
|
270
282
|
# input: <精简 JSON,超 2000B 截断>
|
|
@@ -274,8 +286,10 @@ if [ -n "$TOOL" ] || [ -n "$TOOL_USE_ID" ]; then
|
|
|
274
286
|
fi
|
|
275
287
|
_log_input_line=""
|
|
276
288
|
if [ -n "$INPUT_DIGEST" ] && [ "$INPUT_DIGEST" != "null" ]; then
|
|
277
|
-
|
|
278
|
-
|
|
289
|
+
# PermissionRequest 曾走完全不截断的 full 路径(9248967),实测大 tool_input
|
|
290
|
+
# (ExitPlanMode 的 plan、AUQ 的 questions)可达数 KB~数十 KB,改为 16KB 宽松上限。
|
|
291
|
+
if [ "$EVENT" = "PermissionRequest" ]; then
|
|
292
|
+
_log_input_line="input: $(_ai_log_truncate_input "$INPUT_DIGEST" 16384)"
|
|
279
293
|
else
|
|
280
294
|
_log_input_line="input: $(_ai_log_truncate_input "$INPUT_DIGEST")"
|
|
281
295
|
fi
|