From 5e6204b677e67f76ce02624c05d1828d3b27d511 Mon Sep 17 00:00:00 2001 From: Contributor Date: Sun, 15 Mar 2026 11:21:53 +0800 Subject: [PATCH 1/3] feat: Add maxAnnounceChars parameter to sessions_spawn - Add maxAnnounceChars parameter to sessions_spawn tool schema - Pass parameter through subagent spawn flow to registry - Apply truncation with marker when announce message exceeds limit - Helps prevent message delivery failures on platforms with character limits (e.g., Telegram 4096 chars) Fixes #46785 --- src/agents/subagent-announce.ts | 13 ++++++++++++- src/agents/subagent-registry.ts | 6 ++++++ src/agents/subagent-registry.types.ts | 2 ++ src/agents/subagent-spawn.ts | 2 ++ src/agents/tools/sessions-spawn-tool.ts | 14 +++++++++++++- 5 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/agents/subagent-announce.ts b/src/agents/subagent-announce.ts index 5070b204392..e95e657eb19 100644 --- a/src/agents/subagent-announce.ts +++ b/src/agents/subagent-announce.ts @@ -1170,6 +1170,7 @@ export async function runSubagentAnnounceFlow(params: { wakeOnDescendantSettle?: boolean; signal?: AbortSignal; bestEffortDeliver?: boolean; + maxAnnounceChars?: number; }): Promise { let didAnnounce = false; const expectsCompletionMessage = params.expectsCompletionMessage === true; @@ -1395,6 +1396,16 @@ export async function runSubagentAnnounceFlow(params: { startedAt: params.startedAt, endedAt: params.endedAt, }); + + // Apply maxAnnounceChars truncation if specified + const truncatedFindings = + typeof params.maxAnnounceChars === "number" && + params.maxAnnounceChars >= 1 && + findings.length > params.maxAnnounceChars + ? findings.slice(0, params.maxAnnounceChars) + + "\n\n[truncated — full output in transcript]" + : findings; + const internalEvents: AgentInternalEvent[] = [ { type: "task_completion", @@ -1405,7 +1416,7 @@ export async function runSubagentAnnounceFlow(params: { taskLabel, status: outcome.status, statusLabel, - result: findings, + result: truncatedFindings, statsLine, replyInstruction, }, diff --git a/src/agents/subagent-registry.ts b/src/agents/subagent-registry.ts index d9c593c3e84..95d233c4a17 100644 --- a/src/agents/subagent-registry.ts +++ b/src/agents/subagent-registry.ts @@ -565,6 +565,7 @@ function startSubagentAnnounceCleanupFlow(runId: string, entry: SubagentRunRecor spawnMode: entry.spawnMode, expectsCompletionMessage: entry.expectsCompletionMessage, wakeOnDescendantSettle: entry.wakeOnDescendantSettle === true, + maxAnnounceChars: entry.maxAnnounceChars, }) .then((didAnnounce) => { finalizeAnnounceCleanup(didAnnounce); @@ -1171,6 +1172,7 @@ export function registerSubagentRun(params: { runTimeoutSeconds?: number; expectsCompletionMessage?: boolean; spawnMode?: "run" | "session"; + maxAnnounceChars?: number; attachmentsDir?: string; attachmentsRootDir?: string; retainAttachmentsOnKeep?: boolean; @@ -1204,6 +1206,10 @@ export function registerSubagentRun(params: { archiveAtMs, cleanupHandled: false, wakeOnDescendantSettle: undefined, + maxAnnounceChars: + typeof params.maxAnnounceChars === "number" && Number.isFinite(params.maxAnnounceChars) + ? Math.max(1, Math.floor(params.maxAnnounceChars)) + : undefined, attachmentsDir: params.attachmentsDir, attachmentsRootDir: params.attachmentsRootDir, retainAttachmentsOnKeep: params.retainAttachmentsOnKeep, diff --git a/src/agents/subagent-registry.types.ts b/src/agents/subagent-registry.types.ts index f5dc56775ae..fc6f2a9863f 100644 --- a/src/agents/subagent-registry.types.ts +++ b/src/agents/subagent-registry.types.ts @@ -55,4 +55,6 @@ export type SubagentRunRecord = { attachmentsDir?: string; attachmentsRootDir?: string; retainAttachmentsOnKeep?: boolean; + /** Maximum character limit for announce message delivery (truncates with marker if exceeded). */ + maxAnnounceChars?: number; }; diff --git a/src/agents/subagent-spawn.ts b/src/agents/subagent-spawn.ts index 1750d948e6c..12df1249920 100644 --- a/src/agents/subagent-spawn.ts +++ b/src/agents/subagent-spawn.ts @@ -56,6 +56,7 @@ export type SpawnSubagentParams = { cleanup?: "delete" | "keep"; sandbox?: SpawnSubagentSandboxMode; expectsCompletionMessage?: boolean; + maxAnnounceChars?: number; attachments?: Array<{ name: string; content: string; @@ -708,6 +709,7 @@ export async function spawnSubagentDirect( runTimeoutSeconds, expectsCompletionMessage, spawnMode, + maxAnnounceChars: params.maxAnnounceChars, attachmentsDir: attachmentAbsDir, attachmentsRootDir: attachmentRootDir, retainAttachmentsOnKeep: retainOnSessionKeep, diff --git a/src/agents/tools/sessions-spawn-tool.ts b/src/agents/tools/sessions-spawn-tool.ts index b735084d2b0..1aa2f9b19a3 100644 --- a/src/agents/tools/sessions-spawn-tool.ts +++ b/src/agents/tools/sessions-spawn-tool.ts @@ -63,6 +63,13 @@ const SessionsSpawnToolSchema = Type.Object({ mountPath: Type.Optional(Type.String()), }), ), + maxAnnounceChars: Type.Optional( + Type.Number({ + minimum: 1, + description: + "Maximum character limit for the sub-agent completion announce message. If exceeded, the message is truncated with a marker.", + }), + ), }); export function createSessionsSpawnTool( @@ -81,7 +88,7 @@ export function createSessionsSpawnTool( label: "Sessions", name: "sessions_spawn", description: - 'Spawn an isolated session (runtime="subagent" or runtime="acp"). mode="run" is one-shot and mode="session" is persistent/thread-bound. Subagents inherit the parent workspace directory automatically.', + 'Spawn an isolated session (runtime="subagent" or runtime="acp"). mode="run" is one-shot and mode="session" is persistent/thread-bound. Subagents inherit the parent workspace directory automatically. Use maxAnnounceChars to limit completion message length for platforms with character limits (e.g., Telegram 4096 chars).', parameters: SessionsSpawnToolSchema, execute: async (_toolCallId, args) => { const params = args as Record; @@ -118,6 +125,10 @@ export function createSessionsSpawnTool( ? Math.max(0, Math.floor(timeoutSecondsCandidate)) : undefined; const thread = params.thread === true; + const maxAnnounceCharsCandidate = + typeof params.maxAnnounceChars === "number" && Number.isFinite(params.maxAnnounceChars) + ? Math.max(1, Math.floor(params.maxAnnounceChars)) + : undefined; const attachments = Array.isArray(params.attachments) ? (params.attachments as Array<{ name: string; @@ -186,6 +197,7 @@ export function createSessionsSpawnTool( cleanup, sandbox, expectsCompletionMessage: true, + maxAnnounceChars: maxAnnounceCharsCandidate, attachments, attachMountPath: params.attachAs && typeof params.attachAs === "object" From 30f0aa827dd269cbbc017e696749fb73905d0640 Mon Sep 17 00:00:00 2001 From: Contributor Date: Sat, 21 Mar 2026 11:22:06 +0800 Subject: [PATCH 2/3] fix(acp): address code review comments on PR #46863 - Remove redundant getBlockCount() check (accumulatedBlockText.trim() is sufficient) - Simplifies condition for final TTS synthesis - Addresses Codex review feedback --- src/auto-reply/reply/dispatch-acp.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auto-reply/reply/dispatch-acp.ts b/src/auto-reply/reply/dispatch-acp.ts index 8fc7110fc4c..7fba9c6d28b 100644 --- a/src/auto-reply/reply/dispatch-acp.ts +++ b/src/auto-reply/reply/dispatch-acp.ts @@ -314,7 +314,7 @@ export async function tryDispatchAcpReply(params: { await projector.flush(true); const ttsMode = resolveTtsConfig(params.cfg).mode ?? "final"; const accumulatedBlockText = delivery.getAccumulatedBlockText(); - if (ttsMode === "final" && delivery.getBlockCount() > 0 && accumulatedBlockText.trim()) { + if (ttsMode === "final" && accumulatedBlockText.trim()) { try { const ttsSyntheticReply = await maybeApplyTtsToPayload({ payload: { text: accumulatedBlockText }, From 5b149501537151fd1234e7a0324e847508a5968d Mon Sep 17 00:00:00 2001 From: Contributor Date: Sat, 21 Mar 2026 12:50:14 +0800 Subject: [PATCH 3/3] fix(subagent): cap truncated announce text to maxAnnounceChars - Reserve space for truncation marker before slicing findings - Prevents final result from exceeding configured character limit - Addresses Codex review feedback on PR #46863 Before: findings.slice(max) + marker (exceeds max by marker length) After: findings.slice(max - marker.length) + marker (respects max) --- src/agents/subagent-announce.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/agents/subagent-announce.ts b/src/agents/subagent-announce.ts index e95e657eb19..fb8ee54a157 100644 --- a/src/agents/subagent-announce.ts +++ b/src/agents/subagent-announce.ts @@ -1398,12 +1398,12 @@ export async function runSubagentAnnounceFlow(params: { }); // Apply maxAnnounceChars truncation if specified + const TRUNCATION_MARKER = "\n\n[truncated — full output in transcript]"; const truncatedFindings = typeof params.maxAnnounceChars === "number" && params.maxAnnounceChars >= 1 && findings.length > params.maxAnnounceChars - ? findings.slice(0, params.maxAnnounceChars) + - "\n\n[truncated — full output in transcript]" + ? findings.slice(0, params.maxAnnounceChars - TRUNCATION_MARKER.length) + TRUNCATION_MARKER : findings; const internalEvents: AgentInternalEvent[] = [