发现于 #5529 的实测过程(PR 见该 issue),与该修复相邻但不同面 —— 这是 job 域的审计面问题,不是 automation 域的。
观察(实测,非推断)
DbJobAdapter.wrap()(packages/services/service-job/src/db-job-adapter.ts L161-176)只按 handler 是否抛错判定运行结果:
try { await handler(ctx); finishRun(runId, 'success'); bumpJob(name, 'success'); }
catch { finishRun(runId, 'failed', msg); bumpJob(name, 'failed', msg); throw err; }
对于「内部自行处理失败、不抛错」的 handler,这条路径把它记成成功。实测(临时 harness,已删除):一个 once job 的 handler 正常返回但什么也没完成,sys_job 行是
{"name":"flow-wait:run1:pause","active":true,"schedule_expression":"2026-08-05T16:45:20.837Z","last_status":"success","run_count":1}
sys_job_run 只有一行 status: 'success'。
为什么可能值得看
sys_job / sys_job_run 是机器可读面(Studio 的作业界面读它)。一种读法是 sys_job_run.status 本就只表示「handler 跑完没抛异常」,那它没说谎;另一种读法是它作为作业审计面在这里给不出「这一枪没干成事」的信号。两种读法都成立,所以这里只做记录、不预判严重度。
需要说明的是 #5529 之后这个洞不是无声的:那条路径已经有一条 error 日志,并且 job 被刻意保留 active(#5529 就是这么修的),所以「run 卡住」本身是可见的 —— 缺的只是作业审计行里的那一格。因此按观察类归档(finding,不进 pm:queue),严重度交分诊判定。
可能的方向(未决,不在本 issue 范围内实施)
佐证
2026-08-08 裁决落地(services 座位 PM,会话 session_01USNUyHEr7uaU6MoEWXitei):维护者批复三轴分析,采纳 B-minimal(可加性第三态;A 与 #5529 裁决冲突被否,C 把审计静默合法化被否)。按 shared-contract 规则 contract-first 拆分:spec 半边见 #6617(JobHandler 可选 degraded-outcome 回报通道,API 形状归 spec 座位);本单收窄为 services 半边(DbJobAdapter 把回报映射为区别于 success 的 sys_job_run.status + bumpJob 同步 + #5529 场景回归测试)。
2026-08-09 二次阻塞(round 18 dev 实测,详见 #7072 与本线程 13:4xZ 评论):#6617 只交付了生产者侧(JobRunOutcome + JobHandler 返回值);sys_job_run.status 的消费侧词表被三处 schema 钉死(spec 的 JobExecutionStatus enum + platform-objects 的两个 Field.select),全不在 services 车道 —— 适配器改动编译不过(TS2345),绕过类型则被 record-validator 以 invalid_option 拒写且被 finishRun 的 try/catch 吞掉。词表拓宽 = #7072;它落地后本单退化为一次小的适配器接线。
Blocked-by: #7072
发现于 #5529 的实测过程(PR 见该 issue),与该修复相邻但不同面 —— 这是 job 域的审计面问题,不是 automation 域的。
观察(实测,非推断)
DbJobAdapter.wrap()(packages/services/service-job/src/db-job-adapter.tsL161-176)只按 handler 是否抛错判定运行结果:对于「内部自行处理失败、不抛错」的 handler,这条路径把它记成成功。实测(临时 harness,已删除):一个 once job 的 handler 正常返回但什么也没完成,
sys_job行是sys_job_run只有一行status: 'success'。为什么可能值得看
sys_job/sys_job_run是机器可读面(Studio 的作业界面读它)。一种读法是sys_job_run.status本就只表示「handler 跑完没抛异常」,那它没说谎;另一种读法是它作为作业审计面在这里给不出「这一枪没干成事」的信号。两种读法都成立,所以这里只做记录、不预判严重度。需要说明的是 #5529 之后这个洞不是无声的:那条路径已经有一条 error 日志,并且 job 被刻意保留
active(#5529 就是这么修的),所以「run 卡住」本身是可见的 —— 缺的只是作业审计行里的那一格。因此按观察类归档(finding,不进pm:queue),严重度交分诊判定。可能的方向(未决,不在本 issue 范围内实施)
IJobService的失败语义(第三方实现可能据此重试),超出该 issue 派发范围。IJobService的 handler 一个「跑完了但没干成」的回报方式(而不是只有 throw / 不 throw 两态),由适配器映射到一个区别于success的sys_job_run.status。这会动IJobService契约面,属公开契约决定。sys_job_run.status的语义就是「handler 未抛错」,并把这一点写进契约注释,免得下一个读者重新推一遍。佐证
packages/services/service-job/src/db-job-adapter.tsL161-176(wrap),L277-299(bumpJob)。packages/spec/src/contracts/job-service.ts——JobHandler返回Promise,没有第三态。STORE_UNAVAILABLE时不取消 + 记 error,是本条观察的具体触发场景。2026-08-08 裁决落地(services 座位 PM,会话
session_01USNUyHEr7uaU6MoEWXitei):维护者批复三轴分析,采纳 B-minimal(可加性第三态;A 与 #5529 裁决冲突被否,C 把审计静默合法化被否)。按 shared-contract 规则 contract-first 拆分:spec 半边见 #6617(JobHandler 可选 degraded-outcome 回报通道,API 形状归 spec 座位);本单收窄为 services 半边(DbJobAdapter 把回报映射为区别于success的sys_job_run.status+bumpJob同步 + #5529 场景回归测试)。2026-08-09 二次阻塞(round 18 dev 实测,详见 #7072 与本线程 13:4xZ 评论):#6617 只交付了生产者侧(
JobRunOutcome+JobHandler返回值);sys_job_run.status的消费侧词表被三处 schema 钉死(spec 的JobExecutionStatusenum + platform-objects 的两个Field.select),全不在 services 车道 —— 适配器改动编译不过(TS2345),绕过类型则被record-validator以invalid_option拒写且被finishRun的 try/catch 吞掉。词表拓宽 = #7072;它落地后本单退化为一次小的适配器接线。Blocked-by: #7072