openclaw - ✅(Solved) Fix [Bug]: Subagent completion announce routes to child agent's channel binding instead of parent agent's session [3 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#70574Fetched 2026-04-24 05:56:14
View on GitHub
Comments
1
Participants
2
Timeline
6
Reactions
1
Author
Participants
Timeline (top)
cross-referenced ×5commented ×1

Root Cause

In spawn-requester-origin-C85iH8nz.js, resolveRequesterOriginForChild calls resolveFirstBoundAccountId with agentId: params.targetAgentId (the child's agentId):

const boundAccountId = params.requesterChannel && params.targetAgentId !== params.requesterAgentId
    ? resolveFirstBoundAccountId({
        cfg: params.cfg,
        channelId: params.requesterChannel,
        agentId: params.targetAgentId,  // ← looks up binding for CHILD, not parent
        ...
    }) : void 0;

return normalizeDeliveryContext({
    channel: params.requesterChannel,
    accountId: boundAccountId ?? params.requesterAccountId,  // ← child's binding overrides parent's
    ...
});

When Agent A spawns Agent B (targetAgentId !== requesterAgentId), this returns Agent B's accountId, overwriting Agent A's correct requesterAccountId.

At completion time, resolveSubagentCompletionOrigin (in subagent-announce-delivery-CWrHH6mL.js) builds a requesterConversation from this corrupted requesterOrigin and calls createBoundDeliveryRouter().resolveDestination. The bound router looks up active bindings for the child's subagent session, finds Agent B's Telegram DM binding matching on channel + conversationId, and routes the completion there — bypassing the parent agent.

Fix Action

Fixed

PR fix notes

PR #70607: fix(agents): preserve parent accountId in cross-agent subagent spawn routing

Description (problem / solution / changelog)

Closes openclaw/openclaw#70574

Summary

When Agent A (parent) spawns Agent B (child) via sessions_spawn, Agent B's completion announce routes to Agent B's channel binding instead of Agent A's session.

Root Cause

In resolveRequesterOriginForChild, the condition params.targetAgentId !== params.requesterAgentId was TRUE for cross-agent spawns, causing resolveFirstBoundAccountId to be called with the CHILD's targetAgentId. This resolved the child's binding, overwriting the parent's requesterAccountId in requesterOrigin.

Fix

Change !== to === so resolveFirstBoundAccountId is ONLY called for self-spawns (targetAgentId === requesterAgentId). Cross-agent spawns now preserve the parent's requesterAccountId unchanged.

// Before (buggy):
params.requesterChannel && params.targetAgentId !== params.requesterAgentId

// After (fixed):
params.requesterChannel && params.targetAgentId === params.requesterAgentId

Testing

Updated spawn-requester-origin.test.ts to reflect correct behavior:

  • Cross-agent spawns: expect parent's accountId, not child's binding
  • Same-agent spawns: binding resolution still applies as before

Upstream issue tracked at UnlikeOtherAI/Nessie#107.

Changed files

  • src/agents/spawn-requester-origin.test.ts (modified, +30/-28)
  • src/agents/spawn-requester-origin.ts (modified, +3/-2)

PR #109: fix(agents): document upstream subagent completion routing fix (#107)

Description (problem / solution / changelog)

Closes #107

Summary

This PR documents the upstream fix for the subagent completion routing bug (openclaw/openclaw#70574).

The actual fix is in openclaw/openclaw#70607 — this PR is a tracking/acknowledgement PR on the Nessie side.

Background

When Agent A (parent) spawns Agent B (child) via sessions_spawn, Agent B's completion announce was routing to Agent B's channel binding instead of Agent A's session.

The bug is in openclaw-codebase/src/agents/spawn-requester-origin.tsresolveRequesterOriginForChild called resolveFirstBoundAccountId with the child's targetAgentId for cross-agent spawns, overwriting the parent's requesterAccountId.

Fix (in openclaw/openclaw#70607)

Changed !== to === in the condition:

// Before:
params.requesterChannel && params.targetAgentId !== params.requesterAgentId

// After:
params.requesterChannel && params.targetAgentId === params.requesterAgentId

Nessie Impact

Nessie's OpenClaw interop layer (src/openclaw/announce-converter.ts, src/openclaw/event-translator.ts) was already correct — it properly sets parentSessionKey from parentTaskId. No Nessie-specific code changes were required.

Changed files

  • workspace-architect/SPEC-107-subagent-routing.md (added, +43/-0)

PR #70814: fix(cron): accept numeric telegram announce chat ids (Fixes #70758)

Description (problem / solution / changelog)

Summary

  • Problem: cron announce delivery can treat a numeric Telegram chat ID as delivery.channel, which later fails with Unsupported channel: <chatId>.
  • Why it matters: Telegram cron jobs that should deliver to a direct chat or group chat ID can finish the run but drop the announce step.
  • What changed: when delivery.channel is not actually a channel and there is no separate delivery.to, cron delivery now reuses the sole configured announce channel and treats that value as the explicit target; I also added regressions for both the plain --to 834732674 path and the misplaced delivery.channel=834732674 path.
  • What did NOT change (scope boundary): this does not add broad multi-channel guessing. If channel selection is ambiguous, the resolver still leaves that case alone instead of guessing.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #70758
  • Related #70574
  • This PR fixes a bug or regression

Root Cause (if applicable)

  • Root cause: the cron delivery resolver accepted any lowercased string in delivery.channel long enough for a raw Telegram chat ID to survive as the channel value instead of being treated as the destination.
  • Missing detection / guardrail: there was no regression coverage for a numeric Telegram target reaching cron delivery either through delivery.to or through a misfiled delivery.channel value when only one announce channel is configured.
  • Contributing context (if known): gateway validation intentionally allows raw channel values when there is only one configured announce channel, so the runtime resolver needs to recover cleanly from that shape.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: src/cron/isolated-agent/delivery-target.test.ts
  • Scenario the test should lock in: numeric Telegram-style IDs resolve through the sole configured announce channel both when passed in delivery.to and when the same value lands in delivery.channel.
  • Why this is the smallest reliable guardrail: the bug is in the delivery target resolver, so the resolver test exercises the exact normalization and fallback path without pulling in unrelated cron execution behavior.
  • Existing test that already covers this (if any): none that covered the numeric-target path.
  • If no new test is added, why not: N/A

User-visible / Behavior Changes

  • Isolated cron jobs with Telegram announce delivery can now keep numeric chat IDs as valid targets instead of failing with Unsupported channel.
  • Stored cron jobs that accidentally carry the numeric Telegram chat ID in delivery.channel now recover when there is exactly one configured announce channel.

Diagram (if applicable)

Before:
[cron delivery config with numeric Telegram target]
  -> [resolver keeps numeric value as channel]
  -> [outbound target resolution]
  -> [Unsupported channel: 834732674]

After:
[cron delivery config with numeric Telegram target]
  -> [resolver recognizes "not a real channel" when only one announce channel exists]
  -> [reuse that channel + move numeric value to explicit target]
  -> [announce delivery proceeds]

Security Impact (required)

  • New permissions/capabilities? (Yes/No) No
  • Secrets/tokens handling changed? (Yes/No) No
  • New/changed network calls? (Yes/No) No
  • Command/tool execution surface changed? (Yes/No) No
  • Data access scope changed? (Yes/No) No
  • If any Yes, explain risk + mitigation:

Repro + Verification

Environment

  • OS: macOS
  • Runtime/container: local worktree at /tmp/openclaw-70758-worktree
  • Model/provider: N/A
  • Integration/channel (if any): Telegram-style cron delivery target resolution
  • Relevant config (redacted): one configured announce channel in the validation harness

Steps

  1. Resolve cron delivery with channel: "last" and to: "834732674" while exactly one announce channel is configured.
  2. Resolve cron delivery again with channel: "834732674" and no to while the same single announce channel is configured.
  3. Confirm both cases resolve to that announce channel with to: "834732674".

Expected

  • Numeric Telegram chat IDs stay usable as announce targets.

Actual

  • Before the fix, the misplaced-channel path could surface Unsupported channel: 834732674.
  • After the fix, both resolver cases return the configured channel plus to: "834732674".

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Direct runtime harness output after the fix:

{"name":"explicit-to","ok":true,"channel":"alpha","to":"834732674"}
{"name":"misplaced-channel","ok":true,"channel":"alpha","to":"834732674"}

I also tried pnpm test -- src/cron/isolated-agent/delivery-target.test.ts, but the local Vitest lane aborts before test execution on this machine because the repo wants Node >=22.14.0 and the worktree is currently running Node 22.12.0.

Human Verification (required)

  • Verified scenarios: both numeric-target resolver paths above using a direct tsx harness against the real delivery resolver.
  • Edge cases checked: the recovery only kicks in when there is no separate delivery.to and channel selection collapses to one configured announce channel.
  • What you did not verify: a full Vitest run in this worktree, because the local Node version is below the repo's declared minimum and the runner crashes before loading the file.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? (Yes/No) Yes
  • Config/env changes? (Yes/No) No
  • Migration needed? (Yes/No) No
  • If yes, exact upgrade steps:

Risks and Mitigations

  • Risk: a non-channel delivery.channel value could be interpreted too aggressively.
    • Mitigation: the recovery only applies when there is no separate delivery.to and channel selection resolves to exactly one configured announce channel, so multi-channel setups are not guessed.

Changed files

  • src/cron/isolated-agent/delivery-target.test.ts (modified, +33/-0)
  • src/cron/isolated-agent/delivery-target.ts (modified, +29/-2)

Code Example

const boundAccountId = params.requesterChannel && params.targetAgentId !== params.requesterAgentId
    ? resolveFirstBoundAccountId({
        cfg: params.cfg,
        channelId: params.requesterChannel,
        agentId: params.targetAgentId,  // ← looks up binding for CHILD, not parent
        ...
    }) : void 0;

return normalizeDeliveryContext({
    channel: params.requesterChannel,
    accountId: boundAccountId ?? params.requesterAccountId,  // ← child's binding overrides parent's
    ...
});

---

// Current (buggy):
const boundAccountId = params.requesterChannel && params.targetAgentId !== params.requesterAgentId
    ? resolveFirstBoundAccountId({ agentId: params.targetAgentId, ... }) : void 0;

// Fixed:
const boundAccountId = void 0;  // always use requesterAccountId
RAW_BUFFERClick to expand / collapse

Bug Description

When Agent A (parent) spawns Agent B (child) via sessions_spawn, Agent B's completion announce should route back to Agent A's session context. Instead, the current implementation overrides Agent A's accountId with Agent B's binding accountId. At completion time, the bound delivery router finds Agent B's active binding for Agent B's subagent session and routes the completion directly to Agent B's Telegram DM — bypassing Agent A entirely.

Effect: The completion arrives via the child agent's channel instead of the parent's. The user sees the response coming from the wrong agent.

Root Cause

In spawn-requester-origin-C85iH8nz.js, resolveRequesterOriginForChild calls resolveFirstBoundAccountId with agentId: params.targetAgentId (the child's agentId):

const boundAccountId = params.requesterChannel && params.targetAgentId !== params.requesterAgentId
    ? resolveFirstBoundAccountId({
        cfg: params.cfg,
        channelId: params.requesterChannel,
        agentId: params.targetAgentId,  // ← looks up binding for CHILD, not parent
        ...
    }) : void 0;

return normalizeDeliveryContext({
    channel: params.requesterChannel,
    accountId: boundAccountId ?? params.requesterAccountId,  // ← child's binding overrides parent's
    ...
});

When Agent A spawns Agent B (targetAgentId !== requesterAgentId), this returns Agent B's accountId, overwriting Agent A's correct requesterAccountId.

At completion time, resolveSubagentCompletionOrigin (in subagent-announce-delivery-CWrHH6mL.js) builds a requesterConversation from this corrupted requesterOrigin and calls createBoundDeliveryRouter().resolveDestination. The bound router looks up active bindings for the child's subagent session, finds Agent B's Telegram DM binding matching on channel + conversationId, and routes the completion there — bypassing the parent agent.

Proposed Fix

Remove the resolveFirstBoundAccountId call when targetAgentId !== requesterAgentId. The parent's requesterAccountId must always be preserved in requesterOrigin — it should describe the parent agent's context, never the child's binding.

// Current (buggy):
const boundAccountId = params.requesterChannel && params.targetAgentId !== params.requesterAgentId
    ? resolveFirstBoundAccountId({ agentId: params.targetAgentId, ... }) : void 0;

// Fixed:
const boundAccountId = void 0;  // always use requesterAccountId

The key invariant: requesterOrigin must always describe the parent agent's delivery context.

Test Scenario

  1. Agent A (top-level) spawns Agent B → completion routes to Agent A's session ✅
  2. Agent A spawns itself → completion routes to Agent A's session ✅
  3. Subagent Agent B (depth ≥ 1) spawns Agent C with streamTo="parent" → completion routes to Agent B's session via thread binding ✅

Reported by: Jordi (Chief Engineer, OpenClaw Ecosystem) Date: 2026-04-23

extent analysis

TL;DR

Remove the resolveFirstBoundAccountId call when targetAgentId !== requesterAgentId to preserve the parent agent's requesterAccountId in requesterOrigin.

Guidance

  • Identify instances where targetAgentId !== requesterAgentId and ensure resolveFirstBoundAccountId is not called in these cases.
  • Verify that requesterOrigin always describes the parent agent's delivery context by checking the accountId field.
  • Review the resolveSubagentCompletionOrigin function to ensure it correctly handles the updated requesterOrigin object.
  • Test the fix using the provided test scenarios to ensure completions are routed correctly.

Example

// Fixed:
const boundAccountId = void 0;  // always use requesterAccountId

Notes

This fix assumes that the requesterAccountId is always available and valid when targetAgentId !== requesterAgentId. If this is not the case, additional logic may be needed to handle these scenarios.

Recommendation

Apply the proposed fix to remove the resolveFirstBoundAccountId call when targetAgentId !== requesterAgentId, as it correctly preserves the parent agent's requesterAccountId in requesterOrigin.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix [Bug]: Subagent completion announce routes to child agent's channel binding instead of parent agent's session [3 pull requests, 1 comments, 2 participants]