Skip to content

fix(channels): make nack callbacks idempotent#104919

Merged
steipete merged 2 commits into
openclaw:mainfrom
Glucksberg:fix/channel-nack-idempotency-104903
Jul 12, 2026
Merged

fix(channels): make nack callbacks idempotent#104919
steipete merged 2 commits into
openclaw:mainfrom
Glucksberg:fix/channel-nack-idempotency-104903

Conversation

@Glucksberg

@Glucksberg Glucksberg commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

What Problem This Solves

Repeated or overlapping calls to a channel message receive context's nack() could invoke onNack more than once. That can duplicate failure handling when a receive pipeline revisits an already-failed stage or concurrent consumers report the same failure.

Closes #104903.

Why This Change Was Made

Keep one in-flight nack callback on the shared receive context. Completed calls return immediately, overlapping calls await the same callback, and a rejected callback clears the in-flight slot so a later retry remains possible. The existing ack-to-nack transition is unchanged.

The focused tests cover sequential idempotency, overlapping callback coalescing, retained first-error semantics, and retry after a rejected callback.

User Impact

Channel receive pipelines no longer repeat nack side effects for one message context, including when nack requests overlap. Transient nack callback failures can still be retried.

Evidence

  • Blacksmith Testbox tbx_01kxbkevswvgdcwn72gx7azk77 / Actions 29200690926: 18 lifecycle tests and 8 Telegram tracker tests passed
  • Earlier Testbox tbx_01kxb81fbdgwmxr5ywkmqvyy99 / Actions 29194361519: 17 lifecycle tests and 8 Telegram tracker tests passed
  • pnpm exec oxfmt --check --threads=1 src/channels/message/receive.ts src/channels/message/lifecycle.test.ts
  • node scripts/run-oxlint.mjs src/channels/message/receive.ts src/channels/message/lifecycle.test.ts
  • git diff --check
  • Fresh whole-branch autoreview: clean, confidence 0.93
  • Fix-only autoreview: clean, confidence 0.96

Real behavior proof

Behavior addressed: Completed sequential nack() calls repeated onNack; overlapping calls could also run the callback concurrently.

Real environment tested: Node.js 24 directly imported the production src/channels/message/receive.ts module through the repository's tsx loader. The runtime harness used fresh receive contexts and node:assert/strict; it did not mock createMessageReceiveContext.

Observed result after fix: A completed nack callback runs once; overlapping calls share one callback; a callback that rejects leaves the context retryable; the first nack error remains authoritative.

What was not tested: Live external-channel delivery. The contract is transport-independent and is covered at the shared receive-context boundary plus the Telegram integration tracker.

@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. labels Jul 12, 2026
@clawsweeper

clawsweeper Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed July 12, 2026, 7:39 AM ET / 11:39 UTC.

Summary
The PR makes completed channel-message nack() callbacks idempotent while preserving retry after callback rejection, with focused lifecycle regression tests.

PR surface: Source +4, Tests +25. Total +29 across 2 files.

Reproducibility: yes. Current-main source and the issue’s direct two-call reproduction establish a high-confidence failing path, while the PR supplies production-module after-fix output.

Review metrics: none identified.

Root-cause cluster
Relationship: fixed_by_candidate
Canonical: #104903
Summary: This PR is the narrowest viable fix candidate for a canonical issue with two competing implementation PRs.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge readiness
Overall: 🦞 diamond lobster
Proof: 🦞 diamond lobster
Patch quality: 🦞 diamond lobster
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Risk before merge

  • [P1] Three open contributor PRs target the same canonical issue; maintainers should land only one implementation and close the redundant branches afterward.

Maintainer options:

  1. Decide the mitigation before merge
    Keep one minimal nacked-only guard in the shared receive helper with focused lifecycle coverage, preserving ack-to-nack transitions and rejected-callback retries without adding a permanent one-off proof script.
  2. Pause or close
    Do not merge this PR until maintainers decide whether the risk is worth taking.

Next step before merge

  • [P2] No code repair is needed; the assigned feature owner should select this branch as canonical and resolve the duplicate PR cluster.

Maintainer decision needed

Security
Cleared: The patch changes only an in-memory callback lifecycle guard and focused tests, with no dependency, secret, permission, network, artifact, or code-execution changes.

Review details

Best possible solution:

Keep one minimal nacked-only guard in the shared receive helper with focused lifecycle coverage, preserving ack-to-nack transitions and rejected-callback retries without adding a permanent one-off proof script.

Do we have a high-confidence way to reproduce the issue?

Yes. Current-main source and the issue’s direct two-call reproduction establish a high-confidence failing path, while the PR supplies production-module after-fix output.

Is this the best way to solve the issue?

Yes. The shared receive context owns this invariant, and guarding only the completed nacked state is the narrowest maintainable fix that preserves the established transition and retry contracts.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against cbe6d9d3366b.

Label changes

Label justifications:

  • P2: This fixes a bounded shared-channel callback lifecycle defect with limited blast radius.
  • rating: 🦞 diamond lobster: Overall readiness is 🦞 diamond lobster; proof is 🦞 diamond lobster and patch quality is 🦞 diamond lobster.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR body contains after-fix terminal output from the production receive module showing one callback after duplicate completed nacks and a successful retry after a rejected callback.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body contains after-fix terminal output from the production receive module showing one callback after duplicate completed nacks and a successful retry after a rejected callback.
Evidence reviewed

PR surface:

Source +4, Tests +25. Total +29 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 4 0 +4
Tests 1 25 0 +25
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 29 0 +29

What I checked:

Likely related people:

  • steipete: Peter Steinberger introduced the shared channel lifecycle SDK in commit 8bfabd6, authored every recorded follow-up on the receive-helper path, and is assigned to this PR. (role: feature owner and current assignee; confidence: high; commits: 8bfabd6bb139, 1e7510ae103e, 7d3f1963d30c; files: src/channels/message/receive.ts, src/channels/message/lifecycle.test.ts, src/plugin-sdk/channel-outbound.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.
Review history (5 earlier review cycles)
  • reviewed 2026-07-12T03:11:09.286Z sha 952fdff :: needs real behavior proof before merge. :: none
  • reviewed 2026-07-12T03:29:39.668Z sha 57eb329 :: needs maintainer review before merge. :: none
  • reviewed 2026-07-12T03:49:15.546Z sha 4583b9c :: needs maintainer review before merge. :: none
  • reviewed 2026-07-12T03:55:14.770Z sha 4583b9c :: needs maintainer review before merge. :: none
  • reviewed 2026-07-12T06:51:56.678Z sha 2704505 :: needs maintainer review before merge. :: none

@Glucksberg
Glucksberg force-pushed the fix/channel-nack-idempotency-104903 branch from 952fdff to 57eb329 Compare July 12, 2026 03:20
@Glucksberg

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

The PR body now includes a redacted production-module runtime transcript on rebased commit 57eb329d9898c73b03ffef143dc73b36d3f4a2fc, covering both completed sequential nack idempotency and retry after callback rejection.

@clawsweeper

clawsweeper Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

Re-review progress:

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jul 12, 2026
@Glucksberg
Glucksberg marked this pull request as ready for review July 12, 2026 03:32
@Glucksberg
Glucksberg force-pushed the fix/channel-nack-idempotency-104903 branch from 57eb329 to 4583b9c Compare July 12, 2026 03:39
@clawsweeper clawsweeper Bot removed proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. labels Jul 12, 2026
@Glucksberg

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

Re-review progress:

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. labels Jul 12, 2026
@Glucksberg
Glucksberg force-pushed the fix/channel-nack-idempotency-104903 branch 3 times, most recently from 6d7e0f3 to 2704505 Compare July 12, 2026 06:41
Glucksberg added a commit to Glucksberg/OpenClaw that referenced this pull request Jul 12, 2026
@steipete steipete self-assigned this Jul 12, 2026
@Glucksberg
Glucksberg force-pushed the fix/channel-nack-idempotency-104903 branch from 2704505 to 5fe0d46 Compare July 12, 2026 11:28
@steipete
steipete force-pushed the fix/channel-nack-idempotency-104903 branch from 5fe0d46 to 8dc312a Compare July 12, 2026 16:50
@steipete
steipete force-pushed the fix/channel-nack-idempotency-104903 branch from 8dc312a to fde3084 Compare July 12, 2026 16:58
@steipete
steipete force-pushed the fix/channel-nack-idempotency-104903 branch from fde3084 to d780ed0 Compare July 12, 2026 17:03
@steipete

Copy link
Copy Markdown
Contributor

Land-ready maintainer proof:

  • Refactored the lifecycle guard so completed nack() calls return immediately, overlapping calls share one in-flight rejection, rejected attempts clear for retry, and the first successful rejection error remains authoritative.
  • Regression coverage includes sequential duplicates, concurrent overlap, retry after failure, and the existing ack-to-nack transition.
  • Blacksmith Testbox tbx_01kxbkevswvgdcwn72gx7azk77, Actions 29200690926: channel lifecycle 18/18 and Telegram delivery tracker 8/8 passed.
  • Earlier focused Testbox tbx_01kxb81fbdgwmxr5ywkmqvyy99, Actions 29194361519, also passed the affected channel lifecycle surface.
  • Fresh fix-only autoreview: clean, correctness 0.96. Fresh whole-branch autoreview: clean, correctness 0.93.
  • Exact-head CI 29201293283 passed on d780ed057e2c0bf79d499d9add40dea730fa6c73 after rebasing over current main's unrelated QA-Lab type fix.

Known gap: no live external-channel delivery was needed for this transport-independent lifecycle invariant; the shared lifecycle tests plus Telegram adapter regression cover the affected boundary.

@steipete
steipete merged commit 02d307e into openclaw:main Jul 12, 2026
103 checks passed
@steipete

Copy link
Copy Markdown
Contributor

Merged via squash.

github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jul 13, 2026
* fix(channels): make nack callbacks idempotent

* fix(channels): coalesce overlapping nack callbacks

---------

Co-authored-by: Peter Steinberger <[email protected]>
RomneyDa added a commit that referenced this pull request Jul 22, 2026
* fix: gate diagnostics command to owners

(cherry picked from commit 170bf72)

* fix(agent): replace self-wait with deferred release in retained-lock abort cleanup (#96100)

* fix(agent): wait for retained session write before releasing held lock on abort

* fix(agent): replace self-wait with deferred release in retained-lock abort cleanup

* fix(test): reject fallback acquire with SessionWriteLockTimeoutError in active-scope cleanup test

* fix(agent): trim retained-lock comments

Signed-off-by: sallyom <[email protected]>

---------

Signed-off-by: sallyom <[email protected]>
Co-authored-by: sallyom <[email protected]>
(cherry picked from commit 0a042f6)

* fix(gateway): resume channel after pending task recovery

(cherry picked from commit 6039da3)

* fix(gateway): resume channel after pending task recovery

(cherry picked from commit ecd29fe)

* fix(outbound): ignore empty delivery receipts (#79811)

(cherry picked from commit 9a735be)

* fix(agents): guard delivery-evidence attachment recursion against cycles (#97041)

* fix(agents): guard delivery-evidence attachment recursion against cycles

* fix(agents): guard delivery-evidence attachment recursion against cycles

* fix(agents): guard delivery-evidence attachment recursion against cycles

---------

Co-authored-by: Pick-cat <[email protected]>
Co-authored-by: Vincent Koc <[email protected]>
(cherry picked from commit 4985671)

* fix(opencode-go): re-arm idle timer on block-boundary events to prevent false stalled-stream abort (#97128)

* fix(opencode-go): re-arm idle timer on block-boundary events to prevent false stalled-stream abort

When the opencode-go model finalizes a tool call and deliberates before
the next one, the provider emits real block-boundary SSE events
(text_end, thinking_end, toolcall_start, toolcall_end) that prove the
socket is alive, but the watchdog's isProviderProgressEvent only
returned true for token deltas (text_delta, thinking_delta,
toolcall_delta). This caused the idle timer to fire and falsely abort a
live stream, replacing a completed answer with a stalled error and
dropping the provider's real done event.

Fix: include block-boundary events in isProviderProgressEvent so the
idle timer is re-armed on any forward-progress provider event.
text_start and thinking_start are intentionally excluded because they
are synthetic preamble events that should not shorten the first-event
window.

Closes #96518

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* test(opencode-go): satisfy lint in stream regression

* test(opencode-go): satisfy lint in stream regression

* test(opencode-go): satisfy lint in stream regression

---------

Co-authored-by: Claude Opus 4.8 <[email protected]>
Co-authored-by: Vincent Koc <[email protected]>
(cherry picked from commit 552ec2b)

* fix(model-fallback): don't rethrow provider-side AbortErrors as user cancellations (#90908)

* fix(model-fallback): don't rethrow provider-side AbortErrors as user cancellations

When the LLM API closes the connection mid-stream, the fetch layer
surfaces AbortError("This operation was aborted") with no external
abort signal triggered. The old guard `shouldRethrowAbort()` returned
false for these errors (because isTimeoutError matched the message),
so they fell through to the fallback loop but were never retried —
the error propagated up and produced SILENT_REPLY_TOKEN in group
sessions, permanently silencing the topic.

Replace the guard with a direct check: only rethrow AbortError when
the external abort signal is actually set (user/gateway cancellation).
Provider-side AbortErrors without an external signal now fall through
to the next fallback candidate, giving the system a chance to recover.

* fix(cron): forward abort signal into runWithModelFallback

Thread the cron executor's abort signal into the shared
runWithModelFallback call so that cron timeouts and cancellations
stop the fallback chain instead of retrying with the next candidate.

Previously, the run callback checked params.abortSignal?.aborted and
threw, but runWithModelFallback itself had no signal — so the new
guard in model-fallback.ts could not distinguish a caller abort from
a provider-side AbortError and would retry silently.

Also adds a focused regression test verifying the signal is forwarded.

---------

Co-authored-by: Shengting Xie <[email protected]>
Co-authored-by: yayu <[email protected]>
(cherry picked from commit 98ed83f)

* fix(browser): block node routes when sandbox host control is disabled (#97958)

(cherry picked from commit 2cf765f)

* fix(exec): bind Windows allowlist execution path (#98260)

* fix(exec): bind windows allowlist execution path

* fix(exec): add windows shadow execution proof

* fix(exec): preserve wildcard allowlist behavior

* fix(exec): correct blocked plan test fixture

(cherry picked from commit 3811001)

* fix(mcp): suppress unhandled error on stderr pipe in stdio transport (#99803)

* fix(mcp): suppress unhandled error on stderr pipe in stdio transport

When child.stderr is piped to stderrStream without an error
handler, a stream-level error (EPIPE, I/O failure) crashes the
process. Add a noop error handler before the pipe, consistent
with the error handlers already present on stdin and stdout.

Co-Authored-By: Claude <[email protected]>

* test(mcp): add regression test for stderr pipe error suppression

Co-Authored-By: Claude <[email protected]>

* fix(mcp): report stderr stream errors

* fix(mcp): report stderr stream errors

---------

Co-authored-by: Claude <[email protected]>
Co-authored-by: Vincent Koc <[email protected]>
(cherry picked from commit 1b84316)

* Harden macOS SQLite WAL checkpoints (#99067)

(cherry picked from commit f7f1be2)

* fix(secrets): suppress unhandled stdout/stderr stream errors in exec resolver (#100521)

* fix(secrets): suppress unhandled stdout/stderr stream errors in exec resolver

* proof(secrets): add real behavior proof script for exec resolver stream error catch

* proof(secrets): replace wrapper with real exec resolver stream error proof

* style: apply oxfmt to changed files

(cherry picked from commit c9a0783)

* fix(agents): retry transient filesystem races when reading workspace bootstrap files (#100910)

* fix(agents): retry transient filesystem races when reading workspace bootstrap files

* fix(agents): retry transient boundary resolution

---------

Co-authored-by: Vincent Koc <[email protected]>
(cherry picked from commit f36d170)

* fix(gateway): finish plugin HTTP responses after post-header failures (#102125)

* fix(gateway): finish plugin HTTP responses after post-header failures

* test(gateway): satisfy plugin HTTP regression lint

* fix(gateway): skip ending destroyed plugin responses

---------

Co-authored-by: Peter Steinberger <[email protected]>
(cherry picked from commit 240d350)

* fix(gateway): validate exact custom browser origins (#38290)

Co-authored-by: Peter Steinberger <[email protected]>
(cherry picked from commit fa0349a)

* fix: block unspecified trusted DNS targets (#103075)

(cherry picked from commit c70f3d0)

* fix(channels): make nack callbacks idempotent (#104919)

* fix(channels): make nack callbacks idempotent

* fix(channels): coalesce overlapping nack callbacks

---------

Co-authored-by: Peter Steinberger <[email protected]>
(cherry picked from commit 02d307e)

* fix(channels): prevent base URL credentials in status output (#107754)

* fix(channels): redact credentials in account URLs

* fix(channels): sanitize final status summaries

(cherry picked from commit 210340f)

* fix(channels): prevent lifecycle listener buildup (#109108)

(cherry picked from commit 0e1fad7)

* fix(sandbox): use Buffer.byteLength for env var value size limit (#105017)

* fix(sandbox): use Buffer.byteLength for env var value size limit

validateEnvVarValue checked value.length (UTF-16 code units) against
the 32768-byte limit, so multi-byte CJK values like "值".repeat(11000)
passed the check despite exceeding 33 KB in UTF-8. Switch to
Buffer.byteLength(value, "utf8") so the limit matches the actual byte
count the OS and child processes see.

* test(sandbox): simplify env byte-limit coverage

Co-authored-by: 唐梓夷0668001293 <[email protected]>

---------

Co-authored-by: Peter Steinberger <[email protected]>
(cherry picked from commit 84fb48c)

* fix(gateway): guard process.kill ESRCH race in signalVerifiedGatewayPidSync (#109590)

* fix(gateway): guard process.kill ESRCH race in signalVerifiedGatewayPidSync

A verified gateway process can exit between the argv validation check and
the process.kill call, causing an unhandled ESRCH error. Wrap the kill in
try-catch and silently swallow ESRCH (process already gone = signal
already delivered).

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* docs(gateway): explain ESRCH signal race

Co-authored-by: 丁宇婷0668001435 <[email protected]>

---------

Co-authored-by: Claude Sonnet 4.6 <[email protected]>
Co-authored-by: Peter Steinberger <[email protected]>
(cherry picked from commit 853b1a8)

* fix(litellm): guard loopback hostname auto-allow with isIP to prevent DNS SSRF bypass (#110693)

* fix(litellm): guard loopback hostname auto-allow with isIP to prevent DNS bypass

The isAutoAllowedLitellmHostname helper auto-enables private-network access
for loopback-style hosts. Before this fix, lowered.startsWith("127.")
matched DNS hostnames like 127.evil.com, letting remote endpoints bypass
the explicit allowPrivateNetwork opt-in — a SSRF risk.

Add isIP(host)===4 guard so only literal IPv4 loopback addresses qualify.
Same canonical pattern as extensions/slack/src/monitor/relay-source.ts:271
and the codex loopback fix.

Co-Authored-By: Claude <[email protected]>

* test(litellm): cover loopback endpoint policy

---------

Co-authored-by: Claude <[email protected]>
Co-authored-by: Peter Steinberger <[email protected]>
(cherry picked from commit 3d03b60)

* fix(discord): sustained gateway bursts stop growing memory (#110954)

* fix(discord): sustained gateway bursts stop growing memory

* fix(discord): contain gateway queue overflow

* fix(discord): drop oldest saturated gateway sends

Co-authored-by: 张贵萍0668001030 <[email protected]>

* fix(discord): surface gateway overflow warnings

Co-authored-by: 张贵萍0668001030 <[email protected]>

---------

Co-authored-by: Peter Steinberger <[email protected]>
(cherry picked from commit 69aeba9)

* fix(gateway): bound busy channel health by real run age (#103793)

* fix(gateway): bound busy channel health by real run age

The channel health policy treats a channel as healthy-busy even while
disconnected, bounded only by a 25 minute stale ceiling measured from
lastRunActivityAt. The run-state heartbeat refreshes lastRunActivityAt
every 60 seconds for as long as any run is active, so a run that hangs
forever (for example a send blocking on a dead socket after the
transport already reported connected:false) keeps that timestamp fresh
and the stuck ceiling is never reached. The account is then reported
healthy forever by the health monitor, readiness probe, and health CLI,
and no restart ever fires.

createRunStateMachine now tracks each in-flight run's start time keyed by
an opaque run handle and publishes the oldest still-active run's start as
activeRunStartedAt. The health policy busy override keys its ceiling off
the real run age, so a run stuck longer than the threshold reports stuck
and the monitor can restart it. Because the reported start is the oldest
active run and advances to the next-oldest as runs complete, a channel
churning through many short overlapping runs (activeRuns above 1 across
concurrent queue keys) stays healthy; only a genuinely hung run breaches
the ceiling. Short and active runs stay healthy and the existing
lastRunActivityAt fallback is preserved for snapshots without a start
time.

* fix(channels): retain run-state callback compatibility

Keep the released zero-argument onRunEnd callback source-compatible while allowing internal queue callers to pass a run handle for exact concurrent-run accounting. The compatibility path closes the oldest active run, preserving existing lifecycle behavior for consumers that do not use handles.

* fix(channels): keep anonymous runs out of age tracking

The zero-argument lifecycle callbacks cannot identify which concurrent run completed, so they must not update the identity-sensitive run start used by channel health. Keep their busy count separately and reserve exact start tracking for the shared queue's handle-aware lifecycle path.

* fix(channels): keep tracked runs internal

Keep the public run-state lifecycle callbacks unchanged. The channel queue now owns opaque run identity and augments its status updates with the oldest active queue run, so implementation details do not expand the SDK surface.

* fix(channels): type queue run start status

Keep activeRunStartedAt in the internal status patch type so the queue can publish its private tracked-run age through the existing status sink.

* fix(channels): wrap isActive to satisfy unbound-method lint

* fix(gateway): gate busy run-age ceiling on disconnected transport

(cherry picked from commit 18b79d9)

* fix(deps): update fast-uri past advisory

(cherry picked from commit 1be9db0)

* fix(release): adapt maintenance-line hardening

Backport/adapt 18ec9ce, dea1fe1, 7f32b6c, 1da345e, 931ac3e, 89780d5, and c0d99ed for the 2026.6 extended-stable maintenance line.

* fix(deps): bump protobufjs to 7.6.5

Backport-adapted from a230f74.

* test(gateway): cover bounded macOS process probe

* chore(release): prepare 2026.6.34

* test(dotenv): share path override environment assertions

* fix(release): resolve 2026.6.34 CI blockers

---------

Signed-off-by: sallyom <[email protected]>
Co-authored-by: joshavant <[email protected]>
Co-authored-by: Peter Lee <[email protected]>
Co-authored-by: sallyom <[email protected]>
Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
Co-authored-by: Liu Wenyu <[email protected]>
Co-authored-by: pick-cat <[email protected]>
Co-authored-by: Pick-cat <[email protected]>
Co-authored-by: Vincent Koc <[email protected]>
Co-authored-by: weiqinl <[email protected]>
Co-authored-by: Claude Opus 4.8 <[email protected]>
Co-authored-by: shengting <[email protected]>
Co-authored-by: Shengting Xie <[email protected]>
Co-authored-by: yayu <[email protected]>
Co-authored-by: Agustin Rivera <[email protected]>
Co-authored-by: cxbAsDev <[email protected]>
Co-authored-by: ooiuuii <[email protected]>
Co-authored-by: Masato Hoshino <[email protected]>
Co-authored-by: Vincent Koc <[email protected]>
Co-authored-by: mushuiyu886 <[email protected]>
Co-authored-by: Peter Steinberger <[email protected]>
Co-authored-by: Bruno Wowk (Volky) <[email protected]>
Co-authored-by: Pavan Kumar Gondhi <[email protected]>
Co-authored-by: Glucksberg <[email protected]>
Co-authored-by: xingzhou <[email protected]>
Co-authored-by: tzy-17 <[email protected]>
Co-authored-by: krissding <[email protected]>
Co-authored-by: lsr911 <[email protected]>
Co-authored-by: Yuval Dinodia <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. size: S status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(channels): nack() 缺少幂等性保护,重复调用会多次触发 onNack 回调

2 participants