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.
This commit is contained in:
ata
2026-08-18 01:27:55 +10:00
parent 4509c128df
commit 96cb5b5d95
3 changed files with 23 additions and 3 deletions
+5 -2
View File
@@ -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. */
@@ -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" }),
]);
@@ -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<Api> {
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);
});
});