Skip to content

fix: agent-only announce path, BB message IDs, sender identity, SSRF allowlist#23970

Merged
tyler6204 merged 7 commits intomainfrom
fix/announce-and-bluebubbles-metadata
Mar 2, 2026
Merged

fix: agent-only announce path, BB message IDs, sender identity, SSRF allowlist#23970
tyler6204 merged 7 commits intomainfrom
fix/announce-and-bluebubbles-metadata

Conversation

@tyler6204
Copy link
Member

@tyler6204 tyler6204 commented Feb 22, 2026

Summary

Fixes subagent announce reliability and BlueBubbles metadata handling, and includes follow up test coverage and cleanup from /preparepr. Supersedes #23166 (closed).

Changes

  1. Agent only subagent announce delivery

    • Uses pending descendant tracking (countPendingDescendantRuns) so orchestrator announces wait until descendants fully settle, including cleanup.
    • Keeps announce decisions on the method: "agent" path so parent sessions process completion events reliably.
  2. Completion message cleanup hardening

    • Completion message flows can defer while descendants finish.
    • Adds a hard expiry backstop to prevent indefinite pending state if descendants never settle.
  3. BlueBubbles message ID extraction

    • Expands response parsing roots and prefers message_id when available.
    • Returns messageId and messageGuid consistently for send/reply tracking.
  4. Sender identity in direct message context

    • Includes senderInfo metadata for DM context as well as groups.
    • Prefers sender display name when present.
  5. Inbound metadata simplification

    • Resolves a single message_id (MessageSid ?? MessageSidFull) instead of injecting both IDs.
  6. Control UI method guard behavior

    • Keeps non GET and HEAD requests falling through for non UI routes when no base path is configured.
    • Adds explicit POST test coverage for both fallthrough and base path 405 behavior.
  7. Contributor ranking update

    • Uses composite score ranking and excludes docs only LOC from scoring noise.

Regression fixes

  • Reverts regression from f555835 by restoring reliable agent path completion handling.
  • Reverts regression from 8178ea4 by restoring end to end announce cleanup flow.

Testing

  • Updated subagent registry tests for pending descendant and cleanup behavior
  • Added unit coverage for deferred cleanup hard expiry behavior
  • Added Control UI HTTP tests for POST fallthrough and 405 behavior
  • Updated BlueBubbles send helper coverage for response shape handling

@tyler6204 tyler6204 added the bug Something isn't working label Feb 22, 2026
@openclaw-barnacle openclaw-barnacle bot added channel: bluebubbles Channel integration: bluebubbles agents Agent runtime and tooling size: XL maintainer Maintainer-authored PR labels Feb 22, 2026
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

13 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +14 to +38
export type SubagentRunRecord = {
runId: string;
childSessionKey: string;
requesterSessionKey: string;
requesterOrigin?: DeliveryContext;
requesterDisplayKey: string;
task: string;
cleanup: "delete" | "keep";
label?: string;
model?: string;
runTimeoutSeconds?: number;
createdAt: number;
startedAt?: number;
endedAt?: number;
outcome?: SubagentRunOutcome;
archiveAtMs?: number;
cleanupCompletedAt?: number;
cleanupHandled?: boolean;
suppressAnnounceReason?: "steer-restart" | "killed";
expectsCompletionMessage?: boolean;
/** Number of times announce delivery has been attempted and returned false (deferred). */
announceRetryCount?: number;
/** Timestamp of the last announce retry attempt (for backoff). */
lastAnnounceRetryAt?: number;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate type definition conflicts with subagent-registry.types.ts

The inline SubagentRunRecord type here differs from the one exported in subagent-registry.types.ts (which still includes spawnMode, endedReason, and endedHookEmittedAt). This creates a type inconsistency since subagent-registry.store.ts imports from the types file and sets spawnMode on line 104.

Suggested change
export type SubagentRunRecord = {
runId: string;
childSessionKey: string;
requesterSessionKey: string;
requesterOrigin?: DeliveryContext;
requesterDisplayKey: string;
task: string;
cleanup: "delete" | "keep";
label?: string;
model?: string;
runTimeoutSeconds?: number;
createdAt: number;
startedAt?: number;
endedAt?: number;
outcome?: SubagentRunOutcome;
archiveAtMs?: number;
cleanupCompletedAt?: number;
cleanupHandled?: boolean;
suppressAnnounceReason?: "steer-restart" | "killed";
expectsCompletionMessage?: boolean;
/** Number of times announce delivery has been attempted and returned false (deferred). */
announceRetryCount?: number;
/** Timestamp of the last announce retry attempt (for backoff). */
lastAnnounceRetryAt?: number;
};
import type { SubagentRunRecord } from "./subagent-registry.types.js";

Then update subagent-registry.types.ts to remove the deprecated fields instead of defining the type inline here.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/subagent-registry.ts
Line: 14-38

Comment:
Duplicate type definition conflicts with `subagent-registry.types.ts`

The inline `SubagentRunRecord` type here differs from the one exported in `subagent-registry.types.ts` (which still includes `spawnMode`, `endedReason`, and `endedHookEmittedAt`). This creates a type inconsistency since `subagent-registry.store.ts` imports from the types file and sets `spawnMode` on line 104.

```suggestion
import type { SubagentRunRecord } from "./subagent-registry.types.js";
```

Then update `subagent-registry.types.ts` to remove the deprecated fields instead of defining the type inline here.

How can I resolve this? If you propose a fix, please make it concise.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

void runSubagentAnnounceFlow({
childSessionKey: entry.childSessionKey,
childRunId: entry.runId,
requesterSessionKey: entry.requesterSessionKey,
requesterOrigin,

P1 Badge Preserve completion flags when starting announce cleanup

startSubagentAnnounceCleanupFlow no longer forwards entry.expectsCompletionMessage (or entry.spawnMode) into runSubagentAnnounceFlow, so completion-mode runs are treated as regular announces. This bypasses completion-specific routing (including resolveSubagentCompletionOrigin/subagent_delivery_target handling) and changes delivery behavior from completion-direct semantics to generic queue-first behavior, which can delay or misroute completion updates for manual/session subagent workflows.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const spawnMode = params.spawnMode === "session" ? "session" : "run";
const archiveAtMs =
spawnMode === "session" ? undefined : archiveAfterMs ? now + archiveAfterMs : undefined;
const archiveAtMs = archiveAfterMs ? now + archiveAfterMs : undefined;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep session-mode runs out of archival sweep

registerSubagentRun now always computes archiveAtMs from archiveAfterMinutes, removing the previous session-mode exemption. Persistent session-mode subagents are therefore swept and deleted after the archive window, which breaks long-lived thread-bound subagent sessions that are expected to remain available (for example, focused/persistent agent threads).

Useful? React with 👍 / 👎.

Comment on lines 773 to 775
if (updated > 0) {
persistSubagentRuns();
for (const entry of entriesByChildSessionKey.values()) {
void emitSubagentEndedHookOnce({
entry,
reason: SUBAGENT_ENDED_REASON_KILLED,
sendFarewell: true,
outcome: SUBAGENT_ENDED_OUTCOME_KILLED,
error: reason,
inFlightRunIds: endedHookInFlightRunIds,
persist: persistSubagentRuns,
}).catch(() => {
// Hook failures should not break termination flow.
});
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Emit subagent_ended hook when terminating active runs

After marking runs terminated in markSubagentRunTerminated, the function now only persists state and returns; the prior emitSubagentEndedHookOnce call path was removed. In kill/terminate scenarios, subagent_ended hooks no longer fire, so plugin cleanup and downstream automation that depend on kill lifecycle notifications silently stop running.

Useful? React with 👍 / 👎.

Copy link

@nikolasdehor nikolasdehor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work consolidating these five fixes. I went through all 15 files carefully. The method: "send" removal is the right call and the BB message ID extraction improvements are solid. A few things to flag:


SSRF allowlist — looks safe, one nit

The buildBlueBubblesAttachmentSsrFPolicy approach is sound: it extracts only the hostname from the user-configured serverUrl and pins both allowedHostnames and hostnameAllowlist to that single host. This correctly scopes the SSRF bypass to the exact BB server the user already trusts in their config — it doesn't blanket-allow RFC 1918 ranges or anything like that. The try/catch returning undefined on malformed URLs is a nice defensive fallback.

One thing to verify: does the SSRF guard in fetchRemoteMedia also validate redirect targets against hostnameAllowlist? If BB sends a 3xx redirect to a different internal host, we want to make sure that's still blocked. The comment says "Keep redirects pinned to the same trusted host" but I want to confirm the consumer actually checks hostnameAllowlist on redirect resolution, not just on the initial request. If it does, this is good.

method: "send" removal — correct fix, love the warning comment

The // ⚠️ CRITICAL: DO NOT ADD A "send" PATH HERE comment is excellent given this has regressed 3+ times. The test changes are thorough — every assertion that previously checked sendSpy now correctly checks agentSpy, and the expected message content changed from pre-formatted "✅ Subagent main finished" headers to the raw "Result:" + LLM instruction pattern. This is the right architecture: let the parent LLM process and rephrase the result.

countPendingDescendantRuns — good addition, subtle semantics

The distinction between "active" (not ended) and "pending" (ended but cleanup not completed) is important for avoiding premature announcements. The logic:

const runEnded = typeof entry.endedAt === "number";
const cleanupCompleted = typeof entry.cleanupCompletedAt === "number";
if (!runEnded || !cleanupCompleted) {
  count += 1;
}

This counts a run as pending if it's either still running OR ended-but-not-cleaned-up. The new test in subagent-registry.nested.test.ts covers this correctly. Good.

Codex bot's P1: session-mode archival sweep

The Codex bot flagged that removing the spawnMode === "session" exemption from archiveAtMs means persistent session-mode subagents will now get swept. Is this intentional? If session-mode subagents are being removed entirely (which this PR suggests — spawnMode is stripped from SubagentRunRecord), then yes, they should be archived. But if anything still relies on long-lived session subagents surviving indefinitely, this is a regression. Can you confirm the session-mode concept is fully removed, including from the store layer? The Greptile bot noted subagent-registry.types.ts still exports the old type with spawnMode — if subagent-registry.store.ts references that stale type, you'll get a mismatch at the persistence boundary.

Codex bot's P1: subagent_ended hook no longer fires on kill

markSubagentRunTerminated no longer calls emitSubagentEndedHookOnce. If any plugins rely on the subagent_ended lifecycle hook for cleanup when a subagent is killed (not just when it completes normally), those plugins will silently stop receiving kill notifications. Is this intentional? If the hook infrastructure is being simplified, that's fine, but it should be documented as a breaking change for plugin authors.

Inbound meta changes — mostly good, one question

Promoting SenderName above SenderE164 in the sender field preference chain and including sender info for DMs (not just groups) is a nice UX improvement. The message_id change to prefer MessageSid and never inject both message_id + message_id_full saves tokens.

One question: resolveSenderLabel now receives an id parameter. Is the function signature updated to accept it? The diff only shows the caller side, not the callee. If resolveSenderLabel doesn't use id in its label construction, passing it is harmless but the test expects label: "Tyler (+15551234567)" which suggests it does use it as a fallback when e164 is missing. Just want to confirm the function body handles the new param.

readLatestSubagentOutput removal — simplification is welcome

The old readLatestSubagentOutput had a complex fallback chain: try readLatestAssistantReply, then fall back to chat.history parsing with extractSubagentOutputText / extractToolResultText / extractInlineTextContent. This PR collapses all of that to just readLatestAssistantReply. The test changes confirm that tool-role messages are no longer used as fallback output — they now produce "(no output)" instead. This is cleaner, but is a behavioral change: subagents that only produce tool output without a final assistant message will now report no output. Is that acceptable for all subagent types?

finalizeSubagentCleanup sync vs async

finalizeSubagentCleanup changed from async to sync, and the .then() call in startSubagentAnnounceCleanupFlow dropped the void prefix. This is fine since the function no longer awaits anything internally, but the emitCompletionEndedHookIfNeeded call that was previously awaited inside it is now gone entirely (related to the lifecycle hook removal above).


Overall: the core fixes (send-path removal, SSRF allowlist, BB message IDs, sender identity) are solid. The main concerns are the two items the Codex bot flagged (session-mode archival, killed-hook removal) plus confirming redirect validation in the SSRF guard. Happy to approve once those are addressed or confirmed intentional.

@tyler6204 tyler6204 force-pushed the fix/announce-and-bluebubbles-metadata branch from a5a3785 to 0cd84ea Compare February 24, 2026 05:59
@tyler6204 tyler6204 self-assigned this Feb 24, 2026
@tyler6204 tyler6204 force-pushed the fix/announce-and-bluebubbles-metadata branch from 0cd84ea to b7c8922 Compare March 2, 2026 01:41
@openclaw-barnacle openclaw-barnacle bot added scripts Repository scripts size: L and removed size: XL labels Mar 2, 2026
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b7c8922bde

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

0,
countActiveDescendantRuns(canonicalRequesterSessionKey),
typeof countPendingDescendantRuns === "function"
? countPendingDescendantRuns(canonicalRequesterSessionKey)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude current run from pending-descendant gate

sendSubagentAnnounceDirectly now uses countPendingDescendantRuns(canonicalRequesterSessionKey) to decide whether completion text can be sent directly, but that pending-count includes the just-finished run itself until cleanupCompletedAt is written. At announce time this run is always still pending cleanup, so pendingDescendantRuns is at least 1 even with no siblings/descendants, which makes shouldSendCompletionDirectly false for non-session paths and effectively disables the direct completion-send branch.

Useful? React with 👍 / 👎.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 59ba16a810

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

: messageIdFull && messageIdFull !== messageId
? messageIdFull
: undefined,
message_id: isDirect ? undefined : resolvedMessageId,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve full message IDs when short aliases are emitted

By collapsing MessageSid/MessageSidFull into a single message_id, this drops the full provider ID for channels that intentionally emit a short alias plus full GUID (for example BlueBubbles). In BlueBubbles action flow, resolveBlueBubblesMessageId(..., { requireKnownShortId: true }) throws once the short-ID cache is gone and explicitly asks for MessageSidFull, so after cache expiry/restart the model no longer has the full ID needed to target older messages for replies/reactions. Keeping message_id_full when it differs avoids that failure mode while still preferring short IDs.

Useful? React with 👍 / 👎.

@tyler6204 tyler6204 force-pushed the fix/announce-and-bluebubbles-metadata branch from 5dcfee3 to b564a0b Compare March 2, 2026 06:43
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b564a0bfeb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +314 to +316
if (req.method !== "GET" && req.method !== "HEAD") {
if (!basePath) {
return false;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enforce method guard before basePath redirect

With a configured basePath, requests to the exact base path (for example POST /openclaw) hit the earlier pathname === basePath redirect branch and return 302 before this guard runs, so non-GET/HEAD methods bypass the intended rejection path. That regresses behavior from before this change (where all non-GET/HEAD were rejected) and is inconsistent with POST /openclaw/, which now returns 405.

Useful? React with 👍 / 👎.

@tyler6204 tyler6204 force-pushed the fix/announce-and-bluebubbles-metadata branch from b564a0b to 0573052 Compare March 2, 2026 06:51
This was referenced Mar 2, 2026
hanqizheng pushed a commit to hanqizheng/openclaw that referenced this pull request Mar 2, 2026
…allowlist (openclaw#23970)

* fix(agents): defer announces until descendant cleanup settles

* fix(bluebubbles): harden message metadata extraction

* feat(contributors): rank by composite score (commits, PRs, LOC, tenure)

* refactor(control-ui): move method guard after path checks to improve request handling

* fix subagent completion announce when only current run is pending

* fix(subagents): keep orchestrator runs active until descendants finish

* fix: prepare PR feedback follow-ups (openclaw#23970) (thanks @tyler6204)
execute008 pushed a commit to execute008/openclaw that referenced this pull request Mar 2, 2026
…allowlist (openclaw#23970)

* fix(agents): defer announces until descendant cleanup settles

* fix(bluebubbles): harden message metadata extraction

* feat(contributors): rank by composite score (commits, PRs, LOC, tenure)

* refactor(control-ui): move method guard after path checks to improve request handling

* fix subagent completion announce when only current run is pending

* fix(subagents): keep orchestrator runs active until descendants finish

* fix: prepare PR feedback follow-ups (openclaw#23970) (thanks @tyler6204)
dawi369 pushed a commit to dawi369/davis that referenced this pull request Mar 3, 2026
…allowlist (openclaw#23970)

* fix(agents): defer announces until descendant cleanup settles

* fix(bluebubbles): harden message metadata extraction

* feat(contributors): rank by composite score (commits, PRs, LOC, tenure)

* refactor(control-ui): move method guard after path checks to improve request handling

* fix subagent completion announce when only current run is pending

* fix(subagents): keep orchestrator runs active until descendants finish

* fix: prepare PR feedback follow-ups (openclaw#23970) (thanks @tyler6204)
@jaatster
Copy link

jaatster commented Mar 3, 2026

I thought this was supposed to fix everything but I have the same issues still. Sequential Spawn doesnt work and that Subagent has finished message still comes and my main agent doesn't do anything. Is it possible I can get some help from you? @tyler6204

OWALabuy pushed a commit to kcinzgg/openclaw that referenced this pull request Mar 4, 2026
…allowlist (openclaw#23970)

* fix(agents): defer announces until descendant cleanup settles

* fix(bluebubbles): harden message metadata extraction

* feat(contributors): rank by composite score (commits, PRs, LOC, tenure)

* refactor(control-ui): move method guard after path checks to improve request handling

* fix subagent completion announce when only current run is pending

* fix(subagents): keep orchestrator runs active until descendants finish

* fix: prepare PR feedback follow-ups (openclaw#23970) (thanks @tyler6204)
sachinkundu pushed a commit to sachinkundu/openclaw that referenced this pull request Mar 6, 2026
…allowlist (openclaw#23970)

* fix(agents): defer announces until descendant cleanup settles

* fix(bluebubbles): harden message metadata extraction

* feat(contributors): rank by composite score (commits, PRs, LOC, tenure)

* refactor(control-ui): move method guard after path checks to improve request handling

* fix subagent completion announce when only current run is pending

* fix(subagents): keep orchestrator runs active until descendants finish

* fix: prepare PR feedback follow-ups (openclaw#23970) (thanks @tyler6204)
zooqueen pushed a commit to hanzoai/bot that referenced this pull request Mar 6, 2026
…allowlist (openclaw#23970)

* fix(agents): defer announces until descendant cleanup settles

* fix(bluebubbles): harden message metadata extraction

* feat(contributors): rank by composite score (commits, PRs, LOC, tenure)

* refactor(control-ui): move method guard after path checks to improve request handling

* fix subagent completion announce when only current run is pending

* fix(subagents): keep orchestrator runs active until descendants finish

* fix: prepare PR feedback follow-ups (openclaw#23970) (thanks @tyler6204)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling app: web-ui App: web-ui bug Something isn't working channel: bluebubbles Channel integration: bluebubbles gateway Gateway runtime maintainer Maintainer-authored PR scripts Repository scripts size: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants