fix(core): Handle input timeline events and scrub before truncation

- Merge master: the timeline gained an input event variant, which the
  exhaustive switch in truncateTimeline must handle (CI caught this)
- Scrub serialized payloads before slicing so a secret that straddles
  the cut cannot leave an unscrubbed prefix behind
- Pin the newest-first ordering in the pagination test with a
  two-element page

Linear: CONTEXT-178
This commit is contained in:
Rob 2026-09-29 16:21:02 +01:00
parent a43a46ef52
commit df263a4f61
2 changed files with 18 additions and 8 deletions

View File

@ -1783,17 +1783,22 @@ describe('McpAgentToolsService', () => {
],
} as never);
// Two-element page from offset 0: only a real newest-first reversal
// yields [exec-3, exec-2]; a raw oldest-first slice would fail here.
const result = await callTool('get_agent_session', {
agentId: 'agent-1',
sessionId: 'thread-1',
limit: 1,
offset: 1,
limit: 2,
offset: 0,
});
expect(result.structuredContent).toMatchObject({
ok: true,
executions: [expect.objectContaining({ executionId: 'exec-2' })],
count: 1,
executions: [
expect.objectContaining({ executionId: 'exec-3' }),
expect.objectContaining({ executionId: 'exec-2' }),
],
count: 2,
total: 3,
nextOffset: 2,
});

View File

@ -86,11 +86,12 @@ export function truncateTimeline(
} catch {
serialized = String(value);
}
if (serialized.length <= budget) return value;
// Scrub before slicing: a secret that straddles the cut would otherwise
// leave an unrecognized — and unscrubbed — prefix behind.
const scrubbed = scrubSecretsInText(serialized);
if (scrubbed.length <= budget) return scrubbed === serialized ? value : scrubbed;
truncated = true;
return (
scrubSecretsInText(serialized.slice(0, budget)) + truncationMarker(serialized.length - budget)
);
return scrubbed.slice(0, budget) + truncationMarker(scrubbed.length - budget);
};
const timeline = events.map((event): ShapedTimelineEvent => {
@ -120,6 +121,10 @@ export function truncateTimeline(
return { ...event, response: capValue(event.response) };
case 'background-task-signal':
return { ...event, signal: capValue(event.signal) };
// Carries only a message reference — nothing to cap. No default case:
// a new event variant must fail the build until it gets a decision here.
case 'input':
return { ...event };
}
});