v2 phase 4+5: Discord via Chat SDK, expanded MCP tools, message seq IDs

- Chat SDK bridge + Discord adapter (gateway listener, message routing)
- MCP tools refactored into modular structure: core (send_message, send_file,
  edit_message, add_reaction), scheduling (schedule/list/cancel/pause/resume
  tasks), interactive (ask_user_question, send_card), agents (send_to_agent)
- Message seq IDs: shared integer sequence across messages_in/out so agents
  see small numeric IDs instead of platform snowflakes
- busy_timeout=5000 for session DB (poll loop + MCP server concurrent access)
- Always copy agent-runner source to fix stale cache when non-index files change
- Seed script for Discord testing, e2e test script

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-04-09 02:53:39 +03:00
parent b36f127acc
commit afbc20a6c4
21 changed files with 2702 additions and 37 deletions

View File

@@ -8,6 +8,7 @@ export function getSessionDb(): Database.Database {
if (!_db) {
_db = new Database(process.env.SESSION_DB_PATH || SESSION_DB_PATH);
_db.pragma('journal_mode = DELETE');
_db.pragma('busy_timeout = 5000');
_db.pragma('foreign_keys = ON');
}
return _db;
@@ -20,6 +21,7 @@ export function initTestSessionDb(): Database.Database {
_db.exec(`
CREATE TABLE messages_in (
id TEXT PRIMARY KEY,
seq INTEGER UNIQUE,
kind TEXT NOT NULL,
timestamp TEXT NOT NULL,
status TEXT DEFAULT 'pending',
@@ -34,6 +36,7 @@ export function initTestSessionDb(): Database.Database {
);
CREATE TABLE messages_out (
id TEXT PRIMARY KEY,
seq INTEGER UNIQUE,
in_reply_to TEXT,
timestamp TEXT NOT NULL,
delivered INTEGER DEFAULT 0,

View File

@@ -2,6 +2,7 @@ import { getSessionDb } from './connection.js';
export interface MessageInRow {
id: string;
seq: number | null;
kind: string;
timestamp: string;
status: string;

View File

@@ -2,6 +2,7 @@ import { getSessionDb } from './connection.js';
export interface MessageOutRow {
id: string;
seq: number | null;
in_reply_to: string | null;
timestamp: string;
delivered: number;
@@ -26,22 +27,44 @@ export interface WriteMessageOut {
content: string;
}
/** Write a new outbound message. */
export function writeMessageOut(msg: WriteMessageOut): void {
getSessionDb()
.prepare(
`INSERT INTO messages_out (id, in_reply_to, timestamp, delivered, deliver_after, recurrence, kind, platform_id, channel_type, thread_id, content)
VALUES (@id, @in_reply_to, datetime('now'), 0, @deliver_after, @recurrence, @kind, @platform_id, @channel_type, @thread_id, @content)`,
)
.run({
in_reply_to: null,
deliver_after: null,
recurrence: null,
platform_id: null,
channel_type: null,
thread_id: null,
...msg,
});
/** Write a new outbound message, auto-assigning a seq number. */
export function writeMessageOut(msg: WriteMessageOut): number {
const db = getSessionDb();
const nextSeq = (
db
.prepare(
`SELECT COALESCE(MAX(seq), 0) + 1 AS next FROM (
SELECT seq FROM messages_in WHERE seq IS NOT NULL
UNION ALL
SELECT seq FROM messages_out WHERE seq IS NOT NULL
)`,
)
.get() as { next: number }
).next;
db.prepare(
`INSERT INTO messages_out (id, seq, in_reply_to, timestamp, delivered, deliver_after, recurrence, kind, platform_id, channel_type, thread_id, content)
VALUES (@id, @seq, @in_reply_to, datetime('now'), 0, @deliver_after, @recurrence, @kind, @platform_id, @channel_type, @thread_id, @content)`,
).run({
in_reply_to: null,
deliver_after: null,
recurrence: null,
platform_id: null,
channel_type: null,
thread_id: null,
...msg,
seq: nextSeq,
});
return nextSeq;
}
/** Look up a message's platform ID by seq number. */
export function getMessageIdBySeq(seq: number): string | null {
const inRow = getSessionDb().prepare('SELECT id FROM messages_in WHERE seq = ?').get(seq) as { id: string } | undefined;
if (inRow) return inRow.id;
const outRow = getSessionDb().prepare('SELECT id FROM messages_out WHERE seq = ?').get(seq) as { id: string } | undefined;
return outRow?.id ?? null;
}
/** Get undelivered messages (for host polling). */