|
|
@@ -1547,3 +1547,110 @@ describe("session.message-v2.fromError", () => {
|
|
|
expect(result.name).toBe("MessageAbortedError")
|
|
|
})
|
|
|
})
|
|
|
+
|
|
|
+describe("session.message-v2.latest", () => {
|
|
|
+ const TAIL_USER = MessageID.make("msg_001")
|
|
|
+ const OVERFLOW_ASSISTANT = MessageID.make("msg_002")
|
|
|
+ const COMPACTION_USER = MessageID.make("msg_003")
|
|
|
+ const SUMMARY_ASSISTANT = MessageID.make("msg_004")
|
|
|
+ const CONTINUE_USER = MessageID.make("msg_005")
|
|
|
+ const NEW_COMPACTION_USER = MessageID.make("msg_006")
|
|
|
+
|
|
|
+ const tailUser: MessageV2.WithParts = {
|
|
|
+ info: userInfo(TAIL_USER),
|
|
|
+ parts: [{ ...basePart(TAIL_USER, "p1"), type: "text", text: "original prompt" }] as MessageV2.Part[],
|
|
|
+ }
|
|
|
+
|
|
|
+ const overflowAssistant: MessageV2.WithParts = {
|
|
|
+ info: {
|
|
|
+ ...assistantInfo(OVERFLOW_ASSISTANT, TAIL_USER),
|
|
|
+ finish: "tool-calls",
|
|
|
+ tokens: { input: 280_000, output: 200, reasoning: 0, cache: { read: 0, write: 0 }, total: 280_200 },
|
|
|
+ } as MessageV2.Assistant,
|
|
|
+ parts: [],
|
|
|
+ }
|
|
|
+
|
|
|
+ const compactionUser: MessageV2.WithParts = {
|
|
|
+ info: userInfo(COMPACTION_USER),
|
|
|
+ parts: [
|
|
|
+ {
|
|
|
+ ...basePart(COMPACTION_USER, "p1"),
|
|
|
+ type: "compaction",
|
|
|
+ auto: true,
|
|
|
+ tail_start_id: TAIL_USER,
|
|
|
+ },
|
|
|
+ ] as MessageV2.Part[],
|
|
|
+ }
|
|
|
+
|
|
|
+ const summaryAssistant: MessageV2.WithParts = {
|
|
|
+ info: {
|
|
|
+ ...assistantInfo(SUMMARY_ASSISTANT, COMPACTION_USER),
|
|
|
+ summary: true,
|
|
|
+ finish: "stop",
|
|
|
+ tokens: { input: 150_000, output: 1_500, reasoning: 0, cache: { read: 0, write: 0 }, total: 151_500 },
|
|
|
+ } as MessageV2.Assistant,
|
|
|
+ parts: [],
|
|
|
+ }
|
|
|
+
|
|
|
+ const continueUser: MessageV2.WithParts = {
|
|
|
+ info: userInfo(CONTINUE_USER),
|
|
|
+ parts: [
|
|
|
+ {
|
|
|
+ ...basePart(CONTINUE_USER, "p1"),
|
|
|
+ type: "text",
|
|
|
+ text: "Continue if you have next steps...",
|
|
|
+ synthetic: true,
|
|
|
+ metadata: { compaction_continue: true },
|
|
|
+ },
|
|
|
+ ] as MessageV2.Part[],
|
|
|
+ }
|
|
|
+
|
|
|
+ // Regression for double auto-compaction. The reorder in filterCompacted
|
|
|
+ // (#27145) returns [compaction-user, summary, ...tail..., continue-user],
|
|
|
+ // so picking lastFinished by array position landed on the pre-compaction
|
|
|
+ // overflow assistant and bypassed the `summary !== true` overflow guard
|
|
|
+ // in SessionPrompt.runLoop, firing a second compaction.create immediately.
|
|
|
+ test("finished is the chronologically-latest finished assistant, not the array-latest", () => {
|
|
|
+ const filtered = MessageV2.filterCompacted([
|
|
|
+ continueUser,
|
|
|
+ summaryAssistant,
|
|
|
+ compactionUser,
|
|
|
+ overflowAssistant,
|
|
|
+ tailUser,
|
|
|
+ ])
|
|
|
+
|
|
|
+ const state = MessageV2.latest(filtered)
|
|
|
+
|
|
|
+ expect(state.finished?.id).toBe(SUMMARY_ASSISTANT)
|
|
|
+ expect(state.finished?.summary).toBe(true)
|
|
|
+ expect(state.user?.id).toBe(CONTINUE_USER)
|
|
|
+ expect(state.tasks).toEqual([])
|
|
|
+ })
|
|
|
+
|
|
|
+ test("a fresh compaction-user newer than the latest summary surfaces in tasks", () => {
|
|
|
+ const newCompactionUser: MessageV2.WithParts = {
|
|
|
+ info: userInfo(NEW_COMPACTION_USER),
|
|
|
+ parts: [
|
|
|
+ {
|
|
|
+ ...basePart(NEW_COMPACTION_USER, "p1"),
|
|
|
+ type: "compaction",
|
|
|
+ auto: true,
|
|
|
+ },
|
|
|
+ ] as MessageV2.Part[],
|
|
|
+ }
|
|
|
+
|
|
|
+ const state = MessageV2.latest([
|
|
|
+ tailUser,
|
|
|
+ overflowAssistant,
|
|
|
+ compactionUser,
|
|
|
+ summaryAssistant,
|
|
|
+ continueUser,
|
|
|
+ newCompactionUser,
|
|
|
+ ])
|
|
|
+
|
|
|
+ expect(state.finished?.id).toBe(SUMMARY_ASSISTANT)
|
|
|
+ expect(state.user?.id).toBe(NEW_COMPACTION_USER)
|
|
|
+ expect(state.tasks).toHaveLength(1)
|
|
|
+ expect(state.tasks[0]).toMatchObject({ type: "compaction", auto: true })
|
|
|
+ })
|
|
|
+})
|