Incident explain · 2026-05-29

它不是业务失败。是 agent 自己写的 runner 在第一步就炸了。

`63e5e8a8` 还没走到 ElevenLabs、Rendi、字幕、Mai callback。 它卡在本地临时脚本的 helper 导入。更糟的是,agent 没有修脚本重跑,而是直接把任务打成 failed。

任务 63e5e8a8-09ab-4ac3-9889-076ba1fb359f
快照 voiceover_align v8 · voiceover-align v16
平台 run success / exit_code 0
任务状态 agent 调了 tobatsu-agent fail
没有 result.json 没有 deploy.json 没有媒体产物 没有进入 provider 阶段 Mai 侧没有收到 callback
What actually happened

真实路径很短:写脚本,导 helper,崩溃,直接 fail。

这次失败不是长流程中间某个外部服务报错。它发生在本地脚本刚启动时。 所以任何把它归因到素材、Rendi、ElevenLabs、Cloudflare 的说法都不对。

实际发生的路径

1
agent 生成临时 runner`outputs/.logs/run_voiceover_align.py` 是临场写出来的编排脚本。
2
runner 动态导入 helper`load_helper()` 用 `importlib` 加载 `voiceover_align.py`。这一步发生在读取 payload 之前。
3
helper 的 dataclass 炸了因为 helper module 没先注册到 `sys.modules`。
4
没有自修复agent 包装成 `internal_error`,直接 `tobatsu-agent fail`。

本来应该走的路径

1
先做 helper import smoke test脚本还没花钱前,确认本地 runner 能导入。
2
发现内部脚本错写 `outputs/.logs/self-repair-*.json`,保留 traceback。
3
修 runner,重跑一次这不是上游错,也不是 provider 错。先自救。
4
再进入业务分类只有真实业务、provider、callback 错误才允许 terminal fail。
这条失败的关键判断

没有 `outputs/result.json`,没有 `outputs/deploy.json`,没有音频/视频产物。说明流程没到业务阶段,也没拿到可用的 callback envelope。

The exact code trap

问题不是 importlib 本身。问题是少了一行注册。

Mai 的 correction 也确认了:原始脚本作为程序运行时,runner 自己的 `dataclass` 不是第一爆点。 第一爆点是 `load_helper()` 导入 helper。helper 里有 `@dataclass class Segment`。

当前 runner 写法 · 会炸
def load_helper():
    import importlib.util
    spec = importlib.util.spec_from_file_location(
        "vo",
        ROOT / ".tobatsu/skills/voiceover-align/scripts/voiceover_align.py",
    )
    mod = importlib.util.module_from_spec(spec)
    assert spec and spec.loader
    spec.loader.exec_module(mod)
    return mod
安全写法 · 先注册 module
def load_helper():
    import importlib.util
    import sys
    spec = importlib.util.spec_from_file_location(
        "vo",
        ROOT / ".tobatsu/skills/voiceover-align/scripts/voiceover_align.py",
    )
    mod = importlib.util.module_from_spec(spec)
    assert spec and spec.loader
    sys.modules[spec.name] = mod
    spec.loader.exec_module(mod)
    return mod
为什么 `@dataclass` 会在这里炸

`dataclasses` 会按类的 `__module__` 去 `sys.modules` 里找 module namespace。动态导入时没注册,它拿到 `None`,于是报 `NoneType.__dict__`。

为什么 Mai 没收到 business_failed

runner 的 `main()` 先跑 `load_helper()`,后读 `.tobatsu/context.json`。它在第一步就炸了,所以失败处理里没有完整 payload,也没有 callback URL / token 可以组装 Mai 业务失败回调。

Why it is on us

这不是一次普通失败,是两层指令没兜住。

agent 写错代码本身可以发生。真正不能接受的是:错在自己脚本,还没自修复,就把 task 变成 terminal failed。

哪里出了问题
应该怎么约束
skill 示例
`voiceover-align v16` 里有动态导入 helper 的示例,但示例少了 `sys.modules` 注册。
示例必须改成安全写法,或者让 agent 用 helper CLI,不手写 loader。
workflow 指令
`voiceover_align v8` 要求 agent 跑全流程,但没有明确说 runner 自身 bug 要先自修复。
加硬规则:脚本 import、语法、NameError、dataclass 错都先修脚本重跑。
终态处理
agent 看到 `internal_error` 后直接 `tobatsu-agent fail`。Mai 侧没有 business result,也没有 platform-terminal 结果。
没进入业务阶段前,不允许把内部脚本 bug 当作业务失败;至少要留下可恢复状态和清楚证据。
一句话判断

如果错误发生在 agent 自己写的 glue code 里,且还没调用外部服务,那第一动作是修代码,不是 fail。

Hotfix plan

补两道闸:一闸防写错,一闸防错了直接死。

不改 Mai。不重启 worker。新任务创建时会拿 DB 里的最新 workflow 和 skill。

Skill v17

安全 helper 导入

把 `importlib` 示例改成注册 `sys.modules` 的版本。写明 `dataclass` helper 不能裸 `exec_module`。

Workflow v9

内部脚本先自救

runner import、临时脚本语法、NameError、AttributeError 都要先修脚本并重跑一次。

Terminal gate

不能过早 fail

只有真实业务、provider、callback 错误才进入 failed callback 和 `tobatsu-agent fail`。

发布方式

走 API 发布 DB revision。不要 sync,不要拉 worker,不要重启。旧 task snapshot 不变,新 task 使用新版本。

Retry path

这条任务不能原地变成功。要用新 run 重打。

Tobatsu task 已经 terminal failed。后面就算脚本修好,也不能把同一个 task 改回 done。 正确做法是发布补丁后,让 Mai 用新的 Gateway run / callback envelope 重发。

1. 发布 skill v17 + workflow v9 回读 active revision 和 content hash。确认新任务会拿到修后的文档。
2. Mai 发一条新的 voiceover_align 用同类 payload 或同一业务输入,但必须是新的 Gateway run。
3. 看两个证据 如果 runner 自身出错,应出现 `self-repair` 证据和第二次运行。不能第一下就 failed。
4. 成功或真实失败都要干净回 Mai 成功有 business result。真实失败有 failed result。不能让 Mai Gateway 一直 running,像 `gw_28eb5a35` 这次一样无人收口。