Skip to content

fix: the inline bash patch never runs when the tool face settles after agent/created - #36

Merged
KannaKuron merged 3 commits into
KannaKuron:mainfrom
qiantao448-ux:fix/inline-tool-face-settles-late
Oct 27, 2026
Merged

KannaKuron merged 3 commits into
KannaKuron:mainfrom
qiantao448-ux:fix/inline-tool-face-settles-late

Conversation

@qiantao448-ux

Copy link
Copy Markdown
Contributor

症状(真机复现)

Win11 桌面端冷启动后,某个会话既没有 bash、又留着一个被拦截的 pwsh:

> pwsh: the pwsh tool is hidden on this host by the dsh-gitbash-shell settings (pwsh tool switch, off by default).
>        Use the bash tool for commands.

但 bash 根本不存在。插件自己的日志里没有 own bash tool activated,也没有任何 activation failed —— 也就是说 reconcile() 那次「注册 bash + 遮蔽 pwsh」从来没跑成,活着的只有 tools/pre-execute 那层全局钩子(它与 face 无关,所以照样拒绝 pwsh)。表现出来像"插件坏了",日志里却一片干净。

根因

reconcile() 的触发点只有三个:agent/created、插件挂载时的存量 sweep、loader/volatile-update(设置开关)。而工具面为空时它是这样收场的:

if (attempt < 5 && !names.includes('bash') && !names.includes('pwsh')) {
  const timer = setTimeout(() => reconcile(agent, { attempt: attempt + 1 }), 250)
  ...
  return
}

250ms × 5,过期即终局。 桌面端启动要先挂 profile 级插件、再去挂 preset 行,这个窗口很容易被跨过去;created 那一刻读到的是空面,五次之后插件静默放弃,而之后再没有任何落点。

宿主其实有 tools/change 事件(registry 每次变更都 emit),本插件没监听 —— 这不是"监听写错了",是根本没有第二个对账时机。

修法(两个 commit,一正一反)

1. tools/pre-execute 里惰性再对账 —— 第一次工具调用是"面一定已经落定"的最早时刻。该 agent 还没有 bash scope 就重跑一次决策;state.scope 一旦存在即永久跳过,幂等。故意不监听 tools/change:那不是更好的触发点,而是会自己触发自己的环路(reconcile 内部的 restrict() 也会 emit),在有界重试之外再叠一层事件风暴不值得。

2. 面里没有 pwsh 就撤掉自己注册的 bash —— 这条是上一条的安全阀,单独成 commit。

自有 bash 注册在 agent 自有 scope 里,而宿主契约写明掩码只裁全局工具、scope 注册保持可见:

ctx.tools.restrict(filter) 对单个 agent 继承的全局工具应用允许或拒绝掩码;掩码取交集,作用域注册保持可见。

于是"face 已落定"从性能问题升级成正确性前提:惰性对账把决策推迟到落定之后,但如果哪次抢在前面注册成功,任何 preset 的 restrict({ allow }) 都拿不回来 —— 像纯聊天类 preset(只放行两个工具的白名单)会凭空多出一条命令通道。

reconcile 观测到的 names 是解开自身 pwsh 掩码之后的面(函数开头先 denyPwsh()),所以它权威:只要非空且不含 pwsh,就 dispose 自己那个 scope。names.length > 0 兜住"注册表读不到/读到空",避免把读取失败误判成"被掩码"。

验证

单元 / 冒烟(本机,npm test)

ℹ tests 334
ℹ pass 334
ℹ fail 0

真机(Windows 桌面端,改动前 → 改动后)

改动前 改动后
bash 工具 不存在 存在,uname -s → MINGW64_NT-10.0-26200,GNU bash 5.3.15
pwsh 可见但被拒(自相矛盾) 不再出现在面上
插件日志 无 activated 行 own bash tool ready (private Git Bash executor at C:/Program Files/Git/bin/bash.exe) + own bash tool activated for this agent

验证方式是冷启动后重开会话,并确认插件日志里的 activated 行与 bash 工具同时出现。

一个诚实的边界

commit 2 的撤场是异步 dispose:如果撤场时 provideBash() 正在挂载途中,那一次注册仍会落地,下一次 reconcile 再收掉 —— 收敛但有一个周期的抖动。要彻底消除得给 provideBash 加代际号(state.epoch),改动面比这次修复大,留作后续。

未包含

排查时我还在本机加了一行"把插件日志同时写进 ~/.dsh/logs/"的补丁 —— 那是为了在没有控制台的桌面端取证,没有带进这个 PR:src/ 内直写文件与仓库"不得直写 profile/patch"的守卫测试相邻,且属于调试设施而非修复。要的话可以单独提一个 opt-in(DSH_GITBASH_SHELL_LOG=1)的 PR。

…patch missed

The inline bash patch decides the face once at agent/created (plus a
mount-time sweep) and retries a face that carries NEITHER tool five times
at 250ms before giving up for good. On a machine whose preset rows and
plugin tools finish registering later than that window, the give-up is
final: the agent keeps the official win32 face (pwsh, no bash), so the
session has no shell tool at all while the pwsh tool it does see is denied
by the call-layer guard — a state that reads as "the plugin is broken" but
logs nothing.

Re-run the same decision from tools/pre-execute while this agent still owns
no bash scope. That is the first moment the face is guaranteed to be
settled, it costs one map lookup per call, and it is idempotent: once
provideBash() claims the scope the branch never fires again.

Observed on the desktop build: after a cold start the plugin's own log
never printed 'own bash tool activated', and the model face offered pwsh
(build-time enabled on win32) but no bash.
Our bash tool registers in an agent-owned scope. By contract, masks applied
with tools.restrict() only filter the GLOBAL tools an agent inherits and
scoped registrations stay visible — so once we register, no preset gate can
take the shell away again.

That makes the face-is-settled assumption a correctness requirement, not
just a performance one: a registration decided on a half-built face puts a
shell into a preset that deliberately masked every command tool (the
pure-chat preset keeps only an allow-list of two tools). Combine that with
the lazy re-reconcile in the previous commit and the window widens on a
slow boot.

The pre-mask observation is already authoritative here (reconcile lifts our
own pwsh deny before reading the face), so stand down whenever it carries
no pwsh: dispose the scope, drop the ops-shell deny, and let the next
reconcile decide again. The length guard keeps a failed read (registry
unavailable, empty list) from being mistaken for a masked face.
@KannaKuron
KannaKuron merged commit 051e42e into KannaKuron:main Oct 27, 2026
4 checks passed
KannaKuron added a commit that referenced this pull request Oct 27, 2026
….43.3) — inlineNeeds negative cases for pwsh-less faces, plus source assertions on the pre-execute guard shape and the stand-down's position before the needsBash check
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants