Skip to content

fix(agents): restore subagent announce chain from #22223#23166

Closed
tyler6204 wants to merge 3 commits intomainfrom
fix/restore-subagent-announce-v2
Closed

fix(agents): restore subagent announce chain from #22223#23166
tyler6204 wants to merge 3 commits intomainfrom
fix/restore-subagent-announce-v2

Conversation

@tyler6204
Copy link
Member

@tyler6204 tyler6204 commented Feb 22, 2026

Summary

Restores the exact subagent announce logic from PR #22223 (commit fe57bea) which was overwritten by subsequent commits f555835 and 8178ea4.

Problem

Two commits landed after #22223 merged and rewrote subagent-announce.ts, re-introducing the broken send path that bypasses the parent LLM:

  • f555835 (Shadow): "Channels: add thread-aware model overrides" (+387/-144 lines)
  • 8178ea4 (Onur): "feat: thread-bound subagents on Discord" (+279/-40 lines)

This caused:

  • Raw "✅ Subagent X finished" messages appearing in channels instead of the parent LLM processing results
  • Cron subagent announces routing to dead isolated sessions
  • Nested subagent chains breaking at the top level
  • Intermediate subagent responses leaking to channels before children complete

Fix

Restored src/agents/subagent-announce.ts and src/agents/subagent-registry.ts to exactly match the working state from fe57bea. Updated callers/tests to match the restored interface.

All subagent completions route through method: "agent" only. No send path.

Testing verified

  • Simple subagent ✓
  • Nested subagent (depth 2) ✓
  • Depth 3 chain ✓
  • Parallel children (parent waits for both) ✓
  • Cron targeted ✓
  • Cron untargeted (legacy, no to/channel) ✓
  • No leaked "✅ Subagent main finished" messages

Related

Greptile Summary

This PR reverts subagent-announce.ts and subagent-registry.ts to the working state from PR #22223 (commit fe57bea08), undoing changes introduced by two subsequent commits that re-introduced a broken send delivery path bypassing the parent LLM.

Key changes:

  • Removes the method: "send" direct delivery path from subagent-announce.ts, ensuring all subagent completions route through method: "agent" only
  • Removes spawnMode tracking from SubagentRunRecord in the registry (session-mode vs run-mode distinction for announce routing)
  • Removes subagent_delivery_target hook integration and resolveSubagentCompletionOrigin from the announce flow
  • Removes lifecycle hook emission (emitSubagentEndedHookOnce, completeSubagentRun) from the registry — the subagent_ended hook is no longer explicitly emitted from the registry module
  • Inlines helper functions previously extracted into subagent-registry-cleanup.ts, subagent-registry-completion.ts, subagent-registry-queries.ts, and subagent-registry-state.ts
  • Switches from isDeliverableMessageChannel to isInternalMessageChannel (semantically equivalent)
  • Updates all test files to match the simplified interface

Notable side-effects:

  • emitLifecycleHooks: false was removed from two sessions.delete calls (sweeper and post-announce cleanup), which changes behavior: lifecycle hooks will now fire during these deletions
  • The inline SubagentRunRecord type in subagent-registry.ts now diverges from the version in subagent-registry.types.ts (which still includes spawnMode, endedReason, endedHookEmittedAt) — the store module still references the old type and sets spawnMode on disk-loaded records

Confidence Score: 3/5

  • This PR restores known-working logic but introduces two behavioral side-effects (lifecycle hook emission on delete) that should be verified before merging.
  • The core revert logic (removing the send path, routing all completions through method: "agent") is well-motivated and addresses a real regression. However, the removal of emitLifecycleHooks: false from two sessions.delete calls changes behavior in a way that could cause duplicate farewell messages or unexpected hook side-effects for channel plugins. Additionally, the SubagentRunRecord type now diverges between subagent-registry.ts and subagent-registry.types.ts/subagent-registry.store.ts, creating maintenance risk. The extracted helper modules (subagent-registry-cleanup.ts, subagent-registry-completion.ts, etc.) are left orphaned without being deleted.
  • Pay close attention to src/agents/subagent-registry.ts (lifecycle hook behavior change in sweeper) and src/agents/subagent-announce.ts (lifecycle hook behavior change in post-announce cleanup)

Last reviewed commit: cb9b0a5

@openclaw-barnacle openclaw-barnacle bot added agents Agent runtime and tooling size: XL maintainer Maintainer-authored PR labels Feb 22, 2026
@tyler6204 tyler6204 self-assigned this 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.

7 files reviewed, 3 comments

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 SubagentRunRecord type diverges from store module

This inline SubagentRunRecord type no longer includes spawnMode, endedReason, or endedHookEmittedAt, but subagent-registry.store.ts still imports SubagentRunRecord from subagent-registry.types.ts (which retains all three fields) and explicitly sets spawnMode on line 104 of subagent-registry.store.ts. This means:

  1. Records loaded from disk via loadSubagentRegistryFromDisk() will have spawnMode at runtime, but this type won't expose it.
  2. The two SubagentRunRecord types are now out of sync — the .types.ts version is a superset.

This won't cause a runtime error (TypeScript structural typing allows it), but the .types.ts file and the store module should be updated to match, or this file should import the type from .types.ts and simply not use the removed fields. Otherwise future maintainers may be confused by the divergence.

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 `SubagentRunRecord` type diverges from store module**

This inline `SubagentRunRecord` type no longer includes `spawnMode`, `endedReason`, or `endedHookEmittedAt`, but `subagent-registry.store.ts` still imports `SubagentRunRecord` from `subagent-registry.types.ts` (which retains all three fields) and explicitly sets `spawnMode` on line 104 of `subagent-registry.store.ts`. This means:

1. Records loaded from disk via `loadSubagentRegistryFromDisk()` will have `spawnMode` at runtime, but this type won't expose it.
2. The two `SubagentRunRecord` types are now out of sync — the `.types.ts` version is a superset.

This won't cause a runtime error (TypeScript structural typing allows it), but the `.types.ts` file and the store module should be updated to match, or this file should import the type from `.types.ts` and simply not use the removed fields. Otherwise future maintainers may be confused by the divergence.

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

Comment on lines 254 to +257
try {
await callGateway({
method: "sessions.delete",
params: {
key: entry.childSessionKey,
deleteTranscript: true,
emitLifecycleHooks: false,
},
params: { key: entry.childSessionKey, deleteTranscript: true },
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing emitLifecycleHooks: false on sweeper delete

The previous version explicitly passed emitLifecycleHooks: false here to prevent lifecycle hooks from firing during background archive sweeps. By omitting this parameter, the gateway defaults to emitLifecycleHooks: true (see sessions.ts:473), meaning lifecycle hooks (including subagent_ended) will now fire for every archived session deletion. If plugins have registered subagent_ended hooks, this could trigger unexpected side-effects (e.g., farewell messages or thread unbinding) for sessions that were already cleaned up.

Was this change intentional? If so, please note it in the PR description.

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

Comment:
**Missing `emitLifecycleHooks: false` on sweeper delete**

The previous version explicitly passed `emitLifecycleHooks: false` here to prevent lifecycle hooks from firing during background archive sweeps. By omitting this parameter, the gateway defaults to `emitLifecycleHooks: true` (see `sessions.ts:473`), meaning lifecycle hooks (including `subagent_ended`) will now fire for every archived session deletion. If plugins have registered `subagent_ended` hooks, this could trigger unexpected side-effects (e.g., farewell messages or thread unbinding) for sessions that were already cleaned up.

Was this change intentional? If so, please note it in the PR description.

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

Comment on lines 737 to 740
await callGateway({
method: "sessions.delete",
params: {
key: params.childSessionKey,
deleteTranscript: true,
emitLifecycleHooks: false,
},
params: { key: params.childSessionKey, deleteTranscript: true },
timeoutMs: 10_000,
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing emitLifecycleHooks: false on post-announce session delete

Same issue as in the sweeper: the previous version explicitly suppressed lifecycle hooks when deleting the child session after announcing results. Omitting emitLifecycleHooks: false means the gateway will now emit subagent_ended hooks during this cleanup delete, which may cause duplicate farewell messages or unexpected thread unbinding if subagent_ended hooks are registered by channel plugins.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/subagent-announce.ts
Line: 737-740

Comment:
**Missing `emitLifecycleHooks: false` on post-announce session delete**

Same issue as in the sweeper: the previous version explicitly suppressed lifecycle hooks when deleting the child session after announcing results. Omitting `emitLifecycleHooks: false` means the gateway will now emit `subagent_ended` hooks during this cleanup delete, which may cause duplicate farewell messages or unexpected thread unbinding if `subagent_ended` hooks are registered by channel plugins.

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

Here are some automated review suggestions for this pull request.

Reviewed commit: cb9b0a5b46

ℹ️ 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 543 to +544
const archiveAfterMs = resolveArchiveAfterMs(cfg);
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 Skip archive scheduling for persistent session-mode runs

registerSubagentRun now always sets archiveAtMs, so persistent mode="session" subagents are treated like one-shot runs. Those entries are later removed by sweepSubagentRuns (which calls sessions.delete), so thread-bound sessions that are documented/created as persistent will be auto-deleted after archiveAfterMinutes (default 60), breaking follow-up conversations in the bound thread.

Useful? React with 👍 / 👎.

Comment on lines 768 to 770
entry.cleanupHandled = true;
entry.cleanupCompletedAt = now;
entry.suppressAnnounceReason = "killed";

Choose a reason for hiding this comment

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

P1 Badge Emit ended hooks when marking runs killed

markSubagentRunTerminated now marks killed runs as fully cleaned up (cleanupHandled + cleanupCompletedAt) and only persists state, but no longer emits subagent_ended. Since /kill and abort flows use this function, killed subagents skip immediate lifecycle cleanup (for example thread-unbind/farewell hooks), leaving stale routing/binding state after termination.

Useful? React with 👍 / 👎.

@steipete
Copy link
Contributor

Thanks for the deep dive here.

Closing as superseded by newer targeted announce-path fixes.

This PR is a broad rollback to a historical snapshot and now diverges heavily from current announce/registry behavior. We should keep forward fixes small and focused (e.g. channel deliverability/routing and pairing-scope handling) rather than restoring an older full-file state.

@jaatster
Copy link

jaatster commented Mar 2, 2026

Is this fixed now? @tyler6204

I'm still on an old OpenClaw version because of this issue and I don't know if its safe to update now or not. Would appreciate any insight into this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling maintainer Maintainer-authored PR size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Subagent completion announce bypasses main LLM since v2026.2.19 (send path regression)

3 participants