refactor(v2): extract session DB operations into src/db/session-db.ts

Move all raw SQL out of session-manager, delivery, and host-sweep into
a dedicated DB module. Make session schemas idempotent (IF NOT EXISTS)
so initSessionFolder always applies them. Revert the markdown
plain-text retry from 4c477ac.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-04-12 16:50:03 +03:00
parent 2376c88aaf
commit 669a8444ef
6 changed files with 376 additions and 270 deletions

View File

@@ -10,7 +10,7 @@
* 3. One writer per file — DELETE-mode journal-unlink isn't atomic across
* the mount; concurrent writers corrupt the DB.
*/
import Database from 'better-sqlite3';
import type Database from 'better-sqlite3';
import fs from 'fs';
import path from 'path';
@@ -19,8 +19,16 @@ import { getAgentGroup } from './db/agent-groups.js';
import { getDestinations } from './db/agent-destinations.js';
import { getMessagingGroup } from './db/messaging-groups.js';
import { createSession, findSession, findSessionByAgentGroup, getSession, updateSession } from './db/sessions.js';
import {
ensureSchema,
openInboundDb as openInboundDbRaw,
openOutboundDb as openOutboundDbRaw,
upsertSessionRouting,
replaceDestinations,
insertMessage,
type DestinationRow,
} from './db/session-db.js';
import { log } from './log.js';
import { INBOUND_SCHEMA, OUTBOUND_SCHEMA } from './db/schema.js';
import type { Session } from './types.js';
/** Root directory for all session data. */
@@ -116,23 +124,8 @@ export function initSessionFolder(agentGroupId: string, sessionId: string): void
fs.mkdirSync(dir, { recursive: true });
fs.mkdirSync(path.join(dir, 'outbox'), { recursive: true });
const inPath = inboundDbPath(agentGroupId, sessionId);
if (!fs.existsSync(inPath)) {
const db = new Database(inPath);
db.pragma('journal_mode = DELETE');
db.exec(INBOUND_SCHEMA);
db.close();
log.debug('Inbound DB created', { dbPath: inPath });
}
const outPath = outboundDbPath(agentGroupId, sessionId);
if (!fs.existsSync(outPath)) {
const db = new Database(outPath);
db.pragma('journal_mode = DELETE');
db.exec(OUTBOUND_SCHEMA);
db.close();
log.debug('Outbound DB created', { dbPath: outPath });
}
ensureSchema(inboundDbPath(agentGroupId, sessionId), 'inbound');
ensureSchema(outboundDbPath(agentGroupId, sessionId), 'outbound');
}
/**
@@ -172,18 +165,9 @@ export function writeSessionRouting(agentGroupId: string, sessionId: string): vo
}
}
const db = new Database(dbPath);
db.pragma('journal_mode = DELETE');
db.pragma('busy_timeout = 5000');
const db = openInboundDb(agentGroupId, sessionId);
try {
db.prepare(
`INSERT INTO session_routing (id, channel_type, platform_id, thread_id)
VALUES (1, @channel_type, @platform_id, @thread_id)
ON CONFLICT(id) DO UPDATE SET
channel_type = excluded.channel_type,
platform_id = excluded.platform_id,
thread_id = excluded.thread_id`,
).run({
upsertSessionRouting(db, {
channel_type: channelType,
platform_id: platformId,
thread_id: session.thread_id,
@@ -199,15 +183,7 @@ export function writeDestinations(agentGroupId: string, sessionId: string): void
if (!fs.existsSync(dbPath)) return;
const rows = getDestinations(agentGroupId);
type DestRow = {
name: string;
display_name: string | null;
type: 'channel' | 'agent';
channel_type: string | null;
platform_id: string | null;
agent_group_id: string | null;
};
const resolved: DestRow[] = [];
const resolved: DestinationRow[] = [];
for (const row of rows) {
if (row.target_type === 'channel') {
@@ -235,19 +211,9 @@ export function writeDestinations(agentGroupId: string, sessionId: string): void
}
}
const db = new Database(dbPath);
db.pragma('journal_mode = DELETE');
db.pragma('busy_timeout = 5000');
const db = openInboundDb(agentGroupId, sessionId);
try {
const tx = db.transaction((entries: DestRow[]) => {
db.prepare('DELETE FROM destinations').run();
const stmt = db.prepare(
`INSERT INTO destinations (name, display_name, type, channel_type, platform_id, agent_group_id)
VALUES (@name, @display_name, @type, @channel_type, @platform_id, @agent_group_id)`,
);
for (const e of entries) stmt.run(e);
});
tx(resolved);
replaceDestinations(db, resolved);
} finally {
db.close();
}
@@ -279,28 +245,10 @@ export function writeSessionMessage(
// Extract base64 attachment data, save to inbox, replace with file paths
const content = extractAttachmentFiles(agentGroupId, sessionId, message.id, message.content);
const dbPath = inboundDbPath(agentGroupId, sessionId);
const db = new Database(dbPath);
db.pragma('journal_mode = DELETE');
db.pragma('busy_timeout = 5000');
const db = openInboundDb(agentGroupId, sessionId);
try {
// Host uses even seq, container uses odd. This is not just collision
// avoidance between the two DB files — the seq is the agent-facing
// message ID returned by send_message and accepted by edit_message /
// add_reaction, and those tools look up by seq across BOTH tables
// (see container/agent-runner/src/db/messages-out.ts:getMessageIdBySeq).
// So the {messages_in.seq, messages_out.seq} namespace MUST be disjoint,
// or the agent's "edit message #5" could resolve to the wrong row.
const maxSeq = (db.prepare('SELECT COALESCE(MAX(seq), 0) AS m FROM messages_in').get() as { m: number }).m;
const nextSeq = maxSeq < 2 ? 2 : maxSeq + 2 - (maxSeq % 2); // next even
db.prepare(
`INSERT INTO messages_in (id, seq, kind, timestamp, status, platform_id, channel_type, thread_id, content, process_after, recurrence)
VALUES (@id, @seq, @kind, @timestamp, 'pending', @platformId, @channelType, @threadId, @content, @processAfter, @recurrence)`,
).run({
insertMessage(db, {
id: message.id,
seq: nextSeq,
kind: message.kind,
timestamp: message.timestamp,
platformId: message.platformId ?? null,
@@ -357,19 +305,12 @@ function extractAttachmentFiles(
/** Open the inbound DB for a session (host reads/writes). */
export function openInboundDb(agentGroupId: string, sessionId: string): Database.Database {
const dbPath = inboundDbPath(agentGroupId, sessionId);
const db = new Database(dbPath);
db.pragma('journal_mode = DELETE');
db.pragma('busy_timeout = 5000');
return db;
return openInboundDbRaw(inboundDbPath(agentGroupId, sessionId));
}
/** Open the outbound DB for a session (host reads only). */
export function openOutboundDb(agentGroupId: string, sessionId: string): Database.Database {
const dbPath = outboundDbPath(agentGroupId, sessionId);
const db = new Database(dbPath, { readonly: true });
db.pragma('busy_timeout = 5000');
return db;
return openOutboundDbRaw(outboundDbPath(agentGroupId, sessionId));
}
/**