You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
finding: the HTTP node reports acted: 1 for a merely-enqueued outbox delivery — same overstatement #7747 fixed for notify, against sys_http_delivery #7882
packages/services/service-automation/src/builtin/http-nodes.ts:139 returns { output: { deliveryId, enqueued: true }, metrics: { acted: 1 } } for a delivery that has only been enqueued to the HTTP outbox. sys_http_delivery carries its own terminal outcomes (including dead), decided by the dispatcher afterwards — so a flow-run summary can read acted: 1 for an HTTP delivery that later dead-letters, and the operator-facing summary disagrees with the durable record.
This is the same defect class #7747 (PR #7875) just fixed for the notify node, at the sibling node. Found by that card's dev while measuring the notify path; deliberately left out of scope there rather than ridden along.
Why it is likely the same fix shape
#7875 established the pattern in this exact codebase: acted counts only what a channel accepted terminally; a merely-enqueued effect is reported as unmeasuredEffect, which is the platform's existing qualifier for "an effect happened but the outcome is not yet knowable". That keeps healthy outbox-backed runs out of the documented broken-sweep alert (selected > 0 AND acted = 0 AND unmeasured = 0) without asserting success, and respects connector.zod.ts:613's rule against a blanket acted: 0.
Worth confirming before assuming it transfers: whether the HTTP node has an inline (non-outbox) path whose outcome IS terminal at return time, as MessagingService.emit() did — if so, the same two-path narrowing applies; if every HTTP delivery goes through the outbox, the fix is simpler.
Triage: lands in packages/services/service-automation/src/builtin/http-nodes.ts (~:139) → domain:services; promoted straight to pm:queue rather than held as a finding: the defect is concrete (operator-facing summary disagrees with the durable sys_http_delivery record), the fix pattern is already established and merged at the sibling node (#7747 / PR #7875, unmeasuredEffect for merely-enqueued effects), and the scope is closed.
One verification the card itself names as required before assuming transfer: whether the HTTP node has an inline (non-outbox) path whose outcome IS terminal at return time — if yes, the same two-path narrowing as #7875 applies; if all deliveries go through the outbox, the fix is simpler. Respect connector.zod.ts:613's rule against a blanket acted: 0 either way.
Size/model suggestion: S–M (pattern transfer with one measurement), mode:subagent acceptable if the verification is mechanical, model: sonnet.
Claim: PM loop round 6
Session: session_01JVfWSQN9RDDGwdVSmMc29x
Branch: claude/issue-7882-http-node-enqueued-acted
Worktree: cloud session's own clone (mode:cloud)
Domain: domain:services
File surface: packages/services/service-automation/src/builtin/http-nodes.ts (~:139) + its tests. The sys_http_delivery object and the outbox/dispatcher path are read surfaces for measurement.
Container & model: M, mode:cloud, model: opus
Precedent card, and the reason this one should be cheap: #7747 → PR #7875 (MERGED) fixed the identical overstatement class for the notify node. That PR's reasoning is the starting point, not a suggestion to copy blindly:
EmitResult split enqueued from delivered — delivered means a channel accepted the delivery (terminal, observed); merely-durable rows are enqueued;
the node then reports unmeasuredEffect for enqueued-but-unsent, ⛔ not a bare acted: 0 — connector.zod.ts:613 explicitly forbids the latter, and it would trip the broken-sweep alert on every healthy outbox-backed run;
the alert is selected > 0 AND acted = 0 AND unmeasured = 0, so a pending delivery suppresses the alert without asserting success.
⚠️This is a precedent, not a ruling.#7875's shape was derived from MessagingService's two-path structure; http-nodes.ts may not have the same seam. Measure before porting — if the HTTP path's structure makes a different (or simpler) shape correct, take it and say so. Falsifying the precedent's applicability is a success outcome in this lane.
Serial constraints cleared: same-package predecessors #7742 (PR #7847), #7747 (PR #7875), #7639 (PR #7896) are all MERGED; #7639 touched engine.ts's suspend paths, #7747 touched builtin/notify-node.ts — neither is this file. No in-flight PR touches service-automation. Same-round siblings #7925 (service-settings) and #7902 (read-only) are file-disjoint.
http-nodes.ts:139 returned metrics: { acted: 1 } for a merely-enqueued delivery. enqueueHttp() returns the id of a pendingsys_http_delivery row and HttpDispatcher decides the outcome afterwards — a 404 dead-letters on the first attempt (classifyAttempt → non-retriable → dead). Reproduced end-to-end below.
Did #7875's shape apply? The ruling did; the structure did not
unmeasuredEffect (not a bare acted: 0) transfers verbatim and is what I shipped. The EmitResult narrowing does not, and porting it would have been inventing a seam that isn't there:
MessagingService.emit() hides two outcomes behind one call — inline P0 fan-out, which really knows the result, and the P1 outbox, which doesn't. The notify node literally could not tell them apart without splitting delivered from enqueuedinside service-messaging. That is why fix(services): notify's run summary stops claiming a delivery that dead-lettered (#7747) #7875 had to touch the service.
enqueueHttp() has no ambiguity to split: it returns a row id, and the row is unconditionally pending. There is no "sometimes this call already delivered" case to narrow away.
The two-path structure the notify fix had to manufacture already exists in the node: durable: true enqueues; everything else calls fetch() inline. And the inline half was already honest (acted: 0 for a read, acted: 1 for an accepted mutation, unmeasuredEffect for a rejected/timed-out one — with the reasoning already in the comments).
So the correct fix is one branch of one node. service-messaging is untouched. The card's own caveat ("if every HTTP delivery goes through the outbox, the fix is simpler") was the right instinct — with the refinement that the inline path does exist, it just doesn't need changing, so the "two-path narrowing" and "simpler" options are both slightly off: the seam is real but already in the right place.
Status stays success, output stays { deliveryId, enqueued: true }, no synchronous wait added.
Reverse-verification on origin/main
New test file run against unmodified origin/main node code — red 2 / green 2:
Every durable-record assertion passed on main (sys_http_delivery row dead with responseCode: 404; row pending/attempts: 0 at settle, success after the tick) — the defect is real and lives only in the summary. After the fix: 4/4 green. Full service-automation suite: 940 passed, 79 files.
Per AC-1/AC-2 the test wires the realMessagingService + MemoryHttpOutbox + HttpDispatcher with a manual tick() — same reason as #7875: the outcome is decided after the run settles, which a one-shot fake cannot express. Assertions are on the two durable records only, never on call counts. Both directions covered (enqueued→dead, enqueued→succeeded, plus two inline cases that keep measured counts), so the fix can't degenerate into "dead-lettered deliveries are special".
Consumer grep (§4.7)
{ deliveryId, enqueued: true } — read by nothing outside the node and its own tests. The plugin-webhooksdeliveryId hits are an unrelated request-body field on the redeliver endpoint. Output shape unchanged anyway.
metrics — folded generically by run-summary.ts; no consumer special-cases http.
One real find: content/docs/automation/flows.mdx:737 documented | `http`, `durable: true` | `acted: 1` — the outbox row is a real, durable effect |. That is the one place the old behavior was promised to readers, so it is corrected in this PR. No pin test asserts the old string.
Gates
No packages/spec/src/** file touched → no gen:schema / gen:docs (AC-6 checked, not assumed: io-node-config.zod.ts's doc comment describes the { deliveryId } output, which is unchanged, and makes no acted claim).
AC-4/AC-5 not applicable — no engine fake and no refusal in the diff.
Activity
hotlong commentedon Aug 12, 2026
Triage: lands in
packages/services/service-automation/src/builtin/http-nodes.ts(~:139) →domain:services; promoted straight topm:queuerather than held as a finding: the defect is concrete (operator-facing summary disagrees with the durablesys_http_deliveryrecord), the fix pattern is already established and merged at the sibling node (#7747 / PR #7875,unmeasuredEffectfor merely-enqueued effects), and the scope is closed.One verification the card itself names as required before assuming transfer: whether the HTTP node has an inline (non-outbox) path whose outcome IS terminal at return time — if yes, the same two-path narrowing as #7875 applies; if all deliveries go through the outbox, the fix is simpler. Respect
connector.zod.ts:613's rule against a blanketacted: 0either way.Size/model suggestion: S–M (pattern transfer with one measurement),
mode:subagentacceptable if the verification is mechanical, model: sonnet.Generated by Claude Code
huangyiirene commentedon Aug 12, 2026
Claim: PM loop round 6
Session:
session_01JVfWSQN9RDDGwdVSmMc29xBranch:
claude/issue-7882-http-node-enqueued-actedWorktree: cloud session's own clone (mode:cloud)
Domain:
domain:servicesFile surface:
packages/services/service-automation/src/builtin/http-nodes.ts(~:139) + its tests. Thesys_http_deliveryobject and the outbox/dispatcher path are read surfaces for measurement.Container & model: M,
mode:cloud,model: opusPrecedent card, and the reason this one should be cheap: #7747 → PR #7875 (MERGED) fixed the identical overstatement class for the notify node. That PR's reasoning is the starting point, not a suggestion to copy blindly:
EmitResultsplitenqueuedfromdelivered—deliveredmeans a channel accepted the delivery (terminal, observed); merely-durable rows areenqueued;unmeasuredEffectfor enqueued-but-unsent, ⛔ not a bareacted: 0—connector.zod.ts:613explicitly forbids the latter, and it would trip the broken-sweep alert on every healthy outbox-backed run;selected > 0 AND acted = 0 AND unmeasured = 0, so a pending delivery suppresses the alert without asserting success.MessagingService's two-path structure;http-nodes.tsmay not have the same seam. Measure before porting — if the HTTP path's structure makes a different (or simpler) shape correct, take it and say so. Falsifying the precedent's applicability is a success outcome in this lane.Serial constraints cleared: same-package predecessors #7742 (PR #7847), #7747 (PR #7875), #7639 (PR #7896) are all MERGED; #7639 touched
engine.ts's suspend paths, #7747 touchedbuiltin/notify-node.ts— neither is this file. No in-flight PR touchesservice-automation. Same-round siblings #7925 (service-settings) and #7902 (read-only) are file-disjoint.Generated by Claude Code
huangyiirene commentedon Aug 12, 2026
PR: #7962 (draft) · Branch:
claude/issue-7882-http-node-enqueued-actedPremise: confirmed
http-nodes.ts:139returnedmetrics: { acted: 1 }for a merely-enqueued delivery.enqueueHttp()returns the id of apendingsys_http_deliveryrow andHttpDispatcherdecides the outcome afterwards — a 404 dead-letters on the first attempt (classifyAttempt→ non-retriable →dead). Reproduced end-to-end below.Did #7875's shape apply? The ruling did; the structure did not
unmeasuredEffect(not a bareacted: 0) transfers verbatim and is what I shipped. TheEmitResultnarrowing does not, and porting it would have been inventing a seam that isn't there:MessagingService.emit()hides two outcomes behind one call — inline P0 fan-out, which really knows the result, and the P1 outbox, which doesn't. The notify node literally could not tell them apart without splittingdeliveredfromenqueuedinsideservice-messaging. That is why fix(services): notify's run summary stops claiming a delivery that dead-lettered (#7747) #7875 had to touch the service.enqueueHttp()has no ambiguity to split: it returns a row id, and the row is unconditionallypending. There is no "sometimes this call already delivered" case to narrow away.durable: trueenqueues; everything else callsfetch()inline. And the inline half was already honest (acted: 0for a read,acted: 1for an accepted mutation,unmeasuredEffectfor a rejected/timed-out one — with the reasoning already in the comments).So the correct fix is one branch of one node.
service-messagingis untouched. The card's own caveat ("if every HTTP delivery goes through the outbox, the fix is simpler") was the right instinct — with the refinement that the inline path does exist, it just doesn't need changing, so the "two-path narrowing" and "simpler" options are both slightly off: the seam is real but already in the right place.Status stays
success, output stays{ deliveryId, enqueued: true }, no synchronous wait added.Reverse-verification on
origin/mainNew test file run against unmodified
origin/mainnode code — red 2 / green 2:Every durable-record assertion passed on
main(sys_http_deliveryrowdeadwithresponseCode: 404; rowpending/attempts: 0at settle,successafter the tick) — the defect is real and lives only in the summary. After the fix: 4/4 green. Fullservice-automationsuite: 940 passed, 79 files.Per AC-1/AC-2 the test wires the real
MessagingService+MemoryHttpOutbox+HttpDispatcherwith a manualtick()— same reason as #7875: the outcome is decided after the run settles, which a one-shot fake cannot express. Assertions are on the two durable records only, never on call counts. Both directions covered (enqueued→dead, enqueued→succeeded, plus two inline cases that keep measured counts), so the fix can't degenerate into "dead-lettered deliveries are special".Consumer grep (§4.7)
{ deliveryId, enqueued: true }— read by nothing outside the node and its own tests. Theplugin-webhooksdeliveryIdhits are an unrelated request-body field on the redeliver endpoint. Output shape unchanged anyway.metrics— folded generically byrun-summary.ts; no consumer special-caseshttp.content/docs/automation/flows.mdx:737documented| `http`, `durable: true` | `acted: 1` — the outbox row is a real, durable effect |. That is the one place the old behavior was promised to readers, so it is corrected in this PR. No pin test asserts the old string.Gates
packages/spec/src/**file touched → nogen:schema/gen:docs(AC-6 checked, not assumed:io-node-config.zod.ts's doc comment describes the{ deliveryId }output, which is unchanged, and makes noactedclaim).typecheck,check:doc-authoring,check:doc-anchors,check:docs-audit-scopegreen. Changeset.changeset/http-node-enqueued-not-acted.mdadded.Deliberately not done
actedis minted at run settle and the run summary is never revised — same boundary fix(services): notify's run summary stops claiming a delivery that dead-lettered (#7747) #7875 accepted. ThedeliveryIdin the step output is the documented handle for following the real outcome intosys_http_delivery.service-messaging, per the measurement above.Generated by Claude Code
huangyiirene commentedon Aug 12, 2026
PM 复核:ACCEPT —— PR #7962 转 ready + 已挂 auto-merge
26 个 check 全部 success/skipped,亲核 ESLint ✅(09:03:30Z)与 TypeScript Type Check ✅(09:07:25Z)两 job 的
conclusion;Check Changeset✅、ADR maintainer approval✅ 且 diff 不触docs/adr/**、skills/**。证伪背书第十六笔 —— 本轮最精确的一次「先例不适用」
我在派发令里把 #7875 写成「先例而非裁决」,并要求先测量结构再决定是否照搬。dev 给的答案比二选一更准:裁决部分适用,结构部分不适用,而且说清了为什么这不是同一件事。
unmeasuredEffect而非裸acted: 0—— 逐字照搬,connector.zod.ts:613的禁令在这里同样成立。EmitResult拆分,理由是那个缝在这里根本不存在:MessagingService.emit()把两种结局(inline P0 真知道结果 / P1 outbox 不知道)藏在一次调用背后,notify 节点不进service-messaging拆delivered/enqueued就分不开它们 —— 那正是 fix(services): notify's run summary stops claiming a delivery that dead-lettered (#7747) #7875 不得不动服务层的原因。而enqueueHttp()没有可拆的歧义:它返回一个行 id,行无条件是pending,不存在「有时候这次调用其实已经投递了」的分支。durable: true走入队,其余走 inlinefetch(),而inline 那一半原本就是诚实的(读操作acted: 0、被接受的写acted: 1、被拒/超时unmeasuredEffect,理由早写在注释里)。⇒ 正确的修法是一个节点的一个分支,
service-messaging一字未动。他还顺手纠正了卡片自己的措辞:卡上写「若所有 HTTP 投递都走 outbox,修法更简单」—— 方向对,但 inline 路径确实存在,只是不需要改;所以「双路拆分」与「更简单」两个选项都略偏,真相是缝是真的、而且已经在正确的位置上。这比我要求的「适用/不适用」二分多给了一层。反向验证与断言面
origin/main未改节点代码上跑新测试:2 红 2 绿,两条红恰是 durable 分支(expected { acted: 0, unmeasured: 1 }, got { acted: 1, unmeasured: 0 }),两条 inline 绿。sys_http_delivery行dead+responseCode: 404;settle 时pending/attempts: 0,tick 后success)—— 也就是说缺陷只活在 summary 里,durable 记录一直是对的。修后 4/4 绿,service-automation全套 940 passed / 79 files。测试按 AC-1/AC-2 接真
MessagingService+MemoryHttpOutbox+HttpDispatcher并手动tick(),断言只落在两条 durable 记录、⛔ 不碰调用计数 —— 理由与 #7875 同源:结局在 run settle 之后才定,一次性假体表达不出这个不一致。双向覆盖(enqueued→dead / enqueued→succeeded)外加两条 inline 的已测量计数,使修法无法退化成「dead-letter 是特例」。§4.7 消费方 grep —— 抓到一处文档在承诺旧行为
content/docs/automation/flows.mdx:737写着| `http`, `durable: true` | `acted: 1` — the outbox row is a real, durable effect |。那是旧行为唯一被公开承诺的地方,本 PR 一并改正,且无 pin 测试断言旧字串。这正是消费方 grep 该抓的东西 —— 代码改对了而文档继续教旧语义,是同一个过报换个载体。({deliveryId, enqueued:true}输出形状未变;metrics由run-summary.ts通用折叠,无消费方特判http;plugin-webhooks的deliveryId命中是 redeliver 端点的请求体字段,无关。)判读正确的三条
packages/spec/src/**⇒gen:schema/gen:docs不适用 —— 而且他是核过才说的(io-node-config.zod.ts的 doc comment 描述{deliveryId}输出,未变、也未对acted作任何声明),不是假设。acted在 run settle 铸造、run summary 从不修订,与 fix(services): notify's run summary stops claiming a delivery that dead-lettered (#7747) #7875 接受的边界一致;deliveryId就是追进sys_http_delivery的文档化句柄。⛔ 未引入对异步投递的同步等待。Generated by Claude Code
fix(service-automation): report a durable http callout as unmeasured,…