fix: validate and compact session entries
This commit is contained in:
1 parent
c850d70bfc
commit
84c00e0011
4 files changed
+329
-13
No files matched your search
@@ -49,4 +49,150 @@ describe("SqliteSessionStore", () => {
|
||||
expect((loaded[0] as { role: string }).role).toBe("user");
|
||||
expect((loaded[1] as { role: string }).role).toBe("assistant");
|
||||
});
|
||||
|
||||
it("writes schema versioned envelopes for new messages", () => {
|
||||
const store = new SqliteSessionStore(db, TASK_SESSION_ID);
|
||||
store.appendMessage({
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "hello" }],
|
||||
timestamp: 1,
|
||||
} as AgentMessage);
|
||||
|
||||
const row = db.projection.sessionEntries.listActive(TASK_SESSION_ID)[0];
|
||||
const payload = JSON.parse(row?.payloadJson ?? "{}") as {
|
||||
schemaVersion?: number;
|
||||
kind?: string;
|
||||
message?: { role?: string; timestamp?: number };
|
||||
};
|
||||
expect(payload.schemaVersion).toBe(1);
|
||||
expect(payload.kind).toBe("agent_message");
|
||||
expect(payload.message).toMatchObject({ role: "user", timestamp: 1 });
|
||||
});
|
||||
|
||||
it("reads legacy raw AgentMessage payloads", () => {
|
||||
db.projection.sessionEntries.append({
|
||||
taskSessionId: TASK_SESSION_ID,
|
||||
kind: "message",
|
||||
role: "user",
|
||||
payloadJson: JSON.stringify({
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "legacy" }],
|
||||
}),
|
||||
});
|
||||
const store = new SqliteSessionStore(db, TASK_SESSION_ID);
|
||||
|
||||
const loaded = store.loadMessages();
|
||||
|
||||
expect(loaded).toHaveLength(1);
|
||||
expect((loaded[0] as { role?: string }).role).toBe("user");
|
||||
expect((loaded[0] as { timestamp?: number }).timestamp).toEqual(expect.any(Number));
|
||||
});
|
||||
|
||||
it("rejects invalid message shapes before writing session_entries", () => {
|
||||
const store = new SqliteSessionStore(db, TASK_SESSION_ID);
|
||||
|
||||
expect(() => store.appendMessage({
|
||||
role: "bad-role",
|
||||
content: [{ type: "text", text: "invalid" }],
|
||||
} as unknown as AgentMessage)).toThrow('invalid role "bad-role"');
|
||||
|
||||
expect(db.projection.sessionEntries.listActive(TASK_SESSION_ID)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("rejects new messages without a finite timestamp before writing session_entries", () => {
|
||||
const store = new SqliteSessionStore(db, TASK_SESSION_ID);
|
||||
|
||||
expect(() => store.appendMessage({
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "missing timestamp" }],
|
||||
} as AgentMessage)).toThrow("timestamp must be a finite number");
|
||||
expect(() => store.appendMessage({
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "bad timestamp" }],
|
||||
timestamp: Number.NaN,
|
||||
} as AgentMessage)).toThrow("timestamp must be a finite number");
|
||||
expect(db.projection.sessionEntries.listActive(TASK_SESSION_ID)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("rejects new messages with invalid content before writing session_entries", () => {
|
||||
const store = new SqliteSessionStore(db, TASK_SESSION_ID);
|
||||
|
||||
expect(() => store.appendMessage({
|
||||
role: "user",
|
||||
content: null,
|
||||
timestamp: 1,
|
||||
} as unknown as AgentMessage)).toThrow("content must be a string or an array");
|
||||
expect(db.projection.sessionEntries.listActive(TASK_SESSION_ID)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("fails reads with the corrupt row id when payload JSON is invalid", () => {
|
||||
db.projection.sessionEntries.append({
|
||||
taskSessionId: TASK_SESSION_ID,
|
||||
kind: "message",
|
||||
role: "user",
|
||||
payloadJson: "{",
|
||||
});
|
||||
const row = db.projection.sessionEntries.listActive(TASK_SESSION_ID)[0];
|
||||
const store = new SqliteSessionStore(db, TASK_SESSION_ID);
|
||||
|
||||
expect(() => store.loadMessages()).toThrow(`Invalid session entry ${row?.id} payload JSON`);
|
||||
});
|
||||
|
||||
it("fails reads with the corrupt row id when payload shape drifts", () => {
|
||||
db.projection.sessionEntries.append({
|
||||
taskSessionId: TASK_SESSION_ID,
|
||||
kind: "message",
|
||||
role: null,
|
||||
payloadJson: JSON.stringify({ schemaVersion: 1, kind: "agent_message", message: { role: "assistant" }, persistedAt: new Date().toISOString() }),
|
||||
});
|
||||
const row = db.projection.sessionEntries.listActive(TASK_SESSION_ID)[0];
|
||||
const store = new SqliteSessionStore(db, TASK_SESSION_ID);
|
||||
|
||||
expect(() => store.loadMessages()).toThrow(`Invalid session entry ${row?.id} AgentMessage`);
|
||||
});
|
||||
|
||||
it("durably compacts oversized active transcripts into a summary plus recent rows", () => {
|
||||
const store = new SqliteSessionStore(db, TASK_SESSION_ID);
|
||||
for (let i = 0; i < 46; i += 1) {
|
||||
const role = i % 2 === 0 ? "user" : "assistant";
|
||||
store.appendMessage({
|
||||
role,
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `${role} turn ${i}\n${"context ".repeat(6_000)}`,
|
||||
}],
|
||||
timestamp: i,
|
||||
} as AgentMessage);
|
||||
}
|
||||
|
||||
const loaded = store.loadMessages();
|
||||
const activeRows = db.projection.sessionEntries.listActive(TASK_SESSION_ID);
|
||||
const superseded = db.queryAll<{ count: number }>(
|
||||
"select count(*) as count from session_entries where task_session_id = ? and is_superseded = 1",
|
||||
TASK_SESSION_ID,
|
||||
)[0]?.count ?? 0;
|
||||
|
||||
expect(loaded.length).toBeLessThan(46);
|
||||
expect(activeRows).toHaveLength(loaded.length);
|
||||
expect(activeRows[0]?.sequence).toBe(1);
|
||||
expect(activeRows[0]?.role).toBe("user");
|
||||
expect(activeRows.slice(1).every((row, index, rows) => index === 0 || row.sequence > (rows[index - 1]?.sequence ?? 0))).toBe(true);
|
||||
expect(superseded).toBeGreaterThan(0);
|
||||
expect((loaded[0] as { role?: string }).role).toBe("user");
|
||||
const firstContent = (loaded[0] as { content?: Array<{ text?: string }> }).content;
|
||||
expect(firstContent?.[0]?.text).toContain("Context summary");
|
||||
|
||||
store.appendMessage({
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "continue" }],
|
||||
timestamp: 10_000,
|
||||
} as AgentMessage);
|
||||
const appended = db.projection.sessionEntries.listActive(TASK_SESSION_ID).at(-1);
|
||||
const allMaxSequence = db.queryAll<{ maxSeq: number }>(
|
||||
"select max(sequence) as maxSeq from session_entries where task_session_id = ?",
|
||||
TASK_SESSION_ID,
|
||||
)[0]?.maxSeq;
|
||||
expect(appended?.sequence).toBe(47);
|
||||
expect(allMaxSequence).toBe(47);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user