fix(dsh): agents.get() 返回 agent 本体,当 handle 用缺一层 .agent
## 症状(上一版修复后暴露的新错)
会话 mail-515c0c70… 已在运行,直接 followup(不重复 resume)
new_mail 处理失败: Cannot read properties of undefined (reading 'followup')
去重生效了(不再撞 flock),但下一步炸。
## 根因:两个来源返回的不是同一种东西
ctx.agents.get(id) → **agent 本体**(.followup / .session 直接在上面)
startAgent() → handle,agent 在 **handle.agent**
证据在同文件 findLiveDshSession(约 1059 行)——它自己也要包一层:
const live = ctx.agents.get(bound.dshSessionId);
if (live) return { id: bound.dshSessionId, agent: live };
而去重那段把 `get()` 的返回值**直接赋给 handle** ⇒ `handle.agent` 是 undefined
⇒ 一取 followup 就炸。
⇒ 两条路径拿的是同一个对象,只是用法不同:一个包了一层、一个没包。
## 修法
`handle = { agent: alreadyLive }`。
## 判据(+2 格)
钉「从 `agents.get()` 取到的东西,必须包成 `{ agent: … }` 才能当 handle 用」。
★ 写这格时又被自己的两个假设绊住:
1. 先写的是「看 `handle = …` 那一行有没有 `agents.get`」——
实际代码是**两步**(先绑变量 `alreadyLive`,再 `handle = { agent: alreadyLive }`),
赋值式里根本没有 `get(` 字样 ⇒ 判据报「应至少有一处…」而自己红了。
改成**追变量来源**。
2. 追来源时正则先匹配到接管路径的 `live`(它的用法本来就对),
于是抓不到新加的那处 ⇒ 收紧成只匹配**双可选链** `?.get?.(` 那一处。
dsh 444 格全绿 · tsc 零错 · 部署成功(current → 20261004-094533)· 心跳正常。
变异:去掉 `{ agent }` 包装 → 红 2 格。
This commit is contained in:
@ -1341,10 +1341,19 @@ export function apply(ctx: any, config: PluginConfig): void {
|
||||
//
|
||||
// 1187 行的接管路径早有这段去重,注释写得很清楚(同一条会话两个 handle
|
||||
// 会各自往日志里写、replay 校验不过)。同一个陷阱漏在普通路径上。
|
||||
|
||||
// `ctx.agents.get()` 返回的是 **agent 本体**(见下方 `{ agent }` 处的注释)
|
||||
const alreadyLive = ctx.agents?.get?.(attemptSessionId);
|
||||
|
||||
if (alreadyLive) {
|
||||
handle = alreadyLive;
|
||||
// ★ 必须包一层 `{ agent }`:`ctx.agents.get()` 返回的是 **agent 本体**
|
||||
// (证据:findLiveDshSession 第 1059 行 `ctx.agents.get(id)` 之后
|
||||
// 紧接着 `return { id, agent: live }` —— 它自己也要包)。
|
||||
// 而 `startAgent` 返回的 handle 上才有 `.agent`,后续全部按
|
||||
// `handle.agent.session` / `handle.agent.followup` 取值。
|
||||
// 直接把 agent 当 handle 塞进去 ⇒ `handle.agent` 是 undefined ⇒
|
||||
// 实测报 `Cannot read properties of undefined (reading 'followup')`。
|
||||
handle = { agent: alreadyLive };
|
||||
console.error(
|
||||
`[dsh-mail-bridge] 会话 ${attemptSessionId} 已在运行,直接 followup(不重复 resume)`,
|
||||
);
|
||||
|
||||
@ -278,3 +278,84 @@ test('判据自检:抽掉 live 检查必须能红', () => {
|
||||
'★ 判据在「live 检查被抽掉」这个真实事故形状下仍判绿 —— 它抓不到自己要抓的东西',
|
||||
);
|
||||
});
|
||||
|
||||
/*
|
||||
handle 形状一致性判据(2026-10-04)。
|
||||
|
||||
# 症状
|
||||
|
||||
去重命中了(不再撞 flock),但下一步立刻炸:
|
||||
|
||||
会话 mail-515c0c70… 已在运行,直接 followup(不重复 resume)
|
||||
new_mail 处理失败: Cannot read properties of undefined (reading 'followup')
|
||||
|
||||
# 根因:`ctx.agents.get()` 与 `startAgent()` 返回的**不是同一种东西**
|
||||
|
||||
ctx.agents.get(id) → **agent 本体**(有 .followup / .session)
|
||||
startAgent() → handle,agent 在 **handle.agent**(.agent.followup)
|
||||
|
||||
证据在同文件 findLiveDshSession(约 1059 行)——它自己也要包一层:
|
||||
|
||||
const live = ctx.agents.get(bound.dshSessionId);
|
||||
if (live) return { id: bound.dshSessionId, agent: live };
|
||||
|
||||
而去重那段把 `get()` 的返回值直接当 handle 用 ⇒ `handle.agent` 是 undefined
|
||||
⇒ 一取 followup 就炸。
|
||||
|
||||
# 为什么单测抓不到
|
||||
|
||||
`startAgent` 要 dsh 宿主才能跑,纯函数测试够不着。而这个缺陷的形状是
|
||||
「两个来源返回两种形状,被当成同一种」——纯看类型(都是 `any`)看不出来。
|
||||
|
||||
⇒ 判据钉**赋值点的形状**:从 `get()` 取到的东西,必须包成 `{ agent: … }`
|
||||
才能当 handle 用。这一格就是当初该有、而没有的那一格。
|
||||
|
||||
# 顺带说明:接管路径为什么没炸
|
||||
|
||||
它写的是 `live.followup(...)`(直接用 agent 本体),**形状本来就对**。
|
||||
两条路径拿的是同一个对象,只是用法不同 —— 一个包了一层、一个没包。
|
||||
本判据不判断「哪种用法对」,只判断「从 get() 取出后有没有包」。
|
||||
*/
|
||||
|
||||
test('★ 从 agents.get() 取到的东西必须包成 { agent } 才能当 handle 用', () => {
|
||||
const src = liveNow();
|
||||
|
||||
// ★ 要**追变量来源**,不能只看赋值式本身:
|
||||
// 实际代码是两步 —— `const alreadyLive = ctx.agents?.get?.(id)` 然后
|
||||
// `handle = { agent: alreadyLive }`。只看 `handle = …` 那一行永远看不到
|
||||
// `get(`,第一版这么写直接报「应至少有一处 handle = agents.get(...)」——
|
||||
// 判据没看见它本该看的东西。
|
||||
// ⇒ 两步:先把 `agents.get(…)` 的返回值绑到哪个变量,再看该变量被怎么用。
|
||||
const getVar = src.match(/const\s+([A-Za-z_][\w]*)\s*=\s*ctx\.agents\?\.get\?\./);
|
||||
assert.ok(getVar, '应有一处「从 agents.get() 取值并绑定到变量」');
|
||||
|
||||
const assigns = [...src.matchAll(/handle\s*=\s*([^;\n]+);/g)].map((m) => m[1].trim());
|
||||
const fromGet = assigns.filter((a) => a.includes(getVar[1]));
|
||||
assert.ok(fromGet.length >= 1, `应至少有一处 handle 用到 ${getVar[1]}`);
|
||||
|
||||
const bad = fromGet.filter((a) => !/^\{[^}]*\bagent\s*:/.test(a));
|
||||
assert.deepEqual(
|
||||
bad,
|
||||
[],
|
||||
'★ 这些地方把 agents.get() 的返回值直接当 handle 用:' + bad.join(' | ') +
|
||||
'\\n ⇒ get() 返回 agent 本体,而 handle 上才有 .agent ⇒' +
|
||||
' handle.agent 是 undefined ⇒ 一取 followup 就报' +
|
||||
'「Cannot read properties of undefined」。(实测踩过)',
|
||||
);
|
||||
});
|
||||
|
||||
test('判据自检:去掉 { agent } 包装必须能红', () => {
|
||||
const src = liveNow();
|
||||
const broken = src.replace(/handle\s*=\s*\{\s*agent:\s*([a-zA-Z_][\w]*)\s*\s*\};/,
|
||||
'handle = $1;');
|
||||
assert.notEqual(broken, src, '变异必须真的改到源码(否则下面的转红是假的)');
|
||||
|
||||
const getVar = broken.match(/const\s+([A-Za-z_][\w]*)\s*=\s*ctx\.agents\?\.get\?\./);
|
||||
const assigns = [...broken.matchAll(/handle\s*=\s*([^;\n]+);/g)].map((m) => m[1].trim());
|
||||
const fromGet = assigns.filter((a) => a.includes(getVar[1]));
|
||||
const bad = fromGet.filter((a) => !/^\{[^}]*\bagent\s*:/.test(a));
|
||||
assert.ok(
|
||||
bad.length > 0,
|
||||
'★ 判据在「去掉 { agent } 包装」这个真实事故形状下仍判绿 —— 它抓不到自己要抓的东西',
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user