Skip to content

fix(agents): don't inject A2A turns into isolated-cron sessions_send (#92257)#92283

Merged
vincentkoc merged 7 commits into
openclaw:mainfrom
harjothkhara:fix/sessions-send-fire-and-forget-92257
Jul 2, 2026
Merged

fix(agents): don't inject A2A turns into isolated-cron sessions_send (#92257)#92283
vincentkoc merged 7 commits into
openclaw:mainfrom
harjothkhara:fix/sessions-send-fire-and-forget-92257

Conversation

@harjothkhara

@harjothkhara harjothkhara commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Fire-and-forget cross-session sessions_send ran the A2A ping-pong loop for every requester, and for an isolated cron requester its first iteration fed the target agent's reply back into the isolated cron session as a new turn, corrupting that run ([Bug]: sessions_send with announce delivery injects Argus reply context into isolated cron session, causing agent feedback loop #92257).
  • Narrow the skip to isolated-cron requesters only, detected by a :cron: session-key marker or cron channel (the same signal used by src/cron/isolated-agent/run.ts and src/agents/subagent-registry.ts), NOT by timeoutSeconds. Normal cross-session fire-and-forget keeps its intended ping-pong roundtrip.
  • Intended outcome: an isolated cron sessions_send only announces in the target; it never injects the target's reply back into the cron run.
  • Out of scope: any change to normal (non-cron) requester behavior, and any change to the external announce transport.
  • Success: cron requester is never stepped; normal requester is still stepped; target is still announced.
  • Reviewers should focus on the isIsolatedCronRequester gate and that it does not over-reach into normal requesters.

AI-assisted: this change was prepared with AI assistance. I have reviewed and understand the diff and own it. Happy to share prompt/session detail on request.

Linked context

Closes #92257

Related #91271 (open — edits sessions-send-tool.ts near this area; this PR adds an isIsolatedCronRequester gate around the existing maxPingPongTurns/baseline lines, so a textual rebase against whichever lands first is expected), #76490 (open — rewrites sessions-send-tool.a2a.ts to route A2A announce replies back to the requester; this PR does not touch a2a.ts), #83094 (open — touches nearby sessions-send files). None of these fixes the isolated-cron injection in #92257.

Was this requested by a maintainer or owner? No.

Real behavior proof (required for external PRs)

  • Behavior or issue addressed: Fire-and-forget cross-session sessions_send with announce delivery ([Bug]: sessions_send with announce delivery injects Argus reply context into isolated cron session, causing agent feedback loop #92257). When the requester is an isolated cron session, the A2A ping-pong loop's first iteration injected the target agent's reply back into the isolated cron session as a new turn — corrupting that run. The narrowed fix forces maxPingPongTurns: 0 only for an isolated-cron requester (key contains :cron: or channel cron), so the flow skips requester injection and only announces in the target; a normal requester keeps the full ping-pong.

  • Real environment tested: A real node v22.19.0 process loaded the committed runtime modules (runSessionsSendA2AFlow, runAgentStep, resolvePingPongTurns, readLatestAssistantReplySnapshot) directly from source via the tsx loader, driven through an in-process gateway double that maintains per-session message stores and dispatches the real agent/chat.history/agent.wait/send methods. The harness reproduces the exact committed gate from src/agents/tools/sessions-send-tool.tsisIsolatedCronRequester = requesterSessionKey?.includes(":cron:") || requesterChannel === "cron", then maxPingPongTurns: isIsolatedCronRequester ? 0 : maxPingPongTurns — and records every session key that actually receives an agent step. This is genuine runtime execution of the production code path. Two requester legs were driven against the branch: an isolated-cron requester agent:youtube:cron:bf05dd0f:run:b41f8df6 (channel cron) and a normal requester discord:group:req (channel discord), both to a different target agent:main:main. A BEFORE leg was captured by forcing the unfixed turn count for the same cron requester.

  • Exact steps or command run after this patch: From the worktree, with the real Node runtime, ran the harness once per leg — source ~/.nvm/nvm.sh && nvm use 22.19 && node --import tsx /tmp/proof-92257-narrow/harness.mjs /tmp/openclaw-impl-92257 "agent:youtube:cron:bf05dd0f:run:b41f8df6" cron for the isolated-cron leg and node --import tsx /tmp/proof-92257-narrow/harness.mjs /tmp/openclaw-impl-92257 "discord:group:req" discord for the normal-requester leg. Each invocation computes maxPingPongTurns via the exact committed gate, calls the real runSessionsSendA2AFlow for a fire-and-forget cross-session announce send, and records which session keys actually receive an agent step.

  • Evidence after fix (screenshot, recording, terminal capture, console output, redacted runtime log, linked artifact, or copied live output): Copied live console output from the real node runs.

    LEG A — isolated-cron requester, the bug (agent:youtube:cron:bf05dd0f:run:b41f8df6, channel cron):
    command: node --import tsx /tmp/proof-92257-narrow/harness.mjs /tmp/openclaw-impl-92257 "agent:youtube:cron:bf05dd0f:run:b41f8df6" cron

    [harness] mode=branch repo=openclaw-impl-92257
    [harness] requester=agent:youtube:cron:bf05dd0f:run:b41f8df6 channel=cron
    [harness] requester key contains ":cron:" = true
    [harness] isIsolatedCronRequester = true
    [harness] resolvePingPongTurns(default) = 5
    [harness] maxPingPongTurns passed to runSessionsSendA2AFlow = 0
    [result] agent steps executed (ordered):
               - command.ingress(announce) sessionKey=agent:main:main
    [result] REQUESTER (agent:youtube:cron:bf05dd0f:run:b41f8df6) stepped 0 time(s)  <-- injection signal
    [result] TARGET    (agent:main:main) stepped 1 time(s)
    [verdict] requester-injection (feedback loop) = NO (skipped)
    

    LEG B — normal requester, regression guard (discord:group:req, channel discord):
    command: node --import tsx /tmp/proof-92257-narrow/harness.mjs /tmp/openclaw-impl-92257 "discord:group:req" discord

    [harness] mode=branch repo=openclaw-impl-92257
    [harness] requester=discord:group:req channel=discord
    [harness] requester key contains ":cron:" = false
    [harness] isIsolatedCronRequester = false
    [harness] resolvePingPongTurns(default) = 5
    [harness] maxPingPongTurns passed to runSessionsSendA2AFlow = 5
    [result] agent steps executed (ordered):
               - gateway.agent sessionKey=discord:group:req
               - command.ingress(announce) sessionKey=agent:main:main
    [result] REQUESTER (discord:group:req) stepped 1 time(s)  <-- injection signal
    [result] TARGET    (agent:main:main) stepped 1 time(s)
    [verdict] requester-injection (feedback loop) = YES (ping-pong ran)
    
  • Observed result after fix: LEG A — the isolated-cron requester agent:youtube:cron:bf05dd0f:run:b41f8df6 receives 0 agent steps (the injection loop is skipped because the :cron: marker forces the turn count to 0), while the target agent:main:main still receives its single announce step. LEG B — the normal requester discord:group:req still receives 1 agent step (the configured turn count stays at 5 because the :cron: gate does not fire), and the target is still announced. The two legs together show the fix is narrow: it stops the requester injection for an isolated cron run only, and leaves the normal cross-session roundtrip untouched.

  • What was not tested: A full end-to-end run over a real external channel (Slack/Discord/Telegram) was not exercised — no live channel credentials are available, so the final announce transport hop was driven through the in-process gateway send double rather than a real network transport. The requester-injection behavior that [Bug]: sessions_send with announce delivery injects Argus reply context into isolated cron session, causing agent feedback loop #92257 fixes (the agent step into the requester session) is fully exercised at runtime above.

  • Proof limitations or environment constraints: Modules were loaded from TypeScript source via the tsx loader rather than a prebuilt dist; the source is what the build emits and runs in a real node v22.19.0 process. The gateway is an in-process double (real method dispatch over real per-session stores), not a networked gateway daemon.

  • Before evidence (optional but encouraged): Same isolated-cron requester with the unfixed turn count, captured via the harness's origin-main mode (node --import tsx /tmp/proof-92257-narrow/harness.mjs /tmp/openclaw-impl-92257 "agent:youtube:cron:bf05dd0f:run:b41f8df6" cron origin-main):

    [harness] mode=origin-main repo=openclaw-impl-92257
    [harness] requester=agent:youtube:cron:bf05dd0f:run:b41f8df6 channel=cron
    [harness] isIsolatedCronRequester = true
    [harness] maxPingPongTurns passed to runSessionsSendA2AFlow = 5
    [result] agent steps executed (ordered):
               - gateway.agent sessionKey=agent:youtube:cron:bf05dd0f:run:b41f8df6
               - command.ingress(announce) sessionKey=agent:main:main
    [result] REQUESTER (agent:youtube:cron:bf05dd0f:run:b41f8df6) stepped 1 time(s)  <-- injection signal
    [result] TARGET    (agent:main:main) stepped 1 time(s)
    [verdict] requester-injection (feedback loop) = YES (ping-pong ran)
    

Tests and validation

  • node scripts/run-vitest.mjs src/agents/openclaw-tools.sessions.test.ts src/agents/tools/sessions.test.ts src/agents/tools/sessions-send-tool.a2a.test.ts → 3 files, 91 cases green.
  • pnpm check:test-types (tsgo over the touched test files) → clean.
  • Regression coverage added: (1) an isolated-cron requester (:cron: key / cron channel) forwards maxPingPongTurns: 0 and the requester is never stepped while the target is still announced; (2) a normal discord:group:req requester keeps the configured maxPingPongTurns (not 0), pinning that the narrowing does not over-reach; (3) the pre-existing openclaw-tools.sessions.test.ts "supports fire-and-forget and wait" test (which asserts the normal-requester ping-pong) stays green.
  • What failed before: an earlier broad version of this fix gated on timeoutSeconds === 0 and made the normal-requester fire-and-forget skip its ping-pong, failing openclaw-tools.sessions.test.ts; narrowing the gate to isolated-cron requesters resolves that.

Risk checklist

  • Did user-visible behavior change? Yes — an isolated cron sessions_send announce no longer injects the target's reply back into the cron run. Normal requester behavior is unchanged.
  • Did config, environment, or migration behavior change? No.
  • Did security, auth, secrets, network, or tool execution behavior change? No.
  • Highest-risk area: the isIsolatedCronRequester detection could in principle be too broad/narrow. Mitigated: it reuses the established :cron:-key / cron-channel signal (src/agents/subagent-registry.ts, src/cron/isolated-agent/run.ts), and the normal-requester regression test + the two-leg runtime proof both confirm normal requesters are untouched.

Current review state

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: S triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels Jun 11, 2026
@clawsweeper

clawsweeper Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed July 1, 2026, 8:25 PM ET / 00:25 UTC.

Summary
This PR adds a canonical isolated-cron requester gate in sessions_send, passes zero A2A ping-pong turns for those requester sessions, captures the needed fire-and-forget baseline, and adds regression coverage.

PR surface: Source +18, Tests +211. Total +229 across 3 files.

Reproducibility: yes. at source level. Current main and v2026.6.11 pass positive A2A turns into a callee that can step the requester session, and the linked bug plus PR proof show the isolated-cron feedback-loop path.

Review metrics: 1 noteworthy metric.

  • A2A requester delivery gate: 1 conditional gate changed. The user-visible delivery behavior depends on the classifier that decides whether requester ping-pong runs for cron-run sessions.

Stored data model
Persistent data-model change detected: serialized state: src/agents/tools/sessions-send-tool.a2a.test.ts, serialized state: src/agents/tools/sessions.test.ts, unknown-data-model-change: src/agents/tools/sessions-send-tool.a2a.test.ts, unknown-data-model-change: src/agents/tools/sessions-send-tool.ts, unknown-data-model-change: src/agents/tools/sessions.test.ts. Confirm migration or upgrade compatibility proof before merge.

Root-cause cluster
Relationship: fixed_by_candidate
Canonical: #92257
Summary: This PR is the current open candidate fix for the isolated-cron sessions_send requester-injection bug; the earlier overlapping PR is closed unmerged, while nearby A2A work is adjacent or partial.

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.

Mantis proof suggestion
A live isolated-cron transport run would materially reduce the remaining message-delivery uncertainty before merge. A maintainer can ask Mantis to capture proof by posting this exact PR comment:

@openclaw-mantis telegram live: verify an isolated cron sessions_send announces to the target without stepping the cron requester while a normal requester still gets A2A ping-pong.

Risk before merge

  • [P1] The merge intentionally changes message-delivery behavior for canonical isolated cron requester sessions; an incorrect classifier would either leave the feedback loop in place or suppress legitimate non-cron A2A ping-pong.
  • [P1] The supplied proof exercises the runtime path with copied Node output, but it is not a full live external-channel cron run.

Maintainer options:

  1. Accept The Narrow Cron-Run Gate (recommended)
    Accept the intentional delivery change because the PR gates only canonical cron-run requester sessions and preserves normal requester ping-pong in tests and proof.
  2. Ask For Live Cron Transport Proof
    Pause merge until a real isolated-cron send over an external channel shows the target announce arrives without stepping the cron requester.
  3. Pause For A Different Delivery Contract
    Hold or close the branch only if maintainers decide isolated cron requester ping-pong should remain enabled or choose a different canonical fix.

Next step before merge

  • No automated repair is needed; maintainers need to accept and land or reject this intentional delivery behavior change after normal gates.

Security
Cleared: The diff changes in-repo agent control flow and tests only, with no dependency, workflow, secret, permission, package, or third-party code-execution surface.

Review details

Best possible solution:

Land this canonical cron-run gate after normal required gates, then close the linked bug; request live cron transport proof only if maintainers want extra assurance.

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

Yes, at source level. Current main and v2026.6.11 pass positive A2A turns into a callee that can step the requester session, and the linked bug plus PR proof show the isolated-cron feedback-loop path.

Is this the best way to solve the issue?

Yes. The caller-side canonical cron-run gate is the narrowest maintainable fix I found because it preserves target announce delivery and normal requester ping-pong, unlike a timeout-based or whole-flow skip.

AGENTS.md: found and applied where relevant.

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

Label changes

Label changes:

  • add mantis: telegram-visible-proof: Mantis should capture Telegram visible proof. The reported path is Telegram-visible message delivery from isolated cron, so a short Telegram proof could directly show the user-visible behavior.

Label justifications:

  • P2: The PR addresses a concrete isolated-cron A2A behavior bug with bounded but real session and message-delivery impact.
  • merge-risk: 🚨 message-delivery: The diff intentionally changes whether sessions_send feeds target replies back into canonical isolated cron requester sessions.
  • 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 (live_output): The PR body supplies copied Node before/after output showing the isolated-cron requester is not stepped after the fix while normal requester ping-pong remains; full external-channel cron proof was not supplied.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body supplies copied Node before/after output showing the isolated-cron requester is not stepped after the fix while normal requester ping-pong remains; full external-channel cron proof was not supplied.
  • mantis: telegram-visible-proof: Mantis should capture Telegram visible proof. The reported path is Telegram-visible message delivery from isolated cron, so a short Telegram proof could directly show the user-visible behavior.
Evidence reviewed

PR surface:

Source +18, Tests +211. Total +229 across 3 files.

View PR surface stats
Area Files Added Removed Net
Source 1 21 3 +18
Tests 2 212 1 +211
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 3 233 4 +229

What I checked:

  • Repository policy read: Root and scoped agent/tool AGENTS guidance was read and applied; the review used the required read-beyond-diff and message-delivery risk checks. (AGENTS.md:24, 3558391a75fd)
  • Current main still has requester ping-pong enabled: Current main passes the configured maxPingPongTurns unchanged into runSessionsSendA2AFlow; there is no isolated-cron requester suppression on this caller path. (src/agents/tools/sessions-send-tool.ts:656, 3558391a75fd)
  • A2A callee can step the requester: When maxPingPongTurns > 0 and requester differs from target, the A2A flow starts with currentSessionKey = requesterSessionKey and calls runAgentStep, matching the reported feedback-loop mechanism. (src/agents/tools/sessions-send-tool.a2a.ts:145, 3558391a75fd)
  • Latest release also lacks the gate: Tag v2026.6.11 points at e085fa1a3ffd32d0ea6917e1e6fb4ecbffbb77d2; that release still passes maxPingPongTurns unchanged and has the same requester-stepping A2A callee. (src/agents/tools/sessions-send-tool.ts:656, e085fa1a3ffd)
  • PR head uses the canonical cron-run classifier: The final PR head computes isIsolatedCronRequester with isCronRunSessionKey, captures a baseline for that path, and passes zero ping-pong turns only for that requester class. (src/agents/tools/sessions-send-tool.ts:564, 4dbd299a1e46)
  • Cron-run key shape matches the fix boundary: Cron execution passes runSessionKey into agent execution, and OpenClaw tool construction passes the active session key into createSessionsSendTool; the classifier accepts agent:<id>:cron:<job>:run:<run> keys and rejects cron-like normal channel keys. (src/sessions/session-key-utils.ts:284, 3558391a75fd)

Likely related people:

  • vincentkoc: Live PR data shows Vincent Koc assigned to this review, and release/current history includes recent sessions and cron-key-adjacent commits on the same surface. (role: assigned reviewer and recent area contributor; confidence: medium; commits: e085fa1a3ffd; files: src/agents/tools/sessions-send-tool.ts, src/agents/tools/sessions-send-tool.a2a.ts, src/sessions/session-key-utils.ts)
  • steipete: History search for maxPingPongTurns and the A2A split points to earlier agent tool refactors that shaped the current sessions_send A2A files. (role: feature-history contributor; confidence: medium; commits: bcbfb357bec7, 13c2f22240e4; files: src/agents/tools/sessions-send-tool.ts, src/agents/tools/sessions-send-tool.a2a.ts)
  • gumadeiras: The shared cron run session-key classifier used by the PR traces to the session maintenance and cron pruning work in commit e19a23520c2d. (role: cron session-key classifier contributor; confidence: medium; commits: e19a23520c2d; files: src/sessions/session-key-utils.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.

@clawsweeper clawsweeper Bot added the rating: 🌊 off-meta tidepool PR readiness rating does not apply to this item. label Jun 11, 2026
@harjothkhara harjothkhara marked this pull request as draft June 11, 2026 23:27
@harjothkhara harjothkhara force-pushed the fix/sessions-send-fire-and-forget-92257 branch from 3da89f0 to a32947d Compare June 11, 2026 23:47
@harjothkhara harjothkhara changed the title fix(agents): don't inject A2A turns into fire-and-forget sessions_send (#92257) fix(agents): don't inject A2A turns into isolated-cron sessions_send (#92257) Jun 11, 2026
@harjothkhara harjothkhara marked this pull request as ready for review June 11, 2026 23:47
@openclaw-barnacle openclaw-barnacle Bot added proof: supplied External PR includes structured after-fix real behavior proof. and removed triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels Jun 11, 2026
@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. and removed rating: 🌊 off-meta tidepool PR readiness rating does not apply to this item. labels Jun 15, 2026
@harjothkhara harjothkhara force-pushed the fix/sessions-send-fire-and-forget-92257 branch from a32947d to 034a80a Compare June 20, 2026 05:19
@harjothkhara

Copy link
Copy Markdown
Contributor Author

Addressed the Codex [P2] finding (narrow the cron requester detector).

  • The requester gate now uses the canonical isCronRunSessionKey instead of a raw :cron: substring, so a non-canonical cron-like key such as agent:main:slack:cron:job:run:uuid keeps its intended cross-session ping-pong.
  • Kept the requesterChannel === "cron" arm — isolated cron runs do set that channel (src/cron/isolated-agent/run.ts:1104,1422), so it is a proven path, not dead code.
  • Added a regression in src/agents/tools/sessions.test.ts proving the non-canonical key preserves the configured maxPingPongTurns.
  • Rebased onto latest main.

Local proof: node scripts/run-vitest.mjs src/agents/tools/sessions.test.ts src/agents/tools/sessions-send-tool.a2a.test.ts62 passed. Reverting the one-line detector change makes the new regression fail (forces 0, expected 5), confirming it guards the fix.

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 20, 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.

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. and removed proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. labels Jun 20, 2026
…penclaw#92257)

Address Codex [P2] review: replace the raw `:cron:` substring detector with
the canonical isCronRunSessionKey so a non-canonical cron-like requester key
(e.g. agent:main:slack:cron:job:run:uuid) keeps its intended cross-session
ping-pong. Keep the requesterChannel === "cron" arm (isolated cron runs set
that channel in src/cron/isolated-agent/run.ts). Add a regression covering the
non-canonical key.
…ey only (openclaw#92257)

The requesterChannel === "cron" arm was unreachable: agentChannel is always a
DeliverableMessageChannel from resolveGatewayMessageChannel(messageProvider),
never the literal "cron". The channel: "cron" in src/cron/isolated-agent/run.ts
labels diagnostics events/lifecycle, not the tool channel. Gate on
isCronRunSessionKey alone and fix the misleading comment.

Tests drove the dead arm via agentChannel/requesterChannel "cron" plus a
non-canonical key (agent:main:cron:run:abc, which isCronRunSessionKey rejects).
Switch them to a canonical cron-run key (agent:main:cron:job:run:abc) and a
normal delivery channel so they exercise the real production gate.
@harjothkhara harjothkhara force-pushed the fix/sessions-send-fire-and-forget-92257 branch from 426774a to 1f040bd Compare June 22, 2026 20:43
@vincentkoc vincentkoc self-assigned this Jul 2, 2026
@openclaw-barnacle openclaw-barnacle Bot added channel: telegram Channel integration: telegram app: ios App: ios labels Jul 2, 2026
@openclaw-barnacle openclaw-barnacle Bot added size: S and removed channel: telegram Channel integration: telegram app: ios App: ios size: M labels Jul 2, 2026
@clawsweeper clawsweeper Bot added the mantis: telegram-visible-proof Mantis should capture Telegram visible proof. label Jul 2, 2026
@clawsweeper clawsweeper Bot temporarily deployed to qa-live-shared July 2, 2026 00:28 Inactive
@vincentkoc vincentkoc merged commit c49714f into openclaw:main Jul 2, 2026
113 of 116 checks passed
@vincentkoc

Copy link
Copy Markdown
Member

Merged via squash.

vincentkoc added a commit that referenced this pull request Jul 2, 2026
* origin/main:
  test(gateway): isolate live release agent state
  test(plugins): repair prerelease validation fixtures
  fix(discord): guard JSON.parse against malformed API response bodies (#97889)
  fix(codex): preserve app approvals in side forks (#98812)
  fix(agents): don't inject A2A turns into isolated-cron sessions_send (#92257) (#92283)
  fix(browser): guard setDeep against empty keys array (#98138)
  docs(telegram): move maintainer decisions into scoped AGENTS.md
  fix(ios): generate light app store screenshots only
  fix(gateway): distinguish reachable gateway from failed status probe (#98183)
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jul 2, 2026
…penclaw#92257) (openclaw#92283)

* fix(agents): don't inject A2A turns into isolated-cron sessions_send (openclaw#92257)

Fire-and-forget sessions_send (timeoutSeconds === 0) with announce
delivery runs the A2A ping-pong loop. For a cross-session send
(requester != target) the loop's first iteration feeds the target
agent's reply back into the requester session as a new turn. For a
normal requester that roundtrip is intended, but for an *isolated cron*
requester it injects reply context into the isolated run and causes an
agent feedback loop.

Narrow the fix to isolated-cron requesters only (detected by a session
key containing ":cron:" or channel "cron" -- the same signal used by
src/agents/subagent-registry.ts and set in
src/cron/isolated-agent/run.ts), NOT by timeoutSeconds. Gating on
timeoutSeconds was too broad: it disabled the intended ping-pong for
normal cross-session fire-and-forget sends.

Two hunks in src/agents/tools/sessions-send-tool.ts, both reusing one
`isIsolatedCronRequester` gate:

1. Force maxPingPongTurns to 0 in the runSessionsSendA2AFlow invocation
   only for an isolated-cron requester. The a2a flow's ping-pong guard
   (`maxPingPongTurns > 0`) then skips the requester-injection loop and
   proceeds straight to the announce step in the TARGET session,
   preserving fire-and-forget announce delivery. Normal requesters keep
   the configured turn count.

2. Read baselineReply for fire-and-forget sends when same-session (prior
   behavior) OR isolated-cron. Without a baseline fingerprint, a2a.ts
   would treat pre-existing assistant text in the target session (e.g.
   an unrelated concurrent cron's output) as "the reply" and
   misattribute it. The normal cross-session fire leg's history-call
   count is unchanged from origin/main. Read failures are tolerated so a
   snapshot error never blocks accepting the send.

Tests (sessions.test.ts): an isolated-cron cross-session fire-and-forget
forwards maxPingPongTurns: 0; a normal-requester regression guard
(discord:group:req / channel discord) forwards the configured turn count
(not 0), proving the normal ping-pong is preserved. a2a flow tests
(sessions-send-tool.a2a.test.ts): with turns=0 + requester!=target the
requester is never stepped but the target is still announced, and a
baseline-matching reply is neither injected nor announced.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(agents): gate isolated-cron A2A on canonical cron-run classifier (openclaw#92257)

Address Codex [P2] review: replace the raw `:cron:` substring detector with
the canonical isCronRunSessionKey so a non-canonical cron-like requester key
(e.g. agent:main:slack:cron:job:run:uuid) keeps its intended cross-session
ping-pong. Keep the requesterChannel === "cron" arm (isolated cron runs set
that channel in src/cron/isolated-agent/run.ts). Add a regression covering the
non-canonical key.

* refactor(agents): drop dead cron-channel A2A arm, gate on canonical key only (openclaw#92257)

The requesterChannel === "cron" arm was unreachable: agentChannel is always a
DeliverableMessageChannel from resolveGatewayMessageChannel(messageProvider),
never the literal "cron". The channel: "cron" in src/cron/isolated-agent/run.ts
labels diagnostics events/lifecycle, not the tool channel. Gate on
isCronRunSessionKey alone and fix the misleading comment.

Tests drove the dead arm via agentChannel/requesterChannel "cron" plus a
non-canonical key (agent:main:cron:run:abc, which isCronRunSessionKey rejects).
Switch them to a canonical cron-run key (agent:main:cron:job:run:abc) and a
normal delivery channel so they exercise the real production gate.

* fix(agents): align cron A2A fallback baselines

* chore: prepare branch refresh

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
chenyangjun-xy pushed a commit to chenyangjun-xy/openclaw that referenced this pull request Jul 3, 2026
…penclaw#92257) (openclaw#92283)

* fix(agents): don't inject A2A turns into isolated-cron sessions_send (openclaw#92257)

Fire-and-forget sessions_send (timeoutSeconds === 0) with announce
delivery runs the A2A ping-pong loop. For a cross-session send
(requester != target) the loop's first iteration feeds the target
agent's reply back into the requester session as a new turn. For a
normal requester that roundtrip is intended, but for an *isolated cron*
requester it injects reply context into the isolated run and causes an
agent feedback loop.

Narrow the fix to isolated-cron requesters only (detected by a session
key containing ":cron:" or channel "cron" -- the same signal used by
src/agents/subagent-registry.ts and set in
src/cron/isolated-agent/run.ts), NOT by timeoutSeconds. Gating on
timeoutSeconds was too broad: it disabled the intended ping-pong for
normal cross-session fire-and-forget sends.

Two hunks in src/agents/tools/sessions-send-tool.ts, both reusing one
`isIsolatedCronRequester` gate:

1. Force maxPingPongTurns to 0 in the runSessionsSendA2AFlow invocation
   only for an isolated-cron requester. The a2a flow's ping-pong guard
   (`maxPingPongTurns > 0`) then skips the requester-injection loop and
   proceeds straight to the announce step in the TARGET session,
   preserving fire-and-forget announce delivery. Normal requesters keep
   the configured turn count.

2. Read baselineReply for fire-and-forget sends when same-session (prior
   behavior) OR isolated-cron. Without a baseline fingerprint, a2a.ts
   would treat pre-existing assistant text in the target session (e.g.
   an unrelated concurrent cron's output) as "the reply" and
   misattribute it. The normal cross-session fire leg's history-call
   count is unchanged from origin/main. Read failures are tolerated so a
   snapshot error never blocks accepting the send.

Tests (sessions.test.ts): an isolated-cron cross-session fire-and-forget
forwards maxPingPongTurns: 0; a normal-requester regression guard
(discord:group:req / channel discord) forwards the configured turn count
(not 0), proving the normal ping-pong is preserved. a2a flow tests
(sessions-send-tool.a2a.test.ts): with turns=0 + requester!=target the
requester is never stepped but the target is still announced, and a
baseline-matching reply is neither injected nor announced.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(agents): gate isolated-cron A2A on canonical cron-run classifier (openclaw#92257)

Address Codex [P2] review: replace the raw `:cron:` substring detector with
the canonical isCronRunSessionKey so a non-canonical cron-like requester key
(e.g. agent:main:slack:cron:job:run:uuid) keeps its intended cross-session
ping-pong. Keep the requesterChannel === "cron" arm (isolated cron runs set
that channel in src/cron/isolated-agent/run.ts). Add a regression covering the
non-canonical key.

* refactor(agents): drop dead cron-channel A2A arm, gate on canonical key only (openclaw#92257)

The requesterChannel === "cron" arm was unreachable: agentChannel is always a
DeliverableMessageChannel from resolveGatewayMessageChannel(messageProvider),
never the literal "cron". The channel: "cron" in src/cron/isolated-agent/run.ts
labels diagnostics events/lifecycle, not the tool channel. Gate on
isCronRunSessionKey alone and fix the misleading comment.

Tests drove the dead arm via agentChannel/requesterChannel "cron" plus a
non-canonical key (agent:main:cron:run:abc, which isCronRunSessionKey rejects).
Switch them to a canonical cron-run key (agent:main:cron:job:run:abc) and a
normal delivery channel so they exercise the real production gate.

* fix(agents): align cron A2A fallback baselines

* chore: prepare branch refresh

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
sheyanmin pushed a commit to sheyanmin/openclaw that referenced this pull request Jul 8, 2026
…penclaw#92257) (openclaw#92283)

* fix(agents): don't inject A2A turns into isolated-cron sessions_send (openclaw#92257)

Fire-and-forget sessions_send (timeoutSeconds === 0) with announce
delivery runs the A2A ping-pong loop. For a cross-session send
(requester != target) the loop's first iteration feeds the target
agent's reply back into the requester session as a new turn. For a
normal requester that roundtrip is intended, but for an *isolated cron*
requester it injects reply context into the isolated run and causes an
agent feedback loop.

Narrow the fix to isolated-cron requesters only (detected by a session
key containing ":cron:" or channel "cron" -- the same signal used by
src/agents/subagent-registry.ts and set in
src/cron/isolated-agent/run.ts), NOT by timeoutSeconds. Gating on
timeoutSeconds was too broad: it disabled the intended ping-pong for
normal cross-session fire-and-forget sends.

Two hunks in src/agents/tools/sessions-send-tool.ts, both reusing one
`isIsolatedCronRequester` gate:

1. Force maxPingPongTurns to 0 in the runSessionsSendA2AFlow invocation
   only for an isolated-cron requester. The a2a flow's ping-pong guard
   (`maxPingPongTurns > 0`) then skips the requester-injection loop and
   proceeds straight to the announce step in the TARGET session,
   preserving fire-and-forget announce delivery. Normal requesters keep
   the configured turn count.

2. Read baselineReply for fire-and-forget sends when same-session (prior
   behavior) OR isolated-cron. Without a baseline fingerprint, a2a.ts
   would treat pre-existing assistant text in the target session (e.g.
   an unrelated concurrent cron's output) as "the reply" and
   misattribute it. The normal cross-session fire leg's history-call
   count is unchanged from origin/main. Read failures are tolerated so a
   snapshot error never blocks accepting the send.

Tests (sessions.test.ts): an isolated-cron cross-session fire-and-forget
forwards maxPingPongTurns: 0; a normal-requester regression guard
(discord:group:req / channel discord) forwards the configured turn count
(not 0), proving the normal ping-pong is preserved. a2a flow tests
(sessions-send-tool.a2a.test.ts): with turns=0 + requester!=target the
requester is never stepped but the target is still announced, and a
baseline-matching reply is neither injected nor announced.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(agents): gate isolated-cron A2A on canonical cron-run classifier (openclaw#92257)

Address Codex [P2] review: replace the raw `:cron:` substring detector with
the canonical isCronRunSessionKey so a non-canonical cron-like requester key
(e.g. agent:main:slack:cron:job:run:uuid) keeps its intended cross-session
ping-pong. Keep the requesterChannel === "cron" arm (isolated cron runs set
that channel in src/cron/isolated-agent/run.ts). Add a regression covering the
non-canonical key.

* refactor(agents): drop dead cron-channel A2A arm, gate on canonical key only (openclaw#92257)

The requesterChannel === "cron" arm was unreachable: agentChannel is always a
DeliverableMessageChannel from resolveGatewayMessageChannel(messageProvider),
never the literal "cron". The channel: "cron" in src/cron/isolated-agent/run.ts
labels diagnostics events/lifecycle, not the tool channel. Gate on
isCronRunSessionKey alone and fix the misleading comment.

Tests drove the dead arm via agentChannel/requesterChannel "cron" plus a
non-canonical key (agent:main:cron:run:abc, which isCronRunSessionKey rejects).
Switch them to a canonical cron-run key (agent:main:cron:job:run:abc) and a
normal delivery channel so they exercise the real production gate.

* fix(agents): align cron A2A fallback baselines

* chore: prepare branch refresh

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling mantis: telegram-visible-proof Mantis should capture Telegram visible proof. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. proof: supplied External PR includes structured after-fix real behavior proof. 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.

[Bug]: sessions_send with announce delivery injects Argus reply context into isolated cron session, causing agent feedback loop

2 participants