Background ACP/Subagent Completions Surface Generic Task-Status Messages Instead of Parent-Agent Replies in Group Channels
When background ACP or subagent tasks complete in group/channel contexts, the system delivers raw lifecycle markers (e.g., 'Background task done') directly to the channel instead of routing completion context to the parent agent for user-facing summaries.
π Symptoms
Primary Manifestation
When a delegated or background ACP/subagent task completes in a group or channel context, the visible channel output contains only a raw lifecycle marker without conversational context from the parent agent:
Background task done: synthesized response from task-abc123
Background task failed: task-xyz789 failed with exit code 1
Background task timed out: task-def456 exceeded 30000ms limitSecondary Manifestations
- The user receives completion telemetry rather than an actionable summary in the conversational thread.
- No follow-up message from the parent agent appears after the lifecycle marker, leaving the interaction hanging.
- In multi-agent orchestration scenarios, child agent output may be prematurely exposed to the channel rather than being absorbed by the parent agent for synthesis.
Environment Context
This behavior manifests specifically when:
- The originating conversation occurs within a
GROUPorCHANNELsession type - The task was delegated via
ACPprotocol or subagent invocation - The task completion path traverses
src/tasks/task-registry.ts
Observed Console Behavior
If debug logging is enabled, you may observe entries indicating the completion delivery bypass:
[TaskRegistry] Terminal delivery to origin: group:channel-123
[TaskRegistry] Completion marker emitted without parent-agent handoff
[TaskRegistry] Skipping conversational synthesis for task: task-abc123π§ Root Cause
Architectural Analysis
The issue stems from a divergence in the terminal task delivery path within src/tasks/task-registry.ts. The code contains a conditional branch that, when the requester’s origin is identified as a group/channel context, delivers the completion event directly to that channel without invoking the parent-agent conversational handoff pipeline.
Failure Sequence
The following sequence illustrates the problematic code path:
- Background task completes: The delegated ACP or subagent task finishes execution and emits a completion event.
- TaskRegistry receives completion signal: The
TaskRegistryclass processes the completion via its terminal delivery handler. - Origin detection fails to flag group context: The completion handler detects the requester's origin as a group/channel identifier but does not set the
requiresConversationalHandoffflag. - Direct lifecycle marker emission: The code bypasses the parent-agent synthesis path and emits a raw
Background task done/failed/timed outmessage directly to the channel. - Parent agent excluded: The parent agent session never receives the completion context, so no user-facing summary is generated.
Code-Level Root Cause
In src/tasks/task-registry.ts, the terminal delivery logic likely contains a condition similar to:
// Problematic code path (pseudo-representation)
if (requesterOrigin.type === 'group' || requesterOrigin.type === 'channel') {
// Direct emission without handoff
emitLifecycleMarker(requesterOrigin, taskCompletion);
return; // Early return bypasses parent-agent pipeline
}This early return prevents the completion context from reaching the parent agent’s synthesis handler, which would otherwise generate the user-facing summary.
Architectural Inconsistency
The system’s design intent (as documented in the workflow specification) requires:
Child/Background Output β Internal
β
Parent Session Receives Completion Event
β
Parent Agent Synthesizes Summary β User-Facing ResponseHowever, the group/channel delivery path violates step 2, directly exposing the raw marker to users without the synthesis step.
π οΈ Step-by-Step Fix
Prerequisites
- Access to the
src/tasks/task-registry.tssource file - Understanding of the parent-agent communication protocol in your deployment
- Access to test environment with group/channel context capability
Fix Procedure
Step 1: Locate the Terminal Delivery Handler
Open src/tasks/task-registry.ts and identify the method handling terminal task delivery. The method signature will resemble:
async deliverTerminalCompletion(
taskId: string,
completion: TaskCompletion,
requesterOrigin: SessionOrigin
): Promise<void>Step 2: Modify the Group/Channel Origin Handling
Replace the direct emission logic with a handoff-aware flow. The fix ensures that group/channel completions still route through the parent-agent pipeline:
// BEFORE (problematic)
if (requesterOrigin.type === 'group' || requesterOrigin.type === 'channel') {
// Direct emission without handoff
emitLifecycleMarker(requesterOrigin, taskCompletion);
return; // Bypasses parent-agent pipeline
}
// AFTER (fixed)
if (requesterOrigin.type === 'group' || requesterOrigin.type === 'channel') {
// Route through parent-agent for conversational synthesis
const parentAgentContext = await this.extractParentAgentContext(taskId);
if (parentAgentContext && parentAgentContext.sessionId) {
// Handoff to parent agent for user-facing summary
await this.deliverToParentAgent(
parentAgentContext.sessionId,
taskCompletion,
requesterOrigin
);
// Emit telemetry marker as secondary, not primary output
await this.emitSecondaryTelemetry(requesterOrigin, taskCompletion);
} else {
// Fallback: no parent agent available, emit lifecycle marker
await this.emitLifecycleMarker(requesterOrigin, taskCompletion);
}
return;
}Step 3: Implement the Parent Agent Delivery Method
Add the deliverToParentAgent method if it does not exist:
private async deliverToParentAgent(
parentSessionId: string,
completion: TaskCompletion,
originalOrigin: SessionOrigin
): Promise<void> {
const parentSession = await this.sessionManager.getSession(parentSessionId);
if (!parentSession) {
this.logger.warn(`Parent session ${parentSessionId} not found for task completion`);
return;
}
// Deliver completion context to parent agent for synthesis
await parentSession.deliverCompletionEvent({
taskId: completion.taskId,
result: completion.result,
status: completion.status,
origin: originalOrigin,
timestamp: Date.now()
});
this.logger.info(`Completion context delivered to parent agent session: ${parentSessionId}`);
}Step 4: Implement Secondary Telemetry Emission
Add the emitSecondaryTelemetry method to emit lifecycle markers as non-blocking telemetry:
private async emitSecondaryTelemetry(
origin: SessionOrigin,
completion: TaskCompletion
): Promise<void> {
// Emit as structured telemetry, not as conversational message
await this.telemetryChannel.publish({
type: 'task_completion_telemetry',
origin: origin,
taskId: completion.taskId,
status: completion.status,
summary: this.summarizeCompletion(completion),
timestamp: Date.now()
});
this.logger.debug(`Secondary telemetry emitted for task: ${completion.taskId}`);
}Step 5: Verify Handoff Flagging
Ensure the parent-agent context extraction correctly identifies when a handoff is required. Update extractParentAgentContext if necessary:
private async extractParentAgentContext(
taskId: string
): Promise<ParentAgentContext | null> {
const taskRecord = await this.taskStore.getTask(taskId);
if (!taskRecord) {
return null;
}
// Always attempt to resolve parent context for delegated tasks
return {
sessionId: taskRecord.parentAgentSessionId,
requiresSynthesis: taskRecord.delegationType !== 'direct',
originTaskId: taskId
};
}π§ͺ Verification
Pre-Fix Baseline
Before applying the fix, capture the baseline behavior:
# In a group/channel context, invoke a background ACP task
/agent delegate --task "analyze logs" --background --channel group:engineering
# Observe output - should show only:
# Background task done: analyze logs completed
# (No parent-agent summary follows)Post-Fix Verification Steps
Step 1: Unit Test Validation
Run unit tests for the affected code path:
npm test -- --grep "TaskRegistry.deliverTerminalCompletion"
# Expected: All tests pass with new handoff logic coverageStep 2: Integration Test - Group Channel Completion
# Create a test scenario in a group channel
/agent delegate --task "generate report" --background --channel group:test-room
# Expected behavior after fix:
# 1. No raw "Background task done" message as primary output
# 2. Parent agent synthesizes and sends: "I've completed the report generation.
# Here's the summary: ..."
# 3. Telemetry marker appears in debug logs (not in conversational thread)Step 3: Verify Parent Agent Receives Context
Enable debug logging and observe the delivery sequence:
# Enable debug logging
export LOG_LEVEL=debug
# Execute background task
/agent delegate --task "process data" --background --channel group:verification-room
# Check logs for expected sequence:
# [TaskRegistry] Extracting parent agent context for task: task-xyz
# [TaskRegistry] Parent agent session resolved: session-abc
# [TaskRegistry] Completion context delivered to parent agent session: session-abc
# [TaskRegistry] Secondary telemetry emitted for task: task-xyz
# [ParentAgent] Received completion event, synthesizing summary...Step 4: Verify Telemetry Persistence
Ensure lifecycle telemetry is still captured for monitoring:
# Check telemetry channel receives completion data
curl -X POST http://telemetry-api/internal/events \
-d '{"type":"task_completion_telemetry","taskId":"task-xyz"}'
# Expected: Telemetry record exists with complete task metadataStep 5: Fallback Behavior Verification
Test that the fallback (direct lifecycle emission) still functions when no parent agent is available:
# Execute direct task (no parent agent) in group context
/agent execute --task "quick status" --channel group:no-parent-room
# Expected: Direct lifecycle marker emitted (fallback path)
# Background task done: quick status completedExpected Outcomes
- Primary channel output: Parent-agent synthesized user-facing summary (not raw lifecycle marker)
- Log output: Confirmation of parent-agent handoff
- Telemetry: Secondary lifecycle marker captured in telemetry system
- Exit codes: All tests exit with
0
β οΈ Common Pitfalls
Pitfall 1: Missing Parent Session Resolution
Symptom: Parent-agent handoff fails silently; no summary appears.
Cause: The parentAgentSessionId is not persisted during task delegation.
Mitigation: Ensure task records include parentAgentSessionId at delegation time:
// During task delegation initialization
await taskStore.createTask({
taskId: generatedTaskId,
parentAgentSessionId: currentSession.id,
delegationType: 'ACP' | 'subagent',
// ...other fields
});Pitfall 2: Circular Handoff in Nested Delegation
Symptom: Infinite loop or stack overflow when parent agents delegate to child agents recursively.
Cause: The handoff logic does not track delegation depth.
Mitigation: Implement delegation depth tracking:
if (completion.delegationDepth >= MAX_DELEGATION_DEPTH) {
// Emit direct completion for deep nested tasks
await this.emitLifecycleMarker(requesterOrigin, completion);
return;
}Pitfall 3: Channel Message Flooding
Symptom: Multiple completion messages appear in the channel after the fix.
Cause: Both the parent-agent summary and the telemetry marker are emitted to the channel.
Mitigation: Ensure telemetry emission publishes to a separate channel (not the conversational thread):
// Telemetry goes to telemetry system, not conversation channel
await this.telemetryChannel.publish({...}); // β
Correct
// NOT:
// await this.channel.sendMessage(origin, message); // β IncorrectPitfall 4: Session Manager Availability in Tests
Symptom: Unit tests pass locally but fail in CI due to session manager mock not being injected.
Cause: Direct instantiation of SessionManager instead of dependency injection.
Mitigation: Use constructor injection:
constructor(
private readonly sessionManager: SessionManagerInterface,
private readonly taskStore: TaskStoreInterface,
private readonly telemetryChannel: TelemetryChannelInterface
) {}Pitfall 5: macOS/Darvin Path Separator in Test Fixtures
Symptom: Tests fail to locate fixture files on macOS.
Cause: Test fixtures use POSIX paths that fail on Darwin.
Mitigation: Use path.join() for all file path construction:
import * as path from 'path';
const fixturePath = path.join(__dirname, '..', 'fixtures', 'task-completion.json');Pitfall 6: Docker Environment Session Isolation
Symptom: Parent-agent context is not resolved in Docker-based test environments.
Cause: Session state is not persisted across container restarts during tests.
Mitigation: Use in-memory session store with test isolation:
beforeEach(() => {
testSessionStore.clear();
container.mock(SessionManager, {
getSession: (id) => testSessionStore.get(id)
});
});π Related Errors
Adjacent Issues
- Missing Parent-Agent Summary in Multi-Agent Delegation
Similar root cause: Completion events bypass the synthesis pipeline. Manifests when nested agent delegation completes without user-facing output. - ACP Protocol Timeout Messages Not Synthesized
The ACP protocol may emitBackground task timed outmessages directly without parent-agent awareness. - Group Channel Message Ordering with Background Tasks
Lifecycle markers may appear out of order relative to other channel messages due to async completion handling.
Related Error Codes
E_TASK_REGISTRY_HANDSHAKE_FAILED: Parent-agent handoff handshake times out.E_SESSION_NOT_FOUND: Parent agent session ID resolved but session no longer exists.E_COMPLETION_TELEMETRY_FALLBACK: Fallback to direct emission triggered due to missing parent context.E_DELEGATION_DEPTH_EXCEEDED: Nested delegation exceeded maximum depth threshold.E_ORIGIN_TYPE_UNHANDLED: Unrecognized session origin type in terminal delivery path.
Historical Context
This issue may be adjacent to earlier UX improvements around delegated and background output visibility in group rooms. Prior fixes addressed configuration toggles for output visibility; this issue specifically targets the missing parent-agent conversational handoff that should precede any user-facing completion message.