Repository navigation
daemon: agent 测试运行继承生产 DB 路径变量,可整库清空引擎数据库(bc#853 停摆实锤) #7
Copy link
Copy link
Open
Description
Activity
🤖 Powered by ework
[bot] 🏷 已接手。先给审核结论(动手前的验证):
复现/查证:事故链与代码完全对得上——
packages/daemon/src/db.ts:78:DB_PATH = WORK_DB_PATH || DAEMON_DB_PATH || ~/.local/share/...,模块加载期读环境;config.tsreadDbSection 同样WORK_DB_PATH ?? DAEMON_DB_PATH ?? fallback。测试进程继承 daemon spawn 的 env 后,initDB()直接打开生产库。- 更隐蔽的一点:
packages/daemon/bunfig.toml的[test] preload=["./tests/setup.ts"]只在包目录生效,monorepo 根没有 bunfig.toml——从根跑bun test时 setup 完全不执行,事故向量至今还在。且 setup.ts 只钉了WORK_DB_PATH,没 unsetDAEMON_DB_PATH/OPENCODE_DB_PATH/DAEMON_ENV(DAEMON_ENV 还会把 config 拉进 production 分支)。 - bc#853「线程永久不调度」的机制也查实了:
issues.owner_daemon_id REFERENCES daemons(id);daemons 行被清后,claimIssue 的 UPDATE 写悬空外键 → FK constraint failed → handler 崩溃。而且指向已删除 daemons 行的 owner 引用是死锁态:claimIssue要求owner IS NULL,releaseDeadOwners只匹配"存在但心跳过期"的行——悬空引用谁都接不走,即使重启+回填,旧进程持有旧 daemonId 期间该线程也永远 claim 不到。
层次判断:用户给的修复规格打到了核心。ENV_DENY 是根治(运行时隔离),测试自隔离是纵深,claimIssue 自愈是兜底,三者互补不冲突。我补充两点会一并做:①
releaseDanglingOwners()(清悬空 owner 引用)——这是"丢一行=线程死寂"的另一半,规格里没点名但不加的话自愈不完整;② ENV_DENY 同时覆盖 pi-backend.ts(它有同样的 env 直通,只是没有 always-deny 列表)。方案:四项全做(web→daemon 对账器不在"可选"范围外,一并实现,用 web shim 已验证的
/api/v1/repos/issues/search?state=open端点),各带测试;全量bun test(包内+仓库根两条路径都验)+ tsc;PR 引用 Closes #7。测试本身在隔离环境跑(setup 统一 unset+断言)。开始实现,有进展再更新。
🤖 Powered by ework · qwen3.8-27b
审核结论(动手前已验证)
- 事故链复现成立:config.ts/db.ts 的取值顺序确为
WORK_DB_PATH ?? DAEMON_DB_PATH ?? 默认路径,daemon spawn 环境原样透传子进程(ENV_DENY_ALWAYS 原本只挡 OPENCODE* 键),测试 harness beforeEach 对 messages/op_sessions/issues 全表 DELETE —— 只要进程环境带生产库变量,跑 bun test 必清生产库。 - bc#853 线程死寂是表面现象,根因分两层:① 库被清后 claimIssue 的 UPDATE 触发 FK 失败;② 更隐蔽的一层——
releaseDeadOwners的 WHERE 子句要求 daemons 行存在,永远看不到「owner_daemon_id 指向已删除行」的悬空引用,这类 issue 永久无人认领。修复按三层走:断继承(根治)、自愈(纵深)、对账(兜底)。
规格四项全部落地,各带测试
- 运行时隔离:ENV_DENY_ALWAYS 增加
DAEMON_DB_PATH、WORK_DB_PATH、OPENCODE_DB_PATH、DAEMON_ENV,opencode 与 pi 两个 backend 共享同一常量(pi-backend 存在同样的泄漏,一并修)。测试 spawn 假二进制 dump 自身 env,断言四键不存在。 - 测试自隔离:tests/setup.ts 统一 unset 上述变量(含
WORK_DB_PREFIX)并把 WORK_DB_PATH 钉到os.tmpdir()下唯一文件(pid+uuid;bun 并行文件间 pid 会复用,单 pid 会撞库——本次实测撞过)。新增根级 bunfig.toml,从仓库根裸跑bun test也不再可能碰到生产/默认路径。另有子进程回归模拟测试:继承一个恶意 DAEMON_DB_PATH 起子进程,证明测试库不会被带偏。 - claimIssue 自愈:ensureOwned 先确保 issue 行存在(注意:重建的行拿到全新本地 uid,下游一律用返回的行,这是实测踩出的坑);FK 失败时 healDaemonRow 重新注册 daemon 身份并重试一次。另加
Store.releaseDanglingOwners()清理指向已删 daemons 行的悬空 owner(启动+观察周期两处调用)。 - web→daemon 对账器:观察周期每小时(可触达或零 owned issue 时)查 web 的 open issues 搜索接口,缺失行自动补为 active 且不触发任务;scopes = 现有 active issue 的 scope 并集 + 新增配置
WORK_RECONCILE_SCOPES(work.reconcileScopes)。顺带覆盖本次手工回填的操作。
验证
tsc --noEmit干净;23 个新测试在包目录、仓库根、敌对环境子进程三种上下文全绿。- 全量 daemon 套件 228/258,30 个失败与本沙箱 master 基线逐条 diff 完全一致(纯环境因素:~8 个旧 harness 硬编码 /tmp 路径在只读 /tmp 挂载下 EACCES;loopback clone 走 egress proxy 返回 503)——零回归。
附带发现(按规则只在本线程报告,不另开 issue)
- ~8 个既有测试 harness(multi-engine、exit-chain、concurrency、model-pool、session-capture、stranded-interrupted、pi-backend、workdir)硬编码
/tmp/...路径,任何只读 /tmp 的环境都会 EACCES,建议统一改 tmpdir()。 - 跨包环境变量冲突:web 与 daemon 都从同一个
WORK_DB_PATH解析各自的 DB 但 schema 不同;从仓库根混跑两包测试时文件共享同一进程环境(bun 忽略嵌套 bunfig)→ 会出现「no such column」类串扰。属既有地雷,本 PR 未动。
一句话总结:根治了 agent/测试进程继承生产 DB 路径变量导致的整库清空(运行时剥变量+测试钉 tmpdir+根级 preload 三道),并为「丢一行=线程死寂」加了自愈、悬空清理、小时级 web 对账三层防线,验证零回归,可以合并。
- 事故链复现成立:config.ts/db.ts 的取值顺序确为
继续
Metadata
Metadata
Assignees
Labels
No labels
事故链(2026-09-17 01:17-01:35Z)
agent 按 issue 验收标准在 workdir 里跑
bun test→ 测试进程继承了 daemon spawn 的DAEMON_DB_PATH(config.ts:132WORK_DB_PATH ?? DAEMON_DB_PATH ?? fallback)→ 测试 harness 的 beforeEachDELETE FROM messages/op_sessions/issues/daemons清空了生产引擎库(250 issue 行、全部会话/消息状态)。后果:① 存量线程 comment_created → claimIssue(op.ts:515)FK constraint failed → handler 崩溃、线程永久不调度(bc#853);② 全部进行中任务丢失。已人工恢复:热修 ENV_DENY(线上树)、重启、按 web 库回填 250 行、requeue。修复规格
DAEMON_DB_PATH、WORK_DB_PATH、OPENCODE_DB_PATH、DAEMON_ENV(已在生产热修,需入库+测试:spawn 后子进程 env 不含这些键)验收
四项各带测试;全量 bun test + tsc;PR 引用 Closes #N。注意:本 issue 的测试编写本身就要在隔离环境跑,别重蹈覆辙。
Mirrored from ework issue #7