fix(api): anchor retained phase partitions

This commit is contained in:
Danny Avila 2026-08-13 08:14:05 -04:00
parent 69c117bb1c
commit 2d7597bd30
4 changed files with 64 additions and 3 deletions

View file

@ -253,7 +253,8 @@ describe('groupActivityPhases', () => {
type: ContentTypes.TEXT,
text: 'Substantial result',
} as TMessageContentParts;
const content = [tool, tool, boundary, tool, first as never, tool, second as never];
const final = { type: ContentTypes.TEXT, text: 'Final result' } as TMessageContentParts;
const content = [tool, tool, boundary, tool, first as never, tool, final, second as never];
const segments = groupActivityPhases(content);
@ -272,9 +273,14 @@ describe('groupActivityPhases', () => {
expect.objectContaining({
type: 'phase',
startIndex: 3,
labelIndex: 6,
labelIndex: 7,
contentIndices: [3, 5],
}),
expect.objectContaining({
type: 'content',
startIndex: 6,
contentIndices: [6],
}),
]);
});

View file

@ -192,6 +192,7 @@ export function groupActivityPhases(
* segment so a later phase marker can still claim its declared span. */
if (start < cursor) {
const recoveredIndices: number[] = [];
const deferredTrailingIndices: number[] = [];
for (let segmentIndex = segments.length - 1; segmentIndex >= 0; segmentIndex -= 1) {
const segment = segments[segmentIndex];
if (segment.type !== 'content') {
@ -207,6 +208,8 @@ export function groupActivityPhases(
const childIndex = segment.contentIndices[childPosition];
if (childIndex >= start && childIndex < end) {
recoveredIndices.push(childIndex);
} else if (childIndex >= end) {
deferredTrailingIndices.push(childIndex);
} else {
retainedContent.push(segment.content[childPosition]);
retainedIndices.push(childIndex);
@ -224,6 +227,10 @@ export function groupActivityPhases(
for (const recoveredIndex of recoveredIndices) {
append(phase, recoveredIndex);
}
deferredTrailingIndices.sort((a, b) => a - b);
for (const trailingIndex of deferredTrailingIndices) {
append(trailing, trailingIndex);
}
}
while (definedPosition < definedIndices.length && definedIndices[definedPosition] < index) {
const childIndex = definedIndices[definedPosition];

View file

@ -1210,6 +1210,50 @@ describe('createActivityPhaseWiring', () => {
});
});
it('anchors a phase represented entirely by retained overflow bookkeeping', async () => {
const parts: LooseContentPart[] = [
{ type: ContentTypes.THINK, think: 'Earlier retained work' },
{ type: ContentTypes.TEXT, text: substantialText('Persisted intermediate result.') },
{ type: ContentTypes.TOOL_CALL, tool_call: { id: 'overflow-a' } },
{ type: ContentTypes.TOOL_CALL, tool_call: { id: 'overflow-b' } },
];
const wiring = createActivityPhaseWiring({
initialSnapshot: {
version: 1,
generated: 0,
activityCount: 15,
failedActivityCount: 0,
partialActivityCount: 0,
agentIds: [],
activities: Array.from({ length: 13 }, () => ({
startIndex: 0,
status: 'success' as const,
})),
overflowActivityStartIndex: 30,
overflowToolCallIds: ['overflow-a', 'overflow-b'],
overflowBoundaryToolCallIds: ['overflow-a', 'overflow-b'],
assistantContext: [],
pendingReasoning: [],
},
getContentParts: () => parts,
bumpIndexOffset: jest.fn(),
emitLabelEvent: jest.fn(async () => undefined),
trackPendingFill: jest.fn(),
generatePhase: jest.fn(async () => ({ label: 'Completed the overflow phase' })),
});
wiring.complete();
await flushDetached();
expect(parts[5]).toMatchObject({
activity_start_index: 2,
activity_end_index: 5,
activity_count: 2,
});
expect(Number.isFinite(parts[5]?.activity_start_index)).toBe(true);
expect(Number.isFinite(parts[5]?.activity_end_index)).toBe(true);
});
it('rebases a resumed overflow anchor after content compaction', async () => {
const parts: LooseContentPart[] = Array.from({ length: 13 }, (_, index) => ({
type: ContentTypes.TOOL_CALL,

View file

@ -815,6 +815,7 @@ export function createActivityPhaseWiring(deps: ActivityPhaseHostDeps): Activity
overflowCount > 0 &&
(requestedEndIndex == null ||
(overflowActivityStartIndex != null && overflowActivityStartIndex < requestedEndIndex));
const closingOverflowStartIndex = overflowCloses ? overflowActivityStartIndex : undefined;
const failedCount =
snapshot.filter((activity) => activity.status === 'error').length +
(overflowCloses ? overflowFailedCount : 0);
@ -846,7 +847,10 @@ export function createActivityPhaseWiring(deps: ActivityPhaseHostDeps): Activity
generated += 1;
const phaseIndex = generated - 1;
let startIndex = Math.min(...snapshot.map((activity) => activity.startIndex));
let startIndex =
snapshot.length > 0
? Math.min(...snapshot.map((activity) => activity.startIndex))
: (closingOverflowStartIndex ?? 0);
/** Pull leading commentary/reasoning into the parent card. A prior phase
* marker or steer is the only hard UI boundary; plain text can be
* intermediate context on providers that do not expose phase metadata. */