148 lines
4.8 KiB
TypeScript
148 lines
4.8 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import type { DatabaseSync, SQLInputValue } from "node:sqlite";
|
|
import type { ReadAll, Transaction } from "./helpers";
|
|
import { nowIso } from "./helpers";
|
|
|
|
/**
|
|
* The `session_entries` table only ever stores one kind — full pi
|
|
* `AgentMessage` envelopes emitted by `message_end`. Earlier builds
|
|
* reserved `"compaction"` and `"summary"` variants for a supersede
|
|
* pipeline that never shipped; their writers are gone and the column
|
|
* holds `"message"` for every live row. Narrowing the type prevents
|
|
* new code from writing a dead kind by accident.
|
|
*/
|
|
export type SessionEntryKind = "message";
|
|
|
|
export interface SessionEntryRow {
|
|
id: string;
|
|
taskSessionId: string;
|
|
sequence: number;
|
|
kind: SessionEntryKind;
|
|
role: string | null;
|
|
payloadJson: string;
|
|
createdAt: string;
|
|
}
|
|
|
|
export interface AppendInput {
|
|
taskSessionId: string;
|
|
kind: SessionEntryKind;
|
|
role?: string | null;
|
|
payloadJson: string;
|
|
}
|
|
|
|
export function buildSessionEntryMethods(
|
|
getDb: () => DatabaseSync,
|
|
readAll: ReadAll,
|
|
transaction: Transaction,
|
|
) {
|
|
function nextSequence(taskSessionId: string): number {
|
|
const rows = readAll<{ maxSeq: number | null }>(
|
|
"select max(sequence) as maxSeq from session_entries where task_session_id = ?",
|
|
taskSessionId,
|
|
);
|
|
const max = rows[0]?.maxSeq;
|
|
return (max ?? 0) + 1;
|
|
}
|
|
|
|
function append(input: AppendInput): SessionEntryRow {
|
|
const id = randomUUID();
|
|
const sequence = nextSequence(input.taskSessionId);
|
|
const created = nowIso();
|
|
getDb()
|
|
.prepare(
|
|
"insert into session_entries (id, task_session_id, sequence, kind, role, payload_json, is_superseded, created_at) values (?, ?, ?, ?, ?, ?, 0, ?)",
|
|
)
|
|
.run(
|
|
id,
|
|
input.taskSessionId,
|
|
sequence,
|
|
input.kind,
|
|
input.role ?? null,
|
|
input.payloadJson,
|
|
created,
|
|
);
|
|
return {
|
|
id,
|
|
taskSessionId: input.taskSessionId,
|
|
sequence,
|
|
kind: input.kind,
|
|
role: input.role ?? null,
|
|
payloadJson: input.payloadJson,
|
|
createdAt: created,
|
|
};
|
|
}
|
|
|
|
function listActive(taskSessionId: string): SessionEntryRow[] {
|
|
const rows = readAll<{
|
|
id: string;
|
|
task_session_id: string;
|
|
sequence: number;
|
|
kind: SessionEntryKind;
|
|
role: string | null;
|
|
payload_json: string;
|
|
created_at: string;
|
|
}>(
|
|
"select id, task_session_id, sequence, kind, role, payload_json, created_at from session_entries where task_session_id = ? and is_superseded = 0 order by sequence asc, created_at asc",
|
|
taskSessionId,
|
|
);
|
|
return rows.map((row) => ({
|
|
id: row.id,
|
|
taskSessionId: row.task_session_id,
|
|
sequence: row.sequence,
|
|
kind: row.kind,
|
|
role: row.role,
|
|
payloadJson: row.payload_json,
|
|
createdAt: row.created_at,
|
|
}));
|
|
}
|
|
|
|
function compactWithSummary(input: {
|
|
taskSessionId: string;
|
|
supersedeEntryIds: string[];
|
|
summaryRole: string | null;
|
|
summaryPayloadJson: string;
|
|
}): SessionEntryRow {
|
|
return transaction(() => {
|
|
const uniqueIds = [...new Set(input.supersedeEntryIds)];
|
|
if (uniqueIds.length === 0) {
|
|
throw new Error("session_entries compaction requires at least one entry to supersede.");
|
|
}
|
|
const placeholders = uniqueIds.map(() => "?").join(", ");
|
|
const rows = readAll<{ id: string; sequence: number }>(
|
|
`select id, sequence from session_entries where task_session_id = ? and is_superseded = 0 and id in (${placeholders}) order by sequence asc`,
|
|
input.taskSessionId,
|
|
...(uniqueIds as SQLInputValue[]),
|
|
);
|
|
if (rows.length !== uniqueIds.length) {
|
|
throw new Error(`session_entries compaction expected ${uniqueIds.length} active row(s), found ${rows.length}.`);
|
|
}
|
|
const summarySequence = rows[0]?.sequence;
|
|
if (summarySequence === undefined) {
|
|
throw new Error("session_entries compaction could not resolve summary sequence.");
|
|
}
|
|
getDb()
|
|
.prepare(`update session_entries set is_superseded = 1 where task_session_id = ? and id in (${placeholders})`)
|
|
.run(input.taskSessionId, ...(uniqueIds as SQLInputValue[]));
|
|
|
|
const id = randomUUID();
|
|
const created = nowIso();
|
|
getDb()
|
|
.prepare(
|
|
"insert into session_entries (id, task_session_id, sequence, kind, role, payload_json, is_superseded, created_at) values (?, ?, ?, 'message', ?, ?, 0, ?)",
|
|
)
|
|
.run(id, input.taskSessionId, summarySequence, input.summaryRole, input.summaryPayloadJson, created);
|
|
return {
|
|
id,
|
|
taskSessionId: input.taskSessionId,
|
|
sequence: summarySequence,
|
|
kind: "message",
|
|
role: input.summaryRole,
|
|
payloadJson: input.summaryPayloadJson,
|
|
createdAt: created,
|
|
};
|
|
});
|
|
}
|
|
|
|
return { append, listActive, compactWithSummary };
|
|
}
|