v2 phase 3: host core — router, session manager, delivery, sweep
Host orchestrator connecting channel events to session DBs and delivering responses back through channel adapters. - session-manager.ts: session folder/DB lifecycle, message writing - container-runner-v2.ts: Docker spawn with session + agent group mounts, OneCLI, idle timeout, agent-runner recompilation - router-v2.ts: inbound routing (channel → messaging group → agent group → session → messages_in → wake container) - delivery.ts: two-tier polling (1s active, 60s sweep) for messages_out, channel adapter delivery - host-sweep.ts: stale detection with backoff, recurrence, wake containers for due messages - index-v2.ts: thin entry point wiring everything together - scripts/test-v2-agent.ts: real Claude provider integration test Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
145
src/session-manager.ts
Normal file
145
src/session-manager.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Session lifecycle management.
|
||||
* Creates session folders + DBs, writes messages, manages container status.
|
||||
*/
|
||||
import Database from 'better-sqlite3';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { DATA_DIR } from './config.js';
|
||||
import { createSession, findSession, getSession, updateSession } from './db/sessions.js';
|
||||
import { log } from './log.js';
|
||||
import { SESSION_SCHEMA } from './db/schema.js';
|
||||
import type { Session } from './types-v2.js';
|
||||
|
||||
/** Root directory for all session data. */
|
||||
export function sessionsBaseDir(): string {
|
||||
return path.join(DATA_DIR, 'v2-sessions');
|
||||
}
|
||||
|
||||
/** Directory for a specific session: sessions/{agent_group_id}/{session_id}/ */
|
||||
export function sessionDir(agentGroupId: string, sessionId: string): string {
|
||||
return path.join(sessionsBaseDir(), agentGroupId, sessionId);
|
||||
}
|
||||
|
||||
/** Path to a session's SQLite DB. */
|
||||
export function sessionDbPath(agentGroupId: string, sessionId: string): string {
|
||||
return path.join(sessionDir(agentGroupId, sessionId), 'session.db');
|
||||
}
|
||||
|
||||
function generateId(): string {
|
||||
return `sess-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find or create a session for a messaging group + thread.
|
||||
* Returns the session and whether it was newly created.
|
||||
*/
|
||||
export function resolveSession(agentGroupId: string, messagingGroupId: string, threadId: string | null, sessionMode: 'shared' | 'per-thread'): { session: Session; created: boolean } {
|
||||
// For shared mode, look for any active session with this messaging group (threadId ignored)
|
||||
// For per-thread mode, look for an active session with this specific thread
|
||||
const lookupThreadId = sessionMode === 'shared' ? null : threadId;
|
||||
const existing = findSession(messagingGroupId, lookupThreadId);
|
||||
|
||||
if (existing) {
|
||||
return { session: existing, created: false };
|
||||
}
|
||||
|
||||
// Create new session
|
||||
const id = generateId();
|
||||
const session: Session = {
|
||||
id,
|
||||
agent_group_id: agentGroupId,
|
||||
messaging_group_id: messagingGroupId,
|
||||
thread_id: lookupThreadId,
|
||||
agent_provider: null,
|
||||
status: 'active',
|
||||
container_status: 'stopped',
|
||||
last_active: null,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
createSession(session);
|
||||
initSessionFolder(agentGroupId, id);
|
||||
log.info('Session created', { id, agentGroupId, messagingGroupId, threadId: lookupThreadId });
|
||||
|
||||
return { session, created: true };
|
||||
}
|
||||
|
||||
/** Create the session folder and initialize the session DB. */
|
||||
export function initSessionFolder(agentGroupId: string, sessionId: string): void {
|
||||
const dir = sessionDir(agentGroupId, sessionId);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.mkdirSync(path.join(dir, 'outbox'), { recursive: true });
|
||||
|
||||
const dbPath = sessionDbPath(agentGroupId, sessionId);
|
||||
if (!fs.existsSync(dbPath)) {
|
||||
const db = new Database(dbPath);
|
||||
db.pragma('journal_mode = WAL');
|
||||
db.exec(SESSION_SCHEMA);
|
||||
db.close();
|
||||
log.debug('Session DB created', { dbPath });
|
||||
}
|
||||
}
|
||||
|
||||
/** Write a message to a session's messages_in table. */
|
||||
export function writeSessionMessage(agentGroupId: string, sessionId: string, message: {
|
||||
id: string;
|
||||
kind: string;
|
||||
timestamp: string;
|
||||
platformId?: string | null;
|
||||
channelType?: string | null;
|
||||
threadId?: string | null;
|
||||
content: string;
|
||||
processAfter?: string | null;
|
||||
recurrence?: string | null;
|
||||
}): void {
|
||||
const dbPath = sessionDbPath(agentGroupId, sessionId);
|
||||
const db = new Database(dbPath);
|
||||
db.pragma('journal_mode = WAL');
|
||||
|
||||
try {
|
||||
db.prepare(
|
||||
`INSERT INTO messages_in (id, kind, timestamp, status, platform_id, channel_type, thread_id, content, process_after, recurrence)
|
||||
VALUES (@id, @kind, @timestamp, 'pending', @platformId, @channelType, @threadId, @content, @processAfter, @recurrence)`,
|
||||
).run({
|
||||
id: message.id,
|
||||
kind: message.kind,
|
||||
timestamp: message.timestamp,
|
||||
platformId: message.platformId ?? null,
|
||||
channelType: message.channelType ?? null,
|
||||
threadId: message.threadId ?? null,
|
||||
content: message.content,
|
||||
processAfter: message.processAfter ?? null,
|
||||
recurrence: message.recurrence ?? null,
|
||||
});
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
|
||||
// Update last_active
|
||||
updateSession(sessionId, { last_active: new Date().toISOString() });
|
||||
}
|
||||
|
||||
/** Open a session DB for reading (e.g., polling messages_out). */
|
||||
export function openSessionDb(agentGroupId: string, sessionId: string): Database.Database {
|
||||
const dbPath = sessionDbPath(agentGroupId, sessionId);
|
||||
const db = new Database(dbPath);
|
||||
db.pragma('journal_mode = WAL');
|
||||
return db;
|
||||
}
|
||||
|
||||
/** Mark a container as running for a session. */
|
||||
export function markContainerRunning(sessionId: string): void {
|
||||
updateSession(sessionId, { container_status: 'running', last_active: new Date().toISOString() });
|
||||
}
|
||||
|
||||
/** Mark a container as idle for a session. */
|
||||
export function markContainerIdle(sessionId: string): void {
|
||||
updateSession(sessionId, { container_status: 'idle' });
|
||||
}
|
||||
|
||||
/** Mark a container as stopped for a session. */
|
||||
export function markContainerStopped(sessionId: string): void {
|
||||
updateSession(sessionId, { container_status: 'stopped' });
|
||||
}
|
||||
Reference in New Issue
Block a user