openclaw - 💡(How to fix) Fix [Bug]: plugin runtime subagent.run from before_dispatch does not trigger subagent_delivery_target or final external delivery [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#55517Fetched 2026-04-08 01:38:37
View on GitHub
Comments
1
Participants
2
Timeline
1
Reactions
0
Author
Participants
Timeline (top)
commented ×1

In OpenClaw 2026.3.24, when a plugin before_dispatch hook starts a child run via api.runtime.subagent.run() and returns an immediate handled reply, the child result is not later auto-delivered back to the same external requester channel, and subagent_delivery_target does not appear to run for that child completion path.

Root Cause

In OpenClaw 2026.3.24, when a plugin before_dispatch hook starts a child run via api.runtime.subagent.run() and returns an immediate handled reply, the child result is not later auto-delivered back to the same external requester channel, and subagent_delivery_target does not appear to run for that child completion path.

Fix Action

Fix / Workaround

In OpenClaw 2026.3.24, when a plugin before_dispatch hook starts a child run via api.runtime.subagent.run() and returns an immediate handled reply, the child result is not later auto-delivered back to the same external requester channel, and subagent_delivery_target does not appear to run for that child completion path.

  1. Install OpenClaw 2026.3.24.
  2. Enable a plugin with:
    • a before_dispatch hook
    • a subagent_delivery_target hook
  3. In before_dispatch:
    • match an inbound external-channel message
    • compute/store a delivery target for a fresh child session key
    • call api.runtime.subagent.run({ sessionKey: <fresh child key>, message: <task>, deliver: false, lane: "dispatch", idempotencyKey: <unique> })
    • call api.runtime.subagent.waitForRun({ runId, timeoutMs: 1 }) (or another short timeout) so the hook returns before the child finishes
    • return { handled: true, text: <progress text> }
  4. Send a matching inbound message from an external channel (observed here with iMessage).
  5. Observe that the progress text is delivered.
  6. Wait for the child run to finish.
  7. Observe that:
    • no subagent_delivery_target hook log is emitted for that child session
    • no final child result is auto-delivered to the same external channel
    • the final child result is only recoverable if the plugin manually polls the child session and resends it separately
  • the before_dispatch progress reply is delivered successfully to iMessage
  • the child run finishes and its final text exists in the child session
  • subagent_delivery_target is not triggered for that child run
  • no core final reply is delivered back to iMessage
  • the only working path was a plugin-side workaround that waited for completion, fetched child session messages, and resent the final text out of band

Code Example

Observed progress delivery:

2026-03-26T22:09:17.779+08:00 [gateway] restricted-runtime: before_dispatch delivery target agent:research:subagent:<child-session-1> -> imessage:imessage:+86******2423
2026-03-26T22:09:17.780+08:00 [gateway] restricted-runtime: before_dispatch -> research via research-web-lookup
2026-03-26T22:09:21.054+08:00 [imessage] delivered reply to imessage:+86******2423

Observed: no corresponding `restricted-runtime: subagent_delivery_target ...` log line for that child session.

Observed final result only after plugin-side custom resend workaround:

2026-03-26T22:11:55.995+08:00 [gateway] restricted-runtime: deferred reply delivered via openclaw CLI to imessage:imessage:+86******2423

Same pattern observed in another rule:

2026-03-26T22:18:32.581+08:00 [gateway] restricted-runtime: before_dispatch delivery target agent:research:subagent:<child-session-2> -> imessage:imessage:+86******2423
2026-03-26T22:18:32.582+08:00 [gateway] restricted-runtime: before_dispatch -> research via research-weather-time-fx
2026-03-26T22:18:35.330+08:00 [imessage] delivered reply to imessage:+86******2423
2026-03-26T22:19:41.385+08:00 [gateway] restricted-runtime: deferred reply delivered via openclaw CLI to imessage:imessage:+86******2423
RAW_BUFFERClick to expand / collapse

Bug type

Behavior bug (incorrect output/state without crash)

Beta release blocker

No

Summary

In OpenClaw 2026.3.24, when a plugin before_dispatch hook starts a child run via api.runtime.subagent.run() and returns an immediate handled reply, the child result is not later auto-delivered back to the same external requester channel, and subagent_delivery_target does not appear to run for that child completion path.

Steps to reproduce

  1. Install OpenClaw 2026.3.24.
  2. Enable a plugin with:
    • a before_dispatch hook
    • a subagent_delivery_target hook
  3. In before_dispatch:
    • match an inbound external-channel message
    • compute/store a delivery target for a fresh child session key
    • call api.runtime.subagent.run({ sessionKey: <fresh child key>, message: <task>, deliver: false, lane: "dispatch", idempotencyKey: <unique> })
    • call api.runtime.subagent.waitForRun({ runId, timeoutMs: 1 }) (or another short timeout) so the hook returns before the child finishes
    • return { handled: true, text: <progress text> }
  4. Send a matching inbound message from an external channel (observed here with iMessage).
  5. Observe that the progress text is delivered.
  6. Wait for the child run to finish.
  7. Observe that:
    • no subagent_delivery_target hook log is emitted for that child session
    • no final child result is auto-delivered to the same external channel
    • the final child result is only recoverable if the plugin manually polls the child session and resends it separately

Expected behavior

After the child run finishes, the final child result should be deliverable through a supported core path to the same requester channel.

Grounded reference:

  • core subagent completion delivery exists via registerSubagentRun(...) -> runSubagentAnnounceFlow(...) -> resolveSubagentCompletionOrigin(...)
  • subagent_delivery_target exists as a typed hook for subagent completion target resolution

Actual behavior

Observed behavior in 2026.3.24:

  • the before_dispatch progress reply is delivered successfully to iMessage
  • the child run finishes and its final text exists in the child session
  • subagent_delivery_target is not triggered for that child run
  • no core final reply is delivered back to iMessage
  • the only working path was a plugin-side workaround that waited for completion, fetched child session messages, and resent the final text out of band

OpenClaw version

2026.3.24 (cff6dc9)

Operating system

macOS 26.3.1 (Build 25D771280a)

Install method

brew

Model

zai/glm-5

Provider / routing chain

openclaw -> zai

Additional provider/model setup details

Configured in local OpenClaw config:

  • auth profile: zai:default
  • provider: zai
  • base URL: https://open.bigmodel.cn/api/coding/paas/v4
  • API type: openai-completions
  • default agent primary model: zai/glm-5

For the observed repro path, the matched before_dispatch rules route the request to the research agent, and no separate model override was observed in this plugin path.

Logs, screenshots, and evidence

Observed progress delivery:

2026-03-26T22:09:17.779+08:00 [gateway] restricted-runtime: before_dispatch delivery target agent:research:subagent:<child-session-1> -> imessage:imessage:+86******2423
2026-03-26T22:09:17.780+08:00 [gateway] restricted-runtime: before_dispatch -> research via research-web-lookup
2026-03-26T22:09:21.054+08:00 [imessage] delivered reply to imessage:+86******2423

Observed: no corresponding `restricted-runtime: subagent_delivery_target ...` log line for that child session.

Observed final result only after plugin-side custom resend workaround:

2026-03-26T22:11:55.995+08:00 [gateway] restricted-runtime: deferred reply delivered via openclaw CLI to imessage:imessage:+86******2423

Same pattern observed in another rule:

2026-03-26T22:18:32.581+08:00 [gateway] restricted-runtime: before_dispatch delivery target agent:research:subagent:<child-session-2> -> imessage:imessage:+86******2423
2026-03-26T22:18:32.582+08:00 [gateway] restricted-runtime: before_dispatch -> research via research-weather-time-fx
2026-03-26T22:18:35.330+08:00 [imessage] delivered reply to imessage:+86******2423
2026-03-26T22:19:41.385+08:00 [gateway] restricted-runtime: deferred reply delivered via openclaw CLI to imessage:imessage:+86******2423

Relevant source inspection:

  • plugin runtime subagent path only dispatches gateway agent / agent.wait / sessions.get:
    • src/gateway/server-plugins.ts
  • core subagent spawn path registers completion metadata:
    • src/agents/subagent-spawn.ts
    • src/agents/subagent-registry.ts
  • completion announce path later resolves delivery target and runs subagent_delivery_target:
    • src/agents/subagent-announce.ts
    • src/plugins/types.ts

Relevant links:

Impact and severity

Affected: plugin authors using before_dispatch with api.runtime.subagent.run() for long-running delegation

Severity: High (breaks final result delivery for this plugin pattern)

Frequency: Observed in every tested run where before_dispatch returned early and the child completed later

Consequence: final results do not reach the external requester channel unless the plugin implements its own resend logic

Additional information

Observed source difference:

  • sessions_spawn / spawnSubagentDirect() registers requesterOrigin, expectsCompletionMessage, and related completion metadata
  • api.runtime.subagent.run() appears to only dispatch gateway agent and return runId, without registering that completion metadata

If this difference is intentional, the plugin runtime docs should state clearly that plugin runtime subagent runs do not participate in core completion announce / subagent_delivery_target delivery. If it is not intentional, this appears to be an implementation gap between plugin runtime subagent runs and core subagent runs.

extent analysis

Fix Plan

To fix the issue, we need to modify the api.runtime.subagent.run() function to register the completion metadata, including requesterOrigin and expectsCompletionMessage. This will allow the core subagent completion delivery path to resolve the delivery target and run the subagent_delivery_target hook.

Here are the steps to fix the issue:

  • Modify the api.runtime.subagent.run() function to register the completion metadata:
// src/gateway/server-plugins.ts
api.runtime.subagent.run = async (options) => {
  // ...
  const runId = await spawnSubagentDirect(options);
  const completionMetadata = {
    requesterOrigin: options.requesterOrigin,
    expectsCompletionMessage: true,
  };
  await registerSubagentRun(runId, completionMetadata);
  return runId;
};
  • Update the registerSubagentRun() function to store the completion metadata:
// src/agents/subagent-registry.ts
const subagentRuns = new Map();

async function registerSubagentRun(runId, completionMetadata) {
  subagentRuns.set(runId, completionMetadata);
}
  • Modify the runSubagentAnnounceFlow() function to resolve the delivery target using the completion metadata:
// src/agents/subagent-announce.ts
async function runSubagentAnnounceFlow(runId) {
  const completionMetadata = subagentRuns.get(runId);
  if (completionMetadata) {
    const deliveryTarget = await resolveSubagentCompletionOrigin(completionMetadata.requesterOrigin);
    await runSubagentDeliveryTargetHook(deliveryTarget, completionMetadata);
  }
}

Verification

To verify that the fix worked, you can test the before_dispatch hook with a plugin that uses api.runtime.subagent.run() to start a child run. The child result should be auto-delivered to the same external requester channel after the child run finishes.

You can use the following test code:

// test/before-dispatch-hook.test.js
describe('before_dispatch hook', () => {
  it('should deliver child result to external channel', async () => {
    const plugin = {
      before_dispatch: async (message) => {
        const childRunId = await api.runtime.subagent.run({
          sessionKey: 'child-session',
          message: 'child message',
          deliver: false,
          lane: 'dispatch',
          idempotencyKey: 'unique-key',
        });
        await api.runtime.subagent.waitForRun({ runId: childRunId, timeoutMs: 1000 });
        return { handled: true, text: 'progress text' };
      },
    };
    const message = { text: 'test message' };
    const result = await plugin.before_dispatch(message);
    expect(result).toEqual({ handled: true, text: 'progress text' });
    // Wait for the child run to finish and

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…

FAQ

Expected behavior

After the child run finishes, the final child result should be deliverable through a supported core path to the same requester channel.

Grounded reference:

  • core subagent completion delivery exists via registerSubagentRun(...) -> runSubagentAnnounceFlow(...) -> resolveSubagentCompletionOrigin(...)
  • subagent_delivery_target exists as a typed hook for subagent completion target resolution

Still need to ship something?

×6

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

Back to top recommendations

TRENDING