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
fix: round-2 review — two fix regressions, publish blockers, form bodies
Three of these were introduced by the previous round's fixes, which is
the point of reviewing a diff rather than a codebase.
A. The concurrency slot leaked when the runner threw. Wrapping dispatch
in a try/catch made runner throws survivable, which exposed an
asymmetry that had been unreachable: `activeRuns` was incremented
before `runner.start()` and decremented at five separate later points,
none of them a throw. After maxConcurrentRuns such throws the route
stopped accepting anything, permanently, with nothing recorded. The
slot now has one owner and is released in `finally` unless a
background waiter takes it.
B. Destroying the request stream on a body-read failure killed the socket
the response shares, so a planned 413 became a connection reset —
which Hookdeck retries, re-buffering the oversized body each time,
while our own counter logged a cancellation that never reached the
wire. Only the timeout case destroys now, which is the only one still
reading.
C. The config-error fallback route trusted the raw basePath. Since the
invalid config may BE the basePath, `"/"` — rejected by the parser
precisely because a prefix route there captures every Gateway request
— would have been registered as a catch-all answering 503. Sanitised.
D. A permanently un-retryable orphan came back on every boot: the row was
never settled when retryEvent failed, so it was re-found forever, one
duplicate dead-letter per boot, and consumed the recovery budget ahead
of events that could actually be recovered. A 404 is retention and
settles; anything else may be transient and is left for the next boot.
Publishing: added LICENSE (MIT, as package.json and README already
claimed), a CI workflow, homepage/bugs, and a `files` field so npm ships
the plugin rather than the test suite. Genericised references to sibling
plugins that may not be public, and removed an internal connection name
from the smoke script — mutations there now require naming the connection
explicitly.
Also: form-encoded bodies are supported, since Twilio- and Slack-style
providers could otherwise never pass an ingress whose premise is "any
provider Hookdeck verifies"; a transport fault after the 202 re-queues or
dead-letters instead of only warning; agent turns document plainly that
they are fire-and-forget on the TaskFlow transport, with maxAgentRetries
warned about alongside sync; the shutdown budget is wired and shutdown no
longer pauses routes that no longer exist; a deliberate pause survives a
tunnel reconnect, where a shutdown pause is still lifted; pending
auto-resume timers are cancelled on service stop; disk-mode doctor no
longer reports the live Gateway's work as interrupted; and bulk replay
refuses in http mode rather than reporting success having matched
nothing.
One found by writing the tests: `stop()` registered its exit listener
after sending SIGTERM, so a child that exits promptly was missed and
teardown waited out the whole grace period before SIGKILLing a process
that had already gone.
586 tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: README.md
+10-5Lines changed: 10 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -97,7 +97,11 @@ You do not need to configure destination auth either. CLI destinations default t
97
97
|`maxConcurrent`|`4`| Local admission control. In CLI transport this is the **only** limit — CLI destinations carry no `rate_limit` field. |
98
98
|`busyRetryAfterSeconds`|`10`|`Retry-After` sent when deferring at capacity. |
99
99
|`deferAttemptLimit`|`5`| Deferrals of the same event before the short `Retry-After` is dropped and exponential backoff takes over. Capacity that has not recovered after this many attempts is not the transient condition a short interval assumes. |
100
-
|`dedupe.ttlHours`|`168`| Ledger retention. Must exceed Hookdeck's one-week retry ceiling. |
100
+
|`pause.onShutdown`|`true`| Pause the connection before stopping the listener, so events are held rather than discarded. |
101
+
|`pause.shutdownTimeoutMs`|`5000`| Budget for the whole teardown: pausing, draining and stopping children. |
102
+
|`catchUp.enabled`|`true`| After a reconnect, replay requests that arrived while nothing was listening. |
103
+
|`catchUp.minGapSeconds`|`30`| Below this, an outage is not worth a bulk replay. |
104
+
|`dedupe.ttlHours`|`168`| Ledger retention, matching Hookdeck's one-week retry ceiling. Raise it if you extend retries beyond a week. |
101
105
|`safety.allowRetryCancel`|`false`| See [Retry cancellation](#retry-cancellation). |
|`routes.<id>.path`|`/<id>`| Appended to `ingress.basePath`. Matched as a prefix — see below. |
@@ -261,7 +265,7 @@ Without `apiKey`, orphans are still detected, settled and dead-lettered — they
261
265
262
266
## Agent tools
263
267
264
-
Eight tools, matching the shared contract's five operator verbs plus two read tools an agent host benefits from more than a CLI does.
268
+
Eight tools: the shared contract's five operator verbs — `setup`, `status`, `pause`/`resume`, `replay`, `doctor` — plus three an agent host benefits from more than a CLI does. Two of those correlate what Hookdeck saw with what we did (`hookdeck_recent_deliveries`, `hookdeck_inspect_event`); the third, `hookdeck_issues`, is the dead-letter queue's own lifecycle.
265
269
266
270
| Tool | Answers |
267
271
|---|---|
@@ -341,8 +345,9 @@ Signature headers and resolved secrets are redacted from logs.
341
345
342
346
Not yet implemented:
343
347
344
-
-**No completion tracking for agent turns.**See [Agent turns](#agent-turns). Agent turns run through TaskFlow `run_task`, which exposes flow state rather than a completion signal, so `ackMode: "sync"`behaves as `async_retry`, and `deliver` and `lane` are recorded but not passed to the turn. Each is warned about at startup rather than failing quietly.
348
+
-**Agent turns are fire-and-forget.**They run through TaskFlow `run_task`, which exposes flow state rather than a completion signal, so the delivery is acknowledged as soon as the run *starts*. Concretely: `ackMode: "sync"`does not wait, `maxAgentRetries` never fires, and a crash mid-run is **not** re-queued by boot recovery — the ledger row is already `succeeded`. Run durability belongs to the flow record from that point, and Hookdeck's guarantee covers delivery rather than completion. `deliver` and `lane` are likewise recorded but not passed to the turn. Every one of these warns at startup rather than failing quietly. `taskflow` and `wake` dispatch are unaffected.
345
349
-**A signature authenticates the body, not the headers.** Hookdeck's HMAC covers the raw body only, with a project-level secret and no signed timestamp. So the event id and attempt count arrive unauthenticated, and a captured `(body, signature)` pair stays valid. Deduplication is what provides replay protection, an implausible attempt count is discarded rather than recorded, and provider verification at the Source is the layer that keeps unsigned traffic out in the first place.
350
+
-**Form-encoded and JSON bodies only.**`application/x-www-form-urlencoded` (Twilio, Slack) and `application/json` are parsed; anything else is rejected permanently.
346
351
-**List endpoints read the first page only.**`hookdeck_issues` and `hookdeck_recent_deliveries` report a real total from the count endpoint, but return one page of results.
347
352
348
353
## Development
@@ -353,11 +358,11 @@ npm test
353
358
npm run typecheck
354
359
```
355
360
356
-
559 tests, no Gateway or Hookdeck account required. Signature vectors are computed independently with `openssl`, `test/http-integration.test.ts` exercises the pipeline over a real socket including multi-byte UTF-8 and multi-chunk bodies, the store suites inject write failures at an exact call to prove the degradation rule, and `test/store-io.test.ts` runs against a real filesystem because that is the only place durability actually lives.
361
+
586 tests, no Gateway or Hookdeck account required. Signature vectors are computed independently with `openssl`, `test/http-integration.test.ts` exercises the pipeline over a real socket including multi-byte UTF-8 and multi-chunk bodies, the store suites inject write failures at an exact call to prove the degradation rule, and `test/store-io.test.ts` runs against a real filesystem because that is the only place durability actually lives.
357
362
358
363
## Shared reliability contract
359
364
360
-
This plugin conforms to a contract shared with the Hookdeck plugins for Hermes Agent and n8n, so that "what happens when the run fails" has the same answer in all three: the same verification rule, the same attempt-count deduplication, the same admission-control semantics, and the same operator verbs.
365
+
This plugin conforms to a contract shared across Hookdeck's agent-platform plugins, so that "what happens when the run fails" has the same answer in each: the same verification rule, the same attempt-count deduplication, the same admission-control semantics, and the same operator verbs.
361
366
362
367
Where this plugin adds something the contract does not require — retry cancellation, last-attempt dead-lettering — it defaults to off, so out-of-the-box wire behaviour matches its siblings.
Copy file name to clipboardExpand all lines: openclaw.plugin.json
+1-1Lines changed: 1 addition & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -515,7 +515,7 @@
515
515
"allowRetryCancel": {
516
516
"type": "boolean",
517
517
"default": false,
518
-
"description": "Permit 'Retry-After: -1' to cancel Hookdeck's automatic retries on permanently-invalid input. Off by default: with it off, wire behaviour matches the sibling Hermes and n8n plugins exactly."
518
+
"description": "Permit 'Retry-After: -1' to cancel Hookdeck's automatic retries on permanently-invalid input. Off by default: with it off, wire behaviour matches Hookdeck's other agent-platform plugins exactly."
0 commit comments