Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions packages/core/src/utils/environmentContext.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,69 @@ describe('getStartupContextLength', () => {
);
expect(getStartupContextLength([merged])).toBe(0);
});

// Compressed history prefix: composePostCompactHistory produces
// [user(summary), model(ack), user(postAckParts)?, ...]. The ack sentinel
// is distinct from the legacy ack above.

it('is 2 for a compressed prefix without attachments', () => {
const history: Content[] = [
{
role: 'user',
parts: [{ text: 'summary text\n\nResume the prior task...' }],
},
{
role: 'model',
parts: [{ text: 'Got it. Thanks for the additional context!' }],
},
];
expect(getStartupContextLength(history)).toBe(2);
});

it('is 3 for a compressed prefix with post-compact attachments', () => {
const history: Content[] = [
{
role: 'user',
parts: [{ text: 'summary text\n\nResume the prior task...' }],
},
{
role: 'model',
parts: [{ text: 'Got it. Thanks for the additional context!' }],
},
{
role: 'user',
parts: [
{ text: 'file restoration content' },
{ text: '<system-reminder>\nplan mode\n</system-reminder>' },
],
},
];
expect(getStartupContextLength(history)).toBe(3);
});

it('is 2 for a degraded compression fallback with merged reminders', () => {
const history: Content[] = [
{
role: 'user',
parts: [
{ text: 'summary\n\nResume the prior task...' },
{ text: '<system-reminder>\nplan mode active\n</system-reminder>' },
],
},
{
role: 'model',
parts: [
{ text: 'Got it. Thanks for the additional context!' },
{ functionCall: { name: 'fn', args: {} } },
],
},
{
role: 'user',
parts: [{ functionResponse: { name: 'fn', response: {} } }],
},
];
expect(getStartupContextLength(history)).toBe(2);
});
});

describe('buildAvailableSkillsReminder', () => {
Expand Down
36 changes: 32 additions & 4 deletions packages/core/src/utils/environmentContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -571,10 +571,18 @@ export async function getInitialChatHistory(
}

/**
* Returns the number of initial API entries occupied by the startup reminder
* (0 or 1). A single user message wrapped in <system-reminder> is the only
* shape getInitialChatHistory currently produces, but routes through this
* helper so detection stays consistent across the CLI and ACP integration.
* Returns the number of initial API entries occupied by structural context
* that should be skipped when counting real user turns:
*
* - The startup reminder prelude (0 or 1 entry) — a single user message
* wrapped in `<system-reminder>…</system-reminder>`, produced by
* `getInitialChatHistory`.
* - The legacy ack-pair prelude (2 entries) — sessions saved before the
* startup context moved into system reminders.
* - The compressed-history prefix (2 or 3 entries) — summary, ack, and
* optionally a post-compact attachments entry produced by
* `composePostCompactHistory`. These synthetic entries must not be
* counted as real user prompts for rewind indexing.
*/
export function getStartupContextLength(history: Content[]): number {
Comment thread
yiliang114 marked this conversation as resolved.
Resolved
const firstEntry = history[0];
Expand All @@ -601,9 +609,29 @@ export function getStartupContextLength(history: Content[]): number {
) {
return 2;
}
// Post-compression prefix: composePostCompactHistory always produces
// [user(summary), model(ack)] and optionally appends user(postAckParts)
// when file restorations or state reminders accompany the summary. The
// summary is plain prose (not <system-reminder>-wrapped) so it would pass
// isUserTextContent and corrupt rewind turn-counting. Detected via the
// compression-specific ack sentinel — distinct from the legacy ack above.
if (
history[1]?.role === 'model' &&
history[1]?.parts?.[0]?.text ===
'Got it. Thanks for the additional context!'
) {
if (isUserTextContextEntry(history[2])) return 3;
return 2;
}
return 0;
}

function isUserTextContextEntry(content: Content | undefined): boolean {
if (content?.role !== 'user') return false;
const parts = content.parts ?? [];
return parts.length > 0 && !parts.some((part) => 'functionResponse' in part);
}

Comment thread
yiliang114 marked this conversation as resolved.
Resolved
/**
* True when `content` is a *pure* system-reminder entry: it has parts and
* EVERY part is a text part wrapped in `<system-reminder>…</system-reminder>`.
Expand Down
Loading