Menu
popagent
publicLatest change 6edc7e928c57dab23c0973ed3f7ee70b3ebb72d5 - Initial popagent baseline by AkurAI Build
import { DEFAULT_WORKSPACE_ID, type ChatMessage, type Session, type SessionSummary } from "./api-types";
import { memory, storage } from "./storage";
import { retryableInit } from "./retryable-init";
export const RESOURCE_ID = "popagent-user";
const NEW_CHAT_TITLE = "New chat";
function titleFrom(messages: ChatMessage[]): string {
const firstUser = messages.find((message) => message.role === "user");
const text = firstUser?.parts.find(
(part): part is { type: "text"; text: string } =>
typeof part === "object" && part !== null && "type" in part && "text" in part &&
part.type === "text" && typeof part.text === "string",
)?.text.trim();
if (!text) return NEW_CHAT_TITLE;
return text.length > 48 ? `${text.slice(0, 47).trimEnd()}…` : text;
}
export class SessionStore {
readonly storage = storage;
readonly memory = memory;
private readonly initializeOnce = retryableInit(() => this.initialize());
init(): Promise<void> {
return this.initializeOnce();
}
private async initialize() {
await this.storage.init();
await this.storage.db.none(`
CREATE TABLE IF NOT EXISTS popagent_sessions (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
model TEXT NOT NULL,
workspace_id TEXT NOT NULL DEFAULT '${DEFAULT_WORKSPACE_ID}',
messages JSONB NOT NULL DEFAULT '[]'::jsonb,
revision INTEGER NOT NULL DEFAULT 0,
title_locked BOOLEAN NOT NULL DEFAULT FALSE,
archived_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
ALTER TABLE popagent_sessions ADD COLUMN IF NOT EXISTS workspace_id TEXT NOT NULL DEFAULT '${DEFAULT_WORKSPACE_ID}';
ALTER TABLE popagent_sessions ADD COLUMN IF NOT EXISTS title_locked BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE popagent_sessions ADD COLUMN IF NOT EXISTS archived_at TIMESTAMPTZ;
ALTER TABLE popagent_sessions ADD COLUMN IF NOT EXISTS revision INTEGER NOT NULL DEFAULT 0;
CREATE INDEX IF NOT EXISTS popagent_sessions_workspace_archive_updated_idx
ON popagent_sessions (workspace_id, archived_at, updated_at DESC);
`);
}
async list(archived = false, workspaceId?: string): Promise<SessionSummary[]> {
await this.init();
return this.storage.db.any<SessionSummary>(`
SELECT id, title, model, workspace_id AS "workspaceId", revision,
archived_at::text AS "archivedAt",
created_at::text AS "createdAt", updated_at::text AS "updatedAt"
FROM popagent_sessions
WHERE archived_at IS ${archived ? "NOT NULL" : "NULL"}
AND ($1::text IS NULL OR workspace_id = $1)
ORDER BY updated_at DESC
LIMIT 200
`, [workspaceId ?? null]);
}
async create(model: string, workspaceId = DEFAULT_WORKSPACE_ID): Promise<Session> {
await this.init();
const id = crypto.randomUUID();
const row = await this.storage.db.one<SessionSummary>(`
INSERT INTO popagent_sessions (id, title, model, workspace_id)
VALUES ($1, $2, $3, $4)
RETURNING id, title, model, workspace_id AS "workspaceId", revision,
archived_at::text AS "archivedAt",
created_at::text AS "createdAt", updated_at::text AS "updatedAt"
`, [id, NEW_CHAT_TITLE, model, workspaceId]);
await this.memory.saveThread({
thread: {
id,
resourceId: RESOURCE_ID,
title: NEW_CHAT_TITLE,
metadata: { model, workspaceId },
createdAt: new Date(row.createdAt),
updatedAt: new Date(row.updatedAt),
},
});
return { ...row, messages: [] };
}
async get(id: string): Promise<Session | undefined> {
await this.init();
const row = await this.storage.db.oneOrNone<Session>(`
SELECT id, title, model, workspace_id AS "workspaceId", revision, messages,
archived_at::text AS "archivedAt",
created_at::text AS "createdAt", updated_at::text AS "updatedAt"
FROM popagent_sessions
WHERE id = $1
`, [id]);
return row ?? undefined;
}
async save(
id: string,
input: { model: string; messages: ChatMessage[]; expectedRevision: number },
): Promise<Session | undefined | "conflict"> {
await this.init();
const generatedTitle = titleFrom(input.messages);
const row = await this.storage.db.oneOrNone<SessionSummary>(`
UPDATE popagent_sessions
SET title = CASE WHEN title_locked THEN title ELSE $1 END,
model = $2,
messages = $3::jsonb,
revision = revision + 1,
updated_at = NOW()
WHERE id = $4 AND revision = $5
RETURNING id, title, model, workspace_id AS "workspaceId", revision,
archived_at::text AS "archivedAt",
created_at::text AS "createdAt", updated_at::text AS "updatedAt"
`, [generatedTitle, input.model, JSON.stringify(input.messages), id, input.expectedRevision]);
if (!row) return (await this.get(id)) ? "conflict" : undefined;
await this.memory.updateThread({
id,
title: row.title,
metadata: { model: input.model, workspaceId: row.workspaceId },
});
return { ...row, messages: input.messages };
}
async rename(id: string, title: string): Promise<Session | undefined> {
await this.init();
const cleanTitle = title.trim().slice(0, 120);
if (!cleanTitle) return undefined;
const row = await this.storage.db.oneOrNone<Session>(`
UPDATE popagent_sessions
SET title = $2, title_locked = TRUE, updated_at = NOW()
WHERE id = $1
RETURNING id, title, model, workspace_id AS "workspaceId", revision, messages,
archived_at::text AS "archivedAt",
created_at::text AS "createdAt", updated_at::text AS "updatedAt"
`, [id, cleanTitle]);
if (row) await this.memory.updateThread({ id, title: cleanTitle });
return row ?? undefined;
}
async setArchived(id: string, archived: boolean): Promise<Session | undefined> {
await this.init();
const row = await this.storage.db.oneOrNone<Session>(`
UPDATE popagent_sessions
SET archived_at = ${archived ? "NOW()" : "NULL"}, updated_at = NOW()
WHERE id = $1
RETURNING id, title, model, workspace_id AS "workspaceId", revision, messages,
archived_at::text AS "archivedAt",
created_at::text AS "createdAt", updated_at::text AS "updatedAt"
`, [id]);
return row ?? undefined;
}
async delete(id: string): Promise<boolean> {
await this.init();
const exists = await this.storage.db.oneOrNone<{ id: string }>(
"SELECT id FROM popagent_sessions WHERE id = $1",
[id],
);
if (!exists) return false;
await this.memory.deleteThread(id);
await this.storage.db.tx(async (db) => {
await db.none("DELETE FROM mastra_thread_state WHERE \"threadId\" = $1", [id]);
await db.none("DELETE FROM popagent_sessions WHERE id = $1", [id]);
});
return true;
}
}