Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
22 changes: 22 additions & 0 deletions src/utils/relevancePruning.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,26 @@ describe('relevancePruning', () => {
expect(stats.toolCallCount).toBeGreaterThanOrEqual(0)
})
})

describe('chronological ordering', () => {
// Production Message objects carry the chronological key on the envelope
// `timestamp` (an ISO string), not on `message.created_at`. Build that real
// shape here so the final "restore chronological order" sort is exercised.
function envMessage(idx: number): any {
return {
type: 'user',
uuid: `u${idx}`,
timestamp: new Date(1_700_000_000_000 + idx * 1000).toISOString(),
message: { role: 'user', content: `message number ${idx} content here`, id: `m${idx}` },
}
}

it('returns retained messages in chronological order', () => {
const messages = Array.from({ length: 8 }, (_, i) => envMessage(i))
// Large target keeps every group; with preserveRecent=3 the last three
// are sliced off first, so a broken final sort leaves them out front.
const result = pruneByRelevance(messages, { targetTokens: 1_000_000 })
expect(result.map(m => m.message.id)).toEqual(messages.map(m => m.message.id))
})
})
})
21 changes: 17 additions & 4 deletions src/utils/relevancePruning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,19 @@ export function hasErrors(message: Message): boolean {
return textContent.includes('error') || textContent.includes('fail') || textContent.includes('exception')
}

/**
* Chronological key for a message, in epoch milliseconds. Reads the envelope
* `timestamp` (an ISO-8601 string present on every Message variant), NOT
* `message.message.created_at` — that nested API-body field is never populated
* on our Message objects, so the old code always saw `undefined` and its
* recency scoring, tie-break and final chronological sort were all no-ops.
* Returns 0 for a missing/unparseable timestamp (sorts as oldest).
*/
function messageTimeMs(message: Message | undefined): number {
const parsed = message?.timestamp ? Date.parse(message.timestamp) : NaN
return Number.isNaN(parsed) ? 0 : parsed
}

export function calculateRelevance(
message: Message,
options: PruningOptions,
Expand All @@ -104,7 +117,7 @@ export function calculateRelevance(
score += 0.3
}

const ageHours = (Date.now() - (message.message?.created_at ?? 0)) / (1000 * 60 * 60)
const ageHours = (Date.now() - messageTimeMs(message)) / (1000 * 60 * 60)
if (ageHours < 1) {
score += 0.15
}
Expand Down Expand Up @@ -169,8 +182,8 @@ export function pruneByRelevance(

scored.sort((a, b) => {
if (b.score !== a.score) return b.score - a.score
const aTime = a.group[0]?.message?.created_at ?? 0
const bTime = b.group[0]?.message?.created_at ?? 0
const aTime = messageTimeMs(a.group[0])
const bTime = messageTimeMs(b.group[0])
return bTime - aTime
})

Expand All @@ -191,7 +204,7 @@ export function pruneByRelevance(
totalTokens += tokens
}

return result.sort((a, b) => (a.message?.created_at ?? 0) - (b.message?.created_at ?? 0))
return result.sort((a, b) => messageTimeMs(a) - messageTimeMs(b))
}

export function getTopRelevantMessages(
Expand Down