From e7189e2d52e177f875453ec8fedb7ee17263d89a Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sat, 11 Jul 2026 19:23:04 -0700 Subject: [PATCH] Add shared helper to abbreviate orchestration task specs for brief listi - Normalizes whitespace and caps spec length at 160 chars, flagging truncation separately from whitespace-only changes - Truncates on UTF-16 code point boundaries to avoid splitting surrogate pairs and emitting malformed strings --- src/shared/orchestration-task-summary.test.ts | 38 +++++++++++++++++++ src/shared/orchestration-task-summary.ts | 25 ++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 src/shared/orchestration-task-summary.test.ts create mode 100644 src/shared/orchestration-task-summary.ts diff --git a/src/shared/orchestration-task-summary.test.ts b/src/shared/orchestration-task-summary.test.ts new file mode 100644 index 00000000000..75fa6e68374 --- /dev/null +++ b/src/shared/orchestration-task-summary.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from 'vitest' +import { abbreviateOrchestrationTasks } from './orchestration-task-summary' + +describe('abbreviateOrchestrationTasks', () => { + it('collapses whitespace and caps long task specs', () => { + const [task] = abbreviateOrchestrationTasks([ + { id: 'task_1', spec: `First line\n\n${'detail '.repeat(40)}` } + ]) + + expect(task.id).toBe('task_1') + expect(task.spec).not.toContain('\n') + expect(task.spec).toHaveLength(160) + expect(task.spec.endsWith('…')).toBe(true) + expect(task.spec_truncated).toBe(true) + }) + + it('preserves a short one-line spec', () => { + const [task] = abbreviateOrchestrationTasks([{ spec: 'Short task' }]) + + expect(task).toEqual({ spec: 'Short task', spec_truncated: false }) + }) + + it('does not report whitespace normalization as truncation', () => { + const [task] = abbreviateOrchestrationTasks([{ spec: 'Short\n\n task' }]) + + expect(task).toEqual({ spec: 'Short task', spec_truncated: false }) + }) + + it('does not split a surrogate pair at the truncation boundary', () => { + // 158 chars + an astral emoji spanning UTF-16 units 158-159: a naive + // slice(0, 159) would cut the pair and leave a lone high surrogate. + const [task] = abbreviateOrchestrationTasks([{ spec: `${'a'.repeat(158)}😀${'b'.repeat(40)}` }]) + + expect(task.spec_truncated).toBe(true) + expect(task.spec.isWellFormed()).toBe(true) + expect(task.spec.endsWith('…')).toBe(true) + }) +}) diff --git a/src/shared/orchestration-task-summary.ts b/src/shared/orchestration-task-summary.ts new file mode 100644 index 00000000000..5c2cd46e40f --- /dev/null +++ b/src/shared/orchestration-task-summary.ts @@ -0,0 +1,25 @@ +const TASK_SPEC_BRIEF_LENGTH = 160 + +export function abbreviateOrchestrationTasks( + tasks: readonly T[] +): (T & { spec_truncated: boolean })[] { + return tasks.map((task) => { + const spec = task.spec.replace(/\s+/g, ' ').trim() + const truncated = spec.length > TASK_SPEC_BRIEF_LENGTH + return { + ...task, + spec: truncated ? `${truncateAtCodePoint(spec).trimEnd()}…` : spec, + // Why: whitespace normalization alone is not truncation; flagging it + // would make agents re-fetch full specs that --brief already shows. + spec_truncated: truncated + } + }) +} + +function truncateAtCodePoint(spec: string): string { + const sliced = spec.slice(0, TASK_SPEC_BRIEF_LENGTH - 1) + // Why: a cut through a surrogate pair leaves a lone high surrogate that + // strict JSON consumers reject; drop it rather than emit malformed UTF-16. + const lastUnit = sliced.charCodeAt(sliced.length - 1) + return lastUnit >= 0xd800 && lastUnit <= 0xdbff ? sliced.slice(0, -1) : sliced +}