From aacf42c7ebe96788d739fcdc0b73a36ff7d6310b Mon Sep 17 00:00:00 2001 From: ata <111813260+atacolak@users.noreply.github.com> Date: Mon, 17 Aug 2026 17:01:33 +1000 Subject: [PATCH 1/7] fix(tui): show subagent role and drop echoed HUD labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The anchored Subagents list printed only `Id: description` and treated a label that repeated the spawn handle as a real description. Show the same ⟨role⟩ badge as inline task rows and omit descriptions that only echo the id. --- packages/coding-agent/CHANGELOG.md | 4 +++ .../src/modes/interactive-mode.ts | 25 +++++++++++------ packages/coding-agent/src/task/render.ts | 2 +- .../test/subagent-hud-render.test.ts | 28 +++++++++++++++++++ 4 files changed, 50 insertions(+), 9 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 2f7b500ff..341928737 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -21,6 +21,10 @@ ### Fixed - Fixed `omp stats` and `/stats` dashboards being unreachable from container hosts by accepting an explicit `--host` bind address while preserving the `127.0.0.1` default. +### Fixed + +- Fixed the anchored Subagents HUD omitting the spawned agent role and repeating the spawn id as `Name: Name` when the generated label echoed the handle. + ## [17.3.5] - 2026-08-16 diff --git a/packages/coding-agent/src/modes/interactive-mode.ts b/packages/coding-agent/src/modes/interactive-mode.ts index ec6eb9c6b..2cb0b2487 100644 --- a/packages/coding-agent/src/modes/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive-mode.ts @@ -117,7 +117,7 @@ import { BUILTIN_SLASH_COMMAND_RESERVED_NAMES, buildTuiBuiltinSlashCommands } fr import { formatDuration } from "../slash-commands/helpers/format"; import { STTController, type SttState } from "../stt"; import { discoverTitleSystemPromptFile, resolvePromptInput } from "../system-prompt"; -import { formatTaskId } from "../task/render"; +import { agentTypeBadge, formatTaskId } from "../task/render"; import type { ConfiguredThinkingLevel } from "../thinking"; import { tinyTitleClient } from "../tiny/title-client"; import type { LspStartupServerInfo } from "../tools"; @@ -452,8 +452,8 @@ const SUBAGENT_OBSERVER_UI_COALESCE_MS = 100; /** * Build the anchored subagent HUD block: a bold accent "Subagents" header plus - * a bounded set of running-agent rows in the same `Id: description` shape the - * inline task rows use (muted task preview when no description was given). + * a bounded set of running-agent rows in the same `Id ⟨role⟩: description` shape + * the inline task rows use (muted task preview when no description was given). * Layout mirrors the Todos HUD exactly: unindented header, then * `renderTreeList` rows (dim connectors) shifted right by one space. * Only detached background spawns are listed: a sync task call blocks the @@ -476,16 +476,25 @@ export function renderSubagentHudLines(sessions: ObservableSession[], columns: n expanded: true, renderItem: session => { const displayId = formatTaskId(session.id); - let line = `${dot} ${theme.fg("accent", theme.bold(displayId))}`; + const role = session.agent ?? session.progress?.agent; + const badge = agentTypeBadge(role, theme); + let line = `${dot} ${theme.fg("accent", theme.bold(displayId))}${badge}`; const description = session.description?.trim() || session.progress?.description?.trim(); - if (description) { - const budget = Math.max(TRUNCATE_LENGTHS.SHORT, columns - visibleWidth(displayId) - 10); - line += `${theme.fg("accent", ":")} ${theme.fg("accent", truncateToWidth(replaceTabs(description), budget))}`; + const distinctDescription = + description && description.localeCompare(session.id, undefined, { sensitivity: "accent" }) !== 0 + ? description + : undefined; + if (distinctDescription) { + const budget = Math.max( + TRUNCATE_LENGTHS.SHORT, + columns - visibleWidth(displayId) - visibleWidth(Bun.stripANSI(badge)) - 10, + ); + line += `${theme.fg("accent", ":")} ${theme.fg("accent", truncateToWidth(replaceTabs(distinctDescription), budget))}`; } else { // No spawn description: fall back to a muted task preview, same as // the inline task rows when a row has no label. const taskPreview = session.progress?.task?.trim(); - if (taskPreview) { + if (taskPreview && taskPreview.localeCompare(session.id, undefined, { sensitivity: "accent" }) !== 0) { line += ` ${theme.fg("muted", truncateToWidth(replaceTabs(taskPreview), TRUNCATE_LENGTHS.SHORT))}`; } } diff --git a/packages/coding-agent/src/task/render.ts b/packages/coding-agent/src/task/render.ts index 585e4e740..52ca6fe43 100644 --- a/packages/coding-agent/src/task/render.ts +++ b/packages/coding-agent/src/task/render.ts @@ -710,7 +710,7 @@ function formatAgentHeaderLabel(args: Partial | undefined): string | } /** Dim `⟨agent⟩` badge for a non-default agent type; empty for the generic worker. */ -function agentTypeBadge(agent: string | undefined, theme: Theme): string { +export function agentTypeBadge(agent: string | undefined, theme: Theme): string { const trimmed = agent?.trim(); if (!trimmed || trimmed === "task") return ""; return ` ${theme.fg("dim", `${theme.format.bracketLeft}${trimmed}${theme.format.bracketRight}`)}`; diff --git a/packages/coding-agent/test/subagent-hud-render.test.ts b/packages/coding-agent/test/subagent-hud-render.test.ts index 4f1929b30..2157b5717 100644 --- a/packages/coding-agent/test/subagent-hud-render.test.ts +++ b/packages/coding-agent/test/subagent-hud-render.test.ts @@ -107,6 +107,34 @@ describe("subagent HUD lines", () => { expect(out).toContain("SchemaMigrator: Migrating the users table"); }); + it("shows a non-default role badge and hides descriptions that only echo the id", () => { + const withRole = render([ + makeSession({ + id: "HindsightMcpFunnel", + agent: "scout", + description: "Audit MCP funnel wiring", + }), + ]); + expect(withRole).toContain("HindsightMcpFunnel"); + expect(withRole).toMatch(/HindsightMcpFunnel.*scout/); + expect(withRole).toContain("Audit MCP funnel wiring"); + + const echoed = render([ + makeSession({ + id: "HindsightMcpFunnel", + agent: "scout", + description: "HindsightMcpFunnel", + }), + ]); + expect(echoed).toContain("HindsightMcpFunnel"); + expect(echoed).toMatch(/HindsightMcpFunnel.*scout/); + expect(echoed).not.toContain("HindsightMcpFunnel: HindsightMcpFunnel"); + + const defaultWorker = render([makeSession({ id: "AuthLoader", agent: "task", description: "Refactor auth" })]); + expect(defaultWorker).toContain("AuthLoader: Refactor auth"); + expect(defaultWorker).not.toMatch(/AuthLoader.*task/); + }); + it("only shows active subagents and clears once everything finished", () => { const finishedStates = ["completed", "failed", "aborted"] as const; const sessions: ObservableSession[] = [ From 54ba7fa4ab7a5ca2ccad3beb2d16e2eb189b0c1d Mon Sep 17 00:00:00 2001 From: ata <111813260+atacolak@users.noreply.github.com> Date: Mon, 17 Aug 2026 17:09:20 +1000 Subject: [PATCH 2/7] fix(tui): treat Name-N HUD labels as echoed spawn ids Collision suffixes such as HindsightMcpFunnel-3 were still shown as Name-3: Name because the first pass only compared the raw id. --- .../coding-agent/src/modes/interactive-mode.ts | 14 +++++++++----- .../coding-agent/test/subagent-hud-render.test.ts | 11 +++++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/coding-agent/src/modes/interactive-mode.ts b/packages/coding-agent/src/modes/interactive-mode.ts index 2cb0b2487..02e08c6f7 100644 --- a/packages/coding-agent/src/modes/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive-mode.ts @@ -450,6 +450,13 @@ const MODEL_CYCLE_TRACK_CLEAR_MS = 4000; const SUBAGENT_HUD_VISIBLE_LIMIT = 8; const SUBAGENT_OBSERVER_UI_COALESCE_MS = 100; +/** True when `label` is the spawn handle, including collision suffixes (`Name-2`). */ +function hudLabelEchoesId(id: string, label: string): boolean { + if (label.localeCompare(id, undefined, { sensitivity: "accent" }) === 0) return true; + const suffix = id.startsWith(`${label}-`) ? id.slice(label.length + 1) : ""; + return /^\d+$/.test(suffix); +} + /** * Build the anchored subagent HUD block: a bold accent "Subagents" header plus * a bounded set of running-agent rows in the same `Id ⟨role⟩: description` shape @@ -480,10 +487,7 @@ export function renderSubagentHudLines(sessions: ObservableSession[], columns: n const badge = agentTypeBadge(role, theme); let line = `${dot} ${theme.fg("accent", theme.bold(displayId))}${badge}`; const description = session.description?.trim() || session.progress?.description?.trim(); - const distinctDescription = - description && description.localeCompare(session.id, undefined, { sensitivity: "accent" }) !== 0 - ? description - : undefined; + const distinctDescription = description && !hudLabelEchoesId(session.id, description) ? description : undefined; if (distinctDescription) { const budget = Math.max( TRUNCATE_LENGTHS.SHORT, @@ -494,7 +498,7 @@ export function renderSubagentHudLines(sessions: ObservableSession[], columns: n // No spawn description: fall back to a muted task preview, same as // the inline task rows when a row has no label. const taskPreview = session.progress?.task?.trim(); - if (taskPreview && taskPreview.localeCompare(session.id, undefined, { sensitivity: "accent" }) !== 0) { + if (taskPreview && !hudLabelEchoesId(session.id, taskPreview)) { line += ` ${theme.fg("muted", truncateToWidth(replaceTabs(taskPreview), TRUNCATE_LENGTHS.SHORT))}`; } } diff --git a/packages/coding-agent/test/subagent-hud-render.test.ts b/packages/coding-agent/test/subagent-hud-render.test.ts index 2157b5717..2543235e0 100644 --- a/packages/coding-agent/test/subagent-hud-render.test.ts +++ b/packages/coding-agent/test/subagent-hud-render.test.ts @@ -130,6 +130,17 @@ describe("subagent HUD lines", () => { expect(echoed).toMatch(/HindsightMcpFunnel.*scout/); expect(echoed).not.toContain("HindsightMcpFunnel: HindsightMcpFunnel"); + const collision = render([ + makeSession({ + id: "HindsightMcpFunnel-3", + agent: "scout", + description: "HindsightMcpFunnel", + }), + ]); + expect(collision).toContain("HindsightMcpFunnel-3"); + expect(collision).toMatch(/HindsightMcpFunnel-3.*scout/); + expect(collision).not.toContain("HindsightMcpFunnel-3: HindsightMcpFunnel"); + const defaultWorker = render([makeSession({ id: "AuthLoader", agent: "task", description: "Refactor auth" })]); expect(defaultWorker).toContain("AuthLoader: Refactor auth"); expect(defaultWorker).not.toMatch(/AuthLoader.*task/); From 94417ec99b348afdf722ada10720690eedc9acce Mon Sep 17 00:00:00 2001 From: ata <111813260+atacolak@users.noreply.github.com> Date: Mon, 17 Aug 2026 17:10:25 +1000 Subject: [PATCH 3/7] test(tui): use generic names in subagent HUD fixtures Keep the role-badge and echoed-id cases, but drop the session-specific spawn handle from the source tree. --- .../test/subagent-hud-render.test.ts | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/coding-agent/test/subagent-hud-render.test.ts b/packages/coding-agent/test/subagent-hud-render.test.ts index 2543235e0..e6fede4d3 100644 --- a/packages/coding-agent/test/subagent-hud-render.test.ts +++ b/packages/coding-agent/test/subagent-hud-render.test.ts @@ -110,40 +110,40 @@ describe("subagent HUD lines", () => { it("shows a non-default role badge and hides descriptions that only echo the id", () => { const withRole = render([ makeSession({ - id: "HindsightMcpFunnel", + id: "AuthLoader", agent: "scout", - description: "Audit MCP funnel wiring", + description: "Refactor the auth flow", }), ]); - expect(withRole).toContain("HindsightMcpFunnel"); - expect(withRole).toMatch(/HindsightMcpFunnel.*scout/); - expect(withRole).toContain("Audit MCP funnel wiring"); + expect(withRole).toContain("AuthLoader"); + expect(withRole).toMatch(/AuthLoader.*scout/); + expect(withRole).toContain("Refactor the auth flow"); const echoed = render([ makeSession({ - id: "HindsightMcpFunnel", + id: "AuthLoader", agent: "scout", - description: "HindsightMcpFunnel", + description: "AuthLoader", }), ]); - expect(echoed).toContain("HindsightMcpFunnel"); - expect(echoed).toMatch(/HindsightMcpFunnel.*scout/); - expect(echoed).not.toContain("HindsightMcpFunnel: HindsightMcpFunnel"); + expect(echoed).toContain("AuthLoader"); + expect(echoed).toMatch(/AuthLoader.*scout/); + expect(echoed).not.toContain("AuthLoader: AuthLoader"); const collision = render([ makeSession({ - id: "HindsightMcpFunnel-3", + id: "AuthLoader-3", agent: "scout", - description: "HindsightMcpFunnel", + description: "AuthLoader", }), ]); - expect(collision).toContain("HindsightMcpFunnel-3"); - expect(collision).toMatch(/HindsightMcpFunnel-3.*scout/); - expect(collision).not.toContain("HindsightMcpFunnel-3: HindsightMcpFunnel"); + expect(collision).toContain("AuthLoader-3"); + expect(collision).toMatch(/AuthLoader-3.*scout/); + expect(collision).not.toContain("AuthLoader-3: AuthLoader"); - const defaultWorker = render([makeSession({ id: "AuthLoader", agent: "task", description: "Refactor auth" })]); - expect(defaultWorker).toContain("AuthLoader: Refactor auth"); - expect(defaultWorker).not.toMatch(/AuthLoader.*task/); + const defaultWorker = render([makeSession({ id: "SchemaMigrator", agent: "task", description: "Migrate users" })]); + expect(defaultWorker).toContain("SchemaMigrator: Migrate users"); + expect(defaultWorker).not.toMatch(/SchemaMigrator.*task/); }); it("only shows active subagents and clears once everything finished", () => { From 8a83fb0e5d8a903a155a5a29fa8bb9b6c5cd6df1 Mon Sep 17 00:00:00 2001 From: ata <111813260+atacolak@users.noreply.github.com> Date: Mon, 17 Aug 2026 17:37:25 +1000 Subject: [PATCH 4/7] fix(task): stop using the spawn handle as the HUD description The first HUD commit hid Name: Name. The cause was earlier: task name was copied into identity.label, which became progress.description and skipped generateTaskLabel. Keep the handle for id allocation, but only treat eval label as a real UI description so the tiny-model summary can run. --- packages/coding-agent/CHANGELOG.md | 1 + .../src/modes/interactive-mode.ts | 12 ++------ packages/coding-agent/src/task/index.ts | 2 ++ packages/coding-agent/src/task/label.ts | 12 +++++++- .../src/task/structured-subagent.ts | 8 +++-- packages/coding-agent/test/task-label.test.ts | 30 +++++++++++++++++++ .../test/task/structured-subagent.test.ts | 28 +++++++++++++++++ 7 files changed, 81 insertions(+), 12 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 341928737..4688f3d3e 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -24,6 +24,7 @@ ### Fixed - Fixed the anchored Subagents HUD omitting the spawned agent role and repeating the spawn id as `Name: Name` when the generated label echoed the handle. +- Stopped the task tool from using the spawn handle as the HUD description, which blocked tiny-model label generation and produced `Name: Name`. ## [17.3.5] - 2026-08-16 diff --git a/packages/coding-agent/src/modes/interactive-mode.ts b/packages/coding-agent/src/modes/interactive-mode.ts index 02e08c6f7..23c995523 100644 --- a/packages/coding-agent/src/modes/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive-mode.ts @@ -117,6 +117,7 @@ import { BUILTIN_SLASH_COMMAND_RESERVED_NAMES, buildTuiBuiltinSlashCommands } fr import { formatDuration } from "../slash-commands/helpers/format"; import { STTController, type SttState } from "../stt"; import { discoverTitleSystemPromptFile, resolvePromptInput } from "../system-prompt"; +import { labelEchoesHandle } from "../task/label"; import { agentTypeBadge, formatTaskId } from "../task/render"; import type { ConfiguredThinkingLevel } from "../thinking"; import { tinyTitleClient } from "../tiny/title-client"; @@ -450,13 +451,6 @@ const MODEL_CYCLE_TRACK_CLEAR_MS = 4000; const SUBAGENT_HUD_VISIBLE_LIMIT = 8; const SUBAGENT_OBSERVER_UI_COALESCE_MS = 100; -/** True when `label` is the spawn handle, including collision suffixes (`Name-2`). */ -function hudLabelEchoesId(id: string, label: string): boolean { - if (label.localeCompare(id, undefined, { sensitivity: "accent" }) === 0) return true; - const suffix = id.startsWith(`${label}-`) ? id.slice(label.length + 1) : ""; - return /^\d+$/.test(suffix); -} - /** * Build the anchored subagent HUD block: a bold accent "Subagents" header plus * a bounded set of running-agent rows in the same `Id ⟨role⟩: description` shape @@ -487,7 +481,7 @@ export function renderSubagentHudLines(sessions: ObservableSession[], columns: n const badge = agentTypeBadge(role, theme); let line = `${dot} ${theme.fg("accent", theme.bold(displayId))}${badge}`; const description = session.description?.trim() || session.progress?.description?.trim(); - const distinctDescription = description && !hudLabelEchoesId(session.id, description) ? description : undefined; + const distinctDescription = description && !labelEchoesHandle(session.id, description) ? description : undefined; if (distinctDescription) { const budget = Math.max( TRUNCATE_LENGTHS.SHORT, @@ -498,7 +492,7 @@ export function renderSubagentHudLines(sessions: ObservableSession[], columns: n // No spawn description: fall back to a muted task preview, same as // the inline task rows when a row has no label. const taskPreview = session.progress?.task?.trim(); - if (taskPreview && !hudLabelEchoesId(session.id, taskPreview)) { + if (taskPreview && !labelEchoesHandle(session.id, taskPreview)) { line += ` ${theme.fg("muted", truncateToWidth(replaceTabs(taskPreview), TRUNCATE_LENGTHS.SHORT))}`; } } diff --git a/packages/coding-agent/src/task/index.ts b/packages/coding-agent/src/task/index.ts index a2fe5ca8b..7b59e6cf0 100644 --- a/packages/coding-agent/src/task/index.ts +++ b/packages/coding-agent/src/task/index.ts @@ -1424,6 +1424,8 @@ export class TaskTool implements AgentTool { expect(requestSignal).toBe(controller.signal); expect(await label).toBeNull(); }); + + it("rejects a generated label that only echoes the spawn handle", async () => { + const model = getModelOrThrow("claude-sonnet-4-5"); + vi.spyOn(ai, "completeSimple").mockResolvedValue({ + stopReason: "stop", + content: [{ type: "text", text: "AuthLoader" }], + } as never); + + const echoed = await generateTaskLabel( + "Sleep forty seconds then reply done", + createRegistry(model), + createSettings(model), + "AuthLoader", + ); + expect(echoed).toBeNull(); + + vi.spyOn(ai, "completeSimple").mockResolvedValue({ + stopReason: "stop", + content: [{ type: "text", text: "Sleep then reply done" }], + } as never); + const labeled = await generateTaskLabel( + "Sleep forty seconds then reply done", + createRegistry(model), + createSettings(model), + "AuthLoader", + ); + expect(labeled).toBe("Sleep then reply done"); + }); + + }); diff --git a/packages/coding-agent/test/task/structured-subagent.test.ts b/packages/coding-agent/test/task/structured-subagent.test.ts index a24facde7..9f6b0d009 100644 --- a/packages/coding-agent/test/task/structured-subagent.test.ts +++ b/packages/coding-agent/test/task/structured-subagent.test.ts @@ -186,6 +186,34 @@ describe("structured subagent primitive", () => { expect(settled.result.modelRole).toBe("reviewer"); await fs.rm(settled.artifactsDir, { recursive: true, force: true }); }); + it("does not treat a spawn handle as the HUD description", async () => { + mockDiscovery(); + const dispatched: executorModule.ExecutorOptions[] = []; + vi.spyOn(executorModule, "runSubprocess").mockImplementation(async options => { + dispatched.push(options); + return result(); + }); + + const handleOnly = await runStructuredSubagent( + request({ identity: { id: "AuthLoader", label: "AuthLoader" }, retainArtifacts: true }), + ); + expect(dispatched[0]?.description).toBeUndefined(); + expect(dispatched[0]?.id).toBe("AuthLoader"); + await fs.rm(handleOnly.artifactsDir, { recursive: true, force: true }); + + dispatched.length = 0; + const evalLabeled = await runStructuredSubagent( + request({ + invocationKind: "eval", + identity: { label: "Refactor the auth flow" }, + retainArtifacts: true, + }), + ); + expect(dispatched[0]?.description).toBe("Refactor the auth flow"); + await fs.rm(evalLabeled.artifactsDir, { recursive: true, force: true }); + }); + + it("derives modelRole from the raw selector source in request, override, definition order", async () => { const customAgent = { ...AGENT, model: ["@definition"] }; mockDiscovery(customAgent); From 4509c128df89daa72f2a3724a34a2d9194c0dcad Mon Sep 17 00:00:00 2001 From: ata <111813260+atacolak@users.noreply.github.com> Date: Mon, 17 Aug 2026 17:41:14 +1000 Subject: [PATCH 5/7] style: biome-format HUD and task-label files CI lint failed on line wrapping and extra blank lines in the HUD role/label changes. No behavior change. --- packages/coding-agent/src/modes/interactive-mode.ts | 3 ++- packages/coding-agent/src/task/structured-subagent.ts | 6 ++---- packages/coding-agent/test/subagent-hud-render.test.ts | 4 +++- packages/coding-agent/test/task-label.test.ts | 2 -- packages/coding-agent/test/task/structured-subagent.test.ts | 1 - 5 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/coding-agent/src/modes/interactive-mode.ts b/packages/coding-agent/src/modes/interactive-mode.ts index 23c995523..e268045e4 100644 --- a/packages/coding-agent/src/modes/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive-mode.ts @@ -481,7 +481,8 @@ export function renderSubagentHudLines(sessions: ObservableSession[], columns: n const badge = agentTypeBadge(role, theme); let line = `${dot} ${theme.fg("accent", theme.bold(displayId))}${badge}`; const description = session.description?.trim() || session.progress?.description?.trim(); - const distinctDescription = description && !labelEchoesHandle(session.id, description) ? description : undefined; + const distinctDescription = + description && !labelEchoesHandle(session.id, description) ? description : undefined; if (distinctDescription) { const budget = Math.max( TRUNCATE_LENGTHS.SHORT, diff --git a/packages/coding-agent/src/task/structured-subagent.ts b/packages/coding-agent/src/task/structured-subagent.ts index d1c4c4005..072e8abda 100644 --- a/packages/coding-agent/src/task/structured-subagent.ts +++ b/packages/coding-agent/src/task/structured-subagent.ts @@ -395,8 +395,7 @@ function buildExecutorOptions( planReference: undefined, // Task `name` is the spawn handle (id allocation). Eval `label` is a // real UI description. Copy it only for eval so generateTaskLabel can run. - description: - request.invocationKind === "eval" ? trimToUndefined(request.identity?.label) : undefined, + description: request.invocationKind === "eval" ? trimToUndefined(request.identity?.label) : undefined, index: request.index ?? 0, parentToolCallId: request.parentToolCallId, detached: request.detached, @@ -479,8 +478,7 @@ function buildFailureResult( agentSource: policy.agent.source, task: renderSubagentPrompt(request.assignment), assignment: request.assignment.trim(), - description: - request.invocationKind === "eval" ? trimToUndefined(request.identity?.label) : undefined, + description: request.invocationKind === "eval" ? trimToUndefined(request.identity?.label) : undefined, exitCode: 1, output: "", stderr: message, diff --git a/packages/coding-agent/test/subagent-hud-render.test.ts b/packages/coding-agent/test/subagent-hud-render.test.ts index e6fede4d3..91db8c220 100644 --- a/packages/coding-agent/test/subagent-hud-render.test.ts +++ b/packages/coding-agent/test/subagent-hud-render.test.ts @@ -141,7 +141,9 @@ describe("subagent HUD lines", () => { expect(collision).toMatch(/AuthLoader-3.*scout/); expect(collision).not.toContain("AuthLoader-3: AuthLoader"); - const defaultWorker = render([makeSession({ id: "SchemaMigrator", agent: "task", description: "Migrate users" })]); + const defaultWorker = render([ + makeSession({ id: "SchemaMigrator", agent: "task", description: "Migrate users" }), + ]); expect(defaultWorker).toContain("SchemaMigrator: Migrate users"); expect(defaultWorker).not.toMatch(/SchemaMigrator.*task/); }); diff --git a/packages/coding-agent/test/task-label.test.ts b/packages/coding-agent/test/task-label.test.ts index 9aa3a10e7..4a1ee4206 100644 --- a/packages/coding-agent/test/task-label.test.ts +++ b/packages/coding-agent/test/task-label.test.ts @@ -93,6 +93,4 @@ describe("task label generation", () => { ); expect(labeled).toBe("Sleep then reply done"); }); - - }); diff --git a/packages/coding-agent/test/task/structured-subagent.test.ts b/packages/coding-agent/test/task/structured-subagent.test.ts index 9f6b0d009..358d38a4d 100644 --- a/packages/coding-agent/test/task/structured-subagent.test.ts +++ b/packages/coding-agent/test/task/structured-subagent.test.ts @@ -213,7 +213,6 @@ describe("structured subagent primitive", () => { await fs.rm(evalLabeled.artifactsDir, { recursive: true, force: true }); }); - it("derives modelRole from the raw selector source in request, override, definition order", async () => { const customAgent = { ...AGENT, model: ["@definition"] }; mockDiscovery(customAgent); From 96cb5b5d953d303e4aa945a5a2e8f5f3e0641e75 Mon Sep 17 00:00:00 2001 From: ata <111813260+atacolak@users.noreply.github.com> Date: Tue, 18 Aug 2026 01:27:55 +1000 Subject: [PATCH 6/7] fix(task): treat case-insensitive Name-N labels as handle echoes The exact-echo check used accent-insensitive compare, but the collision suffix path was a case-sensitive startsWith. AuthLoader-3 vs authloader therefore leaked through as a real description. --- packages/coding-agent/src/task/label.ts | 7 +++++-- packages/coding-agent/test/subagent-hud-render.test.ts | 10 ++++++++++ packages/coding-agent/test/task-label.test.ts | 9 ++++++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/coding-agent/src/task/label.ts b/packages/coding-agent/src/task/label.ts index e24ea6fb4..cd0f861c0 100644 --- a/packages/coding-agent/src/task/label.ts +++ b/packages/coding-agent/src/task/label.ts @@ -13,8 +13,11 @@ const TASK_LABEL_SYSTEM_PROMPT = prompt.render(taskLabelSystemPrompt); export function labelEchoesHandle(handle: string | undefined, label: string): boolean { if (!handle) return false; if (label.localeCompare(handle, undefined, { sensitivity: "accent" }) === 0) return true; - const suffix = handle.startsWith(`${label}-`) ? handle.slice(label.length + 1) : ""; - return /^\d+$/.test(suffix); + const separator = handle.lastIndexOf("-"); + if (separator <= 0) return false; + const prefix = handle.slice(0, separator); + const suffix = handle.slice(separator + 1); + return /^\d+$/.test(suffix) && prefix.localeCompare(label, undefined, { sensitivity: "accent" }) === 0; } /** Compresses a delegated assignment into a one-sentence UI label via the tiny title model — fired by the executor spawn path because the task wire schema no longer carries a `description`; null on empty input or failure. */ diff --git a/packages/coding-agent/test/subagent-hud-render.test.ts b/packages/coding-agent/test/subagent-hud-render.test.ts index 91db8c220..e2086fc15 100644 --- a/packages/coding-agent/test/subagent-hud-render.test.ts +++ b/packages/coding-agent/test/subagent-hud-render.test.ts @@ -141,6 +141,16 @@ describe("subagent HUD lines", () => { expect(collision).toMatch(/AuthLoader-3.*scout/); expect(collision).not.toContain("AuthLoader-3: AuthLoader"); + const mixedCase = render([ + makeSession({ + id: "AuthLoader-3", + agent: "scout", + description: "authloader", + }), + ]); + expect(mixedCase).toContain("AuthLoader-3"); + expect(mixedCase).not.toContain("AuthLoader-3: authloader"); + const defaultWorker = render([ makeSession({ id: "SchemaMigrator", agent: "task", description: "Migrate users" }), ]); diff --git a/packages/coding-agent/test/task-label.test.ts b/packages/coding-agent/test/task-label.test.ts index 4a1ee4206..17f678eba 100644 --- a/packages/coding-agent/test/task-label.test.ts +++ b/packages/coding-agent/test/task-label.test.ts @@ -2,7 +2,7 @@ import { afterEach, describe, expect, it, vi } from "bun:test"; import type { Api, Model } from "@oh-my-pi/pi-ai"; import * as ai from "@oh-my-pi/pi-ai"; import { getBundledModel } from "@oh-my-pi/pi-catalog/models"; -import { generateTaskLabel } from "@oh-my-pi/pi-coding-agent/task/label"; +import { generateTaskLabel, labelEchoesHandle } from "@oh-my-pi/pi-coding-agent/task/label"; function getModelOrThrow(id: string): Model { const model = getBundledModel("anthropic", id); @@ -93,4 +93,11 @@ describe("task label generation", () => { ); expect(labeled).toBe("Sleep then reply done"); }); + + it("treats a case-insensitive Name-N collision as an echoed handle", () => { + expect(labelEchoesHandle("AuthLoader-3", "authloader")).toBe(true); + expect(labelEchoesHandle("AuthLoader-3", "AuthLoader")).toBe(true); + expect(labelEchoesHandle("AuthLoader", "authloader")).toBe(true); + expect(labelEchoesHandle("AuthLoader-3", "Migrate users")).toBe(false); + }); }); From 602c89ae80e8a493903d15a00b24947dc6f92c07 Mon Sep 17 00:00:00 2001 From: ata <111813260+atacolak@users.noreply.github.com> Date: Tue, 18 Aug 2026 01:29:21 +1000 Subject: [PATCH 7/7] docs(changelog): collapse HUD label notes into one Unreleased bullet Keep the 17.3.6 notes verbatim and record the HUD/handle split as a single Unreleased fix. --- packages/coding-agent/CHANGELOG.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 4688f3d3e..b3c78c478 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed the Subagents HUD role display and restored generated task labels by keeping spawn handles separate from UI descriptions. + ## [17.3.7] - 2026-08-17 ### Changed @@ -21,11 +25,6 @@ ### Fixed - Fixed `omp stats` and `/stats` dashboards being unreachable from container hosts by accepting an explicit `--host` bind address while preserving the `127.0.0.1` default. -### Fixed - -- Fixed the anchored Subagents HUD omitting the spawned agent role and repeating the spawn id as `Name: Name` when the generated label echoed the handle. -- Stopped the task tool from using the spawn handle as the HUD description, which blocked tiny-model label generation and produced `Name: Name`. - ## [17.3.5] - 2026-08-16