mirror of
https://github.com/Gitlawb/openclaude.git
synced 2026-08-24 02:34:15 -05:00
fix(context): order pruned messages by envelope timestamp, not phantom field (#1934)
pruneByRelevance keyed recency scoring, the group tie-break, and the final "restore chronological order" sort off message.message?.created_at. That nested API-body field is never populated on our Message objects (nothing in the tree assigns it), so every read was undefined and `?? 0` made all three into no-ops: the recency bonus never fired, the tie-break never broke ties, and the final sort left the list as [...recentMessages, ...olderGroups] — the newest preserveRecent messages jumped ahead of older retained ones. The chronological key actually lives on the Message envelope as `timestamp` (an ISO-8601 string present on every variant). Add a messageTimeMs() helper that parses it and route all three sites through it. This runs in the auto-compaction path (autoCompact -> pruneByRelevance), so the reordered list was being sent to the model. Add a regression using the real envelope shape asserting retained messages stay in chronological order.
This commit is contained in:
@@ -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))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user