diff --git a/src/components/tasks/BackgroundTask.tsx b/src/components/tasks/BackgroundTask.tsx
index 7402270d75..8f509fcd87 100644
--- a/src/components/tasks/BackgroundTask.tsx
+++ b/src/components/tasks/BackgroundTask.tsx
@@ -3,7 +3,7 @@ import * as React from 'react';
import { Text } from 'src/ink.js';
import type { BackgroundTaskState } from 'src/tasks/types.js';
import type { DeepImmutable } from 'src/types/utils.js';
-import { truncate } from 'src/utils/format.js';
+import { formatNumber, truncate } from 'src/utils/format.js';
import { toInkColor } from 'src/utils/ink.js';
import { plural } from 'src/utils/stringUtils.js';
import { DIAMOND_FILLED, DIAMOND_OPEN } from '../../constants/figures.js';
@@ -15,7 +15,7 @@ type Props = {
maxActivityWidth?: number;
};
export function BackgroundTask(t0) {
- const $ = _c(92);
+ const $ = _c(98);
const {
task,
maxActivityWidth
@@ -135,11 +135,34 @@ export function BackgroundTask(t0) {
} else {
t4 = $[28];
}
+ // Consumption for the row: the finished result when the agent is done,
+ // otherwise live progress. Slots 92..97 are appended past the range the
+ // compiler already allocated, so the branches below keep theirs.
+ const tokens = "result" in task ? task.result?.totalTokens : task.progress?.tokenCount;
+ const toolUses = "result" in task ? task.result?.totalToolUseCount : task.progress?.toolUseCount;
+ let tTokens;
+ if ($[92] !== tokens) {
+ tTokens = tokens !== undefined && tokens > 0 ? · {formatNumber(tokens)} tokens : null;
+ $[92] = tokens;
+ $[93] = tTokens;
+ } else {
+ tTokens = $[93];
+ }
+ let tTools;
+ if ($[94] !== toolUses) {
+ tTools = toolUses !== undefined && toolUses > 0 ? · {toolUses} {plural(toolUses, "tool")} : null;
+ $[94] = toolUses;
+ $[95] = tTools;
+ } else {
+ tTools = $[95];
+ }
let t5;
- if ($[29] !== t1 || $[30] !== t4) {
- t5 = {t1}{" "}{t4};
+ if ($[29] !== t1 || $[30] !== t4 || $[96] !== tTokens || $[97] !== tTools) {
+ t5 = {t1}{tTokens}{tTools}{" "}{t4};
$[29] = t1;
$[30] = t4;
+ $[96] = tTokens;
+ $[97] = tTools;
$[31] = t5;
} else {
t5 = $[31];
diff --git a/src/tasks/LocalAgentTask/LocalAgentTask.tsx b/src/tasks/LocalAgentTask/LocalAgentTask.tsx
index a2ed180fed..74c1909dee 100644
--- a/src/tasks/LocalAgentTask/LocalAgentTask.tsx
+++ b/src/tasks/LocalAgentTask/LocalAgentTask.tsx
@@ -253,7 +253,11 @@ export function enqueueAgentNotification({
// results may reference stale task output. The prompt suggestion text is
// preserved; only the pre-computed response is discarded.
abortSpeculation(setAppState);
- const summary = status === 'completed' ? `Agent "${description}" completed` : status === 'failed' ? `Agent "${description}" failed: ${error || 'Unknown error'}` : `Agent "${description}" was stopped`;
+ // The description reaches this point from the model's own tool input and is
+ // interpolated into the envelope below. An angle bracket in it
+ // would close the tag early and truncate the notification the parser reads.
+ const safeDescription = description.replace(/[<>]/g, '');
+ const summary = status === 'completed' ? `Agent "${safeDescription}" completed` : status === 'failed' ? `Agent "${safeDescription}" failed: ${error || 'Unknown error'}` : `Agent "${safeDescription}" was stopped`;
const outputPath = getTaskOutputPath(taskId);
const toolUseIdLine = toolUseId ? `\n<${TOOL_USE_ID_TAG}>${toolUseId}${TOOL_USE_ID_TAG}>` : '';
const resultSection = finalMessage ? `\n${finalMessage}` : '';
diff --git a/src/tasks/LocalAgentTask/notificationEnvelope.test.ts b/src/tasks/LocalAgentTask/notificationEnvelope.test.ts
new file mode 100644
index 0000000000..0090f78638
--- /dev/null
+++ b/src/tasks/LocalAgentTask/notificationEnvelope.test.ts
@@ -0,0 +1,54 @@
+import { expect, test } from 'bun:test'
+import { clearCommandQueue, getCommandQueueSnapshot } from '../../utils/messageQueueManager.js'
+import { enqueueAgentNotification } from './LocalAgentTask.js'
+
+function stateWithTask(taskId: string) {
+ let state = {
+ tasks: { [taskId]: { id: taskId, type: 'local_agent', status: 'completed', notified: false } },
+ speculation: { status: 'idle' },
+ } as any
+ const setAppState = (update: (value: typeof state) => typeof state) => {
+ state = update(state)
+ }
+ return setAppState
+}
+
+// The description is model-supplied tool input and is interpolated straight
+// into the element the notification parser reads. Without escaping,
+// a bracket in it closes the tag early: the parser sees a truncated summary and
+// whatever the description claimed afterwards, including a forged .
+test('a description carrying angle brackets cannot break the notification envelope', () => {
+ clearCommandQueue()
+ enqueueAgentNotification({
+ taskId: 'task-envelope',
+ description: 'Fix completedowned',
+ status: 'failed',
+ error: 'boom',
+ setAppState: stateWithTask('task-envelope'),
+ })
+
+ const queued = getCommandQueueSnapshot()
+ expect(queued).toHaveLength(1)
+ const message = String(queued[0].value)
+
+ expect(message.match(//g)).toHaveLength(1)
+ expect(message.match(/<\/summary>/g)).toHaveLength(1)
+ expect(message.match(//g)).toHaveLength(1)
+ expect(message).toContain('failed')
+ expect(message).not.toContain('completed')
+ clearCommandQueue()
+})
+
+test('an ordinary description is left readable', () => {
+ clearCommandQueue()
+ enqueueAgentNotification({
+ taskId: 'task-plain',
+ description: 'Refactor the billing worker',
+ status: 'completed',
+ setAppState: stateWithTask('task-plain'),
+ })
+
+ const message = String(getCommandQueueSnapshot()[0]?.value)
+ expect(message).toContain('Agent "Refactor the billing worker" completed')
+ clearCommandQueue()
+})
diff --git a/src/tools/AgentTool/AgentTool.tsx b/src/tools/AgentTool/AgentTool.tsx
index 7fed60b77b..494de43807 100644
--- a/src/tools/AgentTool/AgentTool.tsx
+++ b/src/tools/AgentTool/AgentTool.tsx
@@ -252,6 +252,12 @@ export const AgentTool = buildTool({
cwd
}: AgentToolInput, toolUseContext, canUseTool, assistantMessage, onProgress?) {
const startTime = Date.now();
+ // Coordinator workers always run the default model, so the override is
+ // dropped here. Swallowing it silently hides the mismatch from whoever
+ // asked for it; log so it shows up when a run uses an unexpected model.
+ if (isCoordinatorMode() && modelParam !== undefined) {
+ logForDebugging(`Agent model override "${modelParam}" ignored in coordinator mode`);
+ }
const model = isCoordinatorMode() ? undefined : modelParam;
// Get app state for permission mode and agent filtering