openclaw - ✅(Solved) Fix bug(subagents): completion announce retry re-injects duplicate internal-context turns — transcript bloat and lane starvation [1 pull requests, 1 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#61479Fetched 2026-04-08 02:58:09
View on GitHub
Comments
0
Participants
1
Timeline
2
Reactions
0
Participants
Timeline (top)
cross-referenced ×1renamed ×1

Completion-announcement retries for subagent runs can re-inject the same synthetic internal runtime context into the parent session transcript more than once.

In practice this creates duplicate [Internal task completion event] user turns in the parent session, often followed by assistant NO_REPLY responses. On large active parent transcripts this causes repeated append/rewrite/lock churn and can starve the parent session lane.

Root Cause

Completion-announcement retries for subagent runs can re-inject the same synthetic internal runtime context into the parent session transcript more than once.

In practice this creates duplicate [Internal task completion event] user turns in the parent session, often followed by assistant NO_REPLY responses. On large active parent transcripts this causes repeated append/rewrite/lock churn and can starve the parent session lane.

Fix Action

Fixed

PR fix notes

PR #61525: fix(subagents): deduplicate completion announce — prevent duplicate internal-context turns in parent session on retry

Description (problem / solution / changelog)

What this fixes (plain English)

When a sub-agent completed its task but the completion announcement failed partway, retrying the cleanup would re-announce the completion to the parent agent — causing duplicate messages. This fix tracks whether the announcement was already delivered and skips re-announcing on retry, only performing cleanup bookkeeping.

Technical details

Root cause: No deduplication of the completion announce flow. On retry/re-entry after a partial cleanup failure, the entire announce+inject cycle re-ran even if the parent had already received the completion message.

Fix:

  • Persist completionAnnouncedAt on the subagent run record after successful delivery
  • Skip announce injection for runs where delivery already succeeded, proceeding directly to cleanup
  • Lifecycle regression test covering retry/re-entry after successful delivery

Files changed:

  • src/agents/subagent-registry.ts — sweep integration with dedup tracking, completionAnnouncedAt persistence
  • src/agents/subagent-registry.types.ts — type definition for completionAnnouncedAt field on subagent run records
  • src/agents/subagent-registry-lifecycle.ts — announce dedup and delivery tracking logic
  • src/agents/subagent-registry-lifecycle.test.ts — lifecycle regression tests for dedup announce behavior

Related

  • Fixes #61479
  • Companion PRs: #61801 (orphan reconciliation), #61803 (heartbeat routing)

Test plan

  • 8/8 lifecycle tests pass
  • Deduped announce skips re-injection for already-delivered completions
  • Ended hook fires correctly on retry with sendFarewell: true
  • cleanupCompletedAt set correctly on dedup path

Changed files

  • src/agents/subagent-registry-lifecycle.test.ts (modified, +189/-0)
  • src/agents/subagent-registry-lifecycle.ts (modified, +47/-7)
  • src/agents/subagent-registry-run-manager.ts (modified, +1/-0)
  • src/agents/subagent-registry.ts (modified, +169/-12)
  • src/agents/subagent-registry.types.ts (modified, +2/-0)

Code Example

export function prependInternalEventContext(body, events) {
  if (hasInternalRuntimeContext(body)) {
    return body;
  }
  const renderedEvents = formatAgentInternalEventsForPrompt(events);
  if (!renderedEvents) {
    return body;
  }
  return [renderedEvents, body].filter(Boolean).join("\n\n");
}
RAW_BUFFERClick to expand / collapse

Summary

Completion-announcement retries for subagent runs can re-inject the same synthetic internal runtime context into the parent session transcript more than once.

In practice this creates duplicate [Internal task completion event] user turns in the parent session, often followed by assistant NO_REPLY responses. On large active parent transcripts this causes repeated append/rewrite/lock churn and can starve the parent session lane.

Observed symptom

In a live agent:main:main session, the same child completion payloads appeared multiple times in the parent transcript. The assistant correctly answered NO_REPLY to stale duplicates, but the duplicates still expanded the parent transcript and correlated with repeated:

  • lane wait exceeded: lane=session:agent:main:main ...
  • context-engine maintenance / transcript rewrite activity
  • lockfile churn on the parent transcript

Why this happens

The retry model currently operates at the whole parent-session agent invocation layer rather than only at downstream delivery.

Relevant source path

  1. completeSubagentRun(...) triggers cleanup announce flow:

    • src/agents/subagent-registry-lifecycle.ts
    • startSubagentAnnounceCleanupFlow(runId, entry)
  2. That calls:

    • runSubagentAnnounceFlow(...)
  3. Which ultimately calls:

    • deliverSubagentAnnouncement(...)
    • callGateway({ method: "agent", ... internalEvents })
  4. In the parent agent execution path, internal events are turned into a new synthetic user turn via:

    • src/agents/command/attempt-execution.ts
    • prependInternalEventContext(body, events)
export function prependInternalEventContext(body, events) {
  if (hasInternalRuntimeContext(body)) {
    return body;
  }
  const renderedEvents = formatAgentInternalEventsForPrompt(events);
  if (!renderedEvents) {
    return body;
  }
  return [renderedEvents, body].filter(Boolean).join("\n\n");
}

So every retry of the announce flow creates another parent-session input turn containing the same completion event.

The retry window

cleanupHandled is intentionally cleared on retry/defer paths, and resumeSubagentRun(runId) can re-enter startSubagentAnnounceCleanupFlow(...) as long as cleanupCompletedAt has not been set.

Relevant files / lines:

  • src/agents/subagent-registry-lifecycle.ts
    • beginSubagentCleanup(...)
    • finalizeSubagentCleanup(...)
    • retryDeferredCompletedAnnounces(...)
    • startSubagentAnnounceCleanupFlow(...)
  • src/agents/subagent-registry.ts
    • resumeSubagentRun(runId)

Notably, retries are intentionally supported, but there is no persistent guard ensuring that the same (childSessionKey, childRunId) completion context is injected into the parent session transcript only once.

Why current idempotency is not sufficient

The announce path does derive stable announce ids and idempotency keys:

  • buildAnnounceIdFromChildRun(...)
  • buildAnnounceIdempotencyKey(...)

But those are applied to the announce/delivery request. The retry still re-runs the parent agent invocation path that prepends internal runtime context into the parent input body. In other words, retries are happening at too high a layer.

Expected behavior

For a given child completion (childSessionKey, childRunId):

  • the parent session should ingest the internal completion context at most once
  • retries should only affect downstream delivery / unresolved follow-up behavior
  • retrying should not create duplicate synthetic user turns in the parent transcript

Actual behavior

The same completion can be re-announced into the parent session transcript more than once before cleanupCompletedAt is finalized.

Impact

  • duplicate internal completion context messages in parent transcript
  • duplicate assistant work on stale completion events
  • repeated NO_REPLY responses
  • much larger active parent transcript than necessary
  • repeated transcript rewrite / lock churn
  • lane starvation on the parent session

Suggested fix direction

Split these concerns:

  1. Completion ingestion into parent session — record once per (childSessionKey, childRunId)
  2. Delivery / follow-up retry — may retry, but must not re-inject the same internal context into the parent transcript

A per-run persisted marker such as one of these would likely be enough:

  • completionContextInjectedAt
  • deliveredAnnounceId
  • announcedChildRunIds[]

and gate runSubagentAnnounceFlow(...) / the agent reinvocation path accordingly.

Additional note

The existing subagent-registry.announce-loop-guard tests cover eventual give-up / retry bookkeeping, but they do not appear to assert the stronger invariant that the same child completion cannot produce duplicate internal-context parent turns.

extent analysis

TL;DR

Implement a persisted marker to track completion context injection into the parent session, ensuring each child completion is ingested only once.

Guidance

  • Introduce a persisted marker, such as completionContextInjectedAt or announcedChildRunIds[], to track which child completions have been injected into the parent session.
  • Modify startSubagentAnnounceCleanupFlow to check this marker before invoking runSubagentAnnounceFlow and prepending internal event context.
  • Update the subagent-registry.announce-loop-guard tests to assert the invariant that the same child completion cannot produce duplicate internal-context parent turns.
  • Consider adding a unique identifier to each completion announcement to facilitate tracking and deduplication.

Example

// Example of how to implement the persisted marker
const announcedChildRunIds: Set<string> = new Set();

// ...

function startSubagentAnnounceCleanupFlow(runId, entry) {
  const childSessionKey = entry.childSessionKey;
  const childRunId = entry.childRunId;
  const key = `${childSessionKey}:${childRunId}`;

  if (announcedChildRunIds.has(key)) {
    // Do not inject duplicate completion context
    return;
  }

  announcedChildRunIds.add(key);
  // Proceed with announcing the completion
  runSubagentAnnounceFlow(runId, entry);
}

Notes

The suggested fix direction involves splitting concerns between completion ingestion and delivery/retry, which should help prevent duplicate internal completion context messages in the parent transcript. However, the implementation details may vary depending on the specific requirements and constraints of the system.

Recommendation

Apply the workaround by implementing a persisted marker to track completion context injection, as this will prevent duplicate internal-context parent turns and mitigate the associated issues.

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

For a given child completion (childSessionKey, childRunId):

  • the parent session should ingest the internal completion context at most once
  • retries should only affect downstream delivery / unresolved follow-up behavior
  • retrying should not create duplicate synthetic user turns in the parent transcript

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(subagents): completion announce retry re-injects duplicate internal-context turns — transcript bloat and lane starvation [1 pull requests, 1 participants]