style: apply prettier formatting to v2 source files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -263,7 +263,10 @@ async function buildContainerArgs(
|
|||||||
// Override entrypoint: compile agent-runner source, run v2 entry point (no stdin)
|
// Override entrypoint: compile agent-runner source, run v2 entry point (no stdin)
|
||||||
args.push('--entrypoint', 'bash');
|
args.push('--entrypoint', 'bash');
|
||||||
args.push(CONTAINER_IMAGE);
|
args.push(CONTAINER_IMAGE);
|
||||||
args.push('-c', 'cd /app && npx tsc --outDir /tmp/dist 2>&1 >&2 && ln -sf /app/node_modules /tmp/dist/node_modules && node /tmp/dist/index-v2.js');
|
args.push(
|
||||||
|
'-c',
|
||||||
|
'cd /app && npx tsc --outDir /tmp/dist 2>&1 >&2 && ln -sf /app/node_modules /tmp/dist/node_modules && node /tmp/dist/index-v2.js',
|
||||||
|
);
|
||||||
|
|
||||||
return args;
|
return args;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,13 @@ const ACTIVE_POLL_MS = 1000;
|
|||||||
const SWEEP_POLL_MS = 60_000;
|
const SWEEP_POLL_MS = 60_000;
|
||||||
|
|
||||||
export interface ChannelDeliveryAdapter {
|
export interface ChannelDeliveryAdapter {
|
||||||
deliver(channelType: string, platformId: string, threadId: string | null, kind: string, content: string): Promise<void>;
|
deliver(
|
||||||
|
channelType: string,
|
||||||
|
platformId: string,
|
||||||
|
threadId: string | null,
|
||||||
|
kind: string,
|
||||||
|
content: string,
|
||||||
|
): Promise<void>;
|
||||||
setTyping?(channelType: string, platformId: string, threadId: string | null): Promise<void>;
|
setTyping?(channelType: string, platformId: string, threadId: string | null): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +122,14 @@ async function deliverSessionMessages(session: Session): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deliverMessage(
|
async function deliverMessage(
|
||||||
msg: { id: string; kind: string; platform_id: string | null; channel_type: string | null; thread_id: string | null; content: string },
|
msg: {
|
||||||
|
id: string;
|
||||||
|
kind: string;
|
||||||
|
platform_id: string | null;
|
||||||
|
channel_type: string | null;
|
||||||
|
thread_id: string | null;
|
||||||
|
content: string;
|
||||||
|
},
|
||||||
session: Session,
|
session: Session,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!deliveryAdapter) {
|
if (!deliveryAdapter) {
|
||||||
|
|||||||
@@ -8,8 +8,22 @@ import fs from 'fs';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||||
|
|
||||||
import { initTestDb, closeDb, runMigrations, createAgentGroup, createMessagingGroup, createMessagingGroupAgent } from './db/index.js';
|
import {
|
||||||
import { resolveSession, writeSessionMessage, initSessionFolder, sessionDir, sessionDbPath, sessionsBaseDir } from './session-manager.js';
|
initTestDb,
|
||||||
|
closeDb,
|
||||||
|
runMigrations,
|
||||||
|
createAgentGroup,
|
||||||
|
createMessagingGroup,
|
||||||
|
createMessagingGroupAgent,
|
||||||
|
} from './db/index.js';
|
||||||
|
import {
|
||||||
|
resolveSession,
|
||||||
|
writeSessionMessage,
|
||||||
|
initSessionFolder,
|
||||||
|
sessionDir,
|
||||||
|
sessionDbPath,
|
||||||
|
sessionsBaseDir,
|
||||||
|
} from './session-manager.js';
|
||||||
import { getSession, findSession } from './db/sessions.js';
|
import { getSession, findSession } from './db/sessions.js';
|
||||||
import type { InboundEvent } from './router-v2.js';
|
import type { InboundEvent } from './router-v2.js';
|
||||||
|
|
||||||
@@ -50,8 +64,24 @@ afterEach(() => {
|
|||||||
|
|
||||||
describe('session manager', () => {
|
describe('session manager', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createAgentGroup({ id: 'ag-1', name: 'Test Agent', folder: 'test-agent', is_admin: 0, agent_provider: null, container_config: null, created_at: now() });
|
createAgentGroup({
|
||||||
createMessagingGroup({ id: 'mg-1', channel_type: 'discord', platform_id: 'chan-123', name: 'General', is_group: 1, admin_user_id: null, created_at: now() });
|
id: 'ag-1',
|
||||||
|
name: 'Test Agent',
|
||||||
|
folder: 'test-agent',
|
||||||
|
is_admin: 0,
|
||||||
|
agent_provider: null,
|
||||||
|
container_config: null,
|
||||||
|
created_at: now(),
|
||||||
|
});
|
||||||
|
createMessagingGroup({
|
||||||
|
id: 'mg-1',
|
||||||
|
channel_type: 'discord',
|
||||||
|
platform_id: 'chan-123',
|
||||||
|
name: 'General',
|
||||||
|
is_group: 1,
|
||||||
|
admin_user_id: null,
|
||||||
|
created_at: now(),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create session folder and DB', () => {
|
it('should create session folder and DB', () => {
|
||||||
@@ -110,7 +140,12 @@ describe('session manager', () => {
|
|||||||
// Read from the session DB
|
// Read from the session DB
|
||||||
const dbPath = sessionDbPath('ag-1', session.id);
|
const dbPath = sessionDbPath('ag-1', session.id);
|
||||||
const db = new Database(dbPath);
|
const db = new Database(dbPath);
|
||||||
const rows = db.prepare('SELECT * FROM messages_in').all() as Array<{ id: string; kind: string; status: string; content: string }>;
|
const rows = db.prepare('SELECT * FROM messages_in').all() as Array<{
|
||||||
|
id: string;
|
||||||
|
kind: string;
|
||||||
|
status: string;
|
||||||
|
content: string;
|
||||||
|
}>;
|
||||||
db.close();
|
db.close();
|
||||||
|
|
||||||
expect(rows).toHaveLength(1);
|
expect(rows).toHaveLength(1);
|
||||||
@@ -136,9 +171,34 @@ describe('session manager', () => {
|
|||||||
|
|
||||||
describe('router', () => {
|
describe('router', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createAgentGroup({ id: 'ag-1', name: 'Test Agent', folder: 'test-agent', is_admin: 0, agent_provider: null, container_config: null, created_at: now() });
|
createAgentGroup({
|
||||||
createMessagingGroup({ id: 'mg-1', channel_type: 'discord', platform_id: 'chan-123', name: 'General', is_group: 1, admin_user_id: null, created_at: now() });
|
id: 'ag-1',
|
||||||
createMessagingGroupAgent({ id: 'mga-1', messaging_group_id: 'mg-1', agent_group_id: 'ag-1', trigger_rules: null, response_scope: 'all', session_mode: 'shared', priority: 0, created_at: now() });
|
name: 'Test Agent',
|
||||||
|
folder: 'test-agent',
|
||||||
|
is_admin: 0,
|
||||||
|
agent_provider: null,
|
||||||
|
container_config: null,
|
||||||
|
created_at: now(),
|
||||||
|
});
|
||||||
|
createMessagingGroup({
|
||||||
|
id: 'mg-1',
|
||||||
|
channel_type: 'discord',
|
||||||
|
platform_id: 'chan-123',
|
||||||
|
name: 'General',
|
||||||
|
is_group: 1,
|
||||||
|
admin_user_id: null,
|
||||||
|
created_at: now(),
|
||||||
|
});
|
||||||
|
createMessagingGroupAgent({
|
||||||
|
id: 'mga-1',
|
||||||
|
messaging_group_id: 'mg-1',
|
||||||
|
agent_group_id: 'ag-1',
|
||||||
|
trigger_rules: null,
|
||||||
|
response_scope: 'all',
|
||||||
|
session_mode: 'shared',
|
||||||
|
priority: 0,
|
||||||
|
created_at: now(),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should route a message end-to-end', async () => {
|
it('should route a message end-to-end', async () => {
|
||||||
@@ -215,7 +275,12 @@ describe('router', () => {
|
|||||||
channelType: 'discord',
|
channelType: 'discord',
|
||||||
platformId: 'chan-123',
|
platformId: 'chan-123',
|
||||||
threadId: null,
|
threadId: null,
|
||||||
message: { id: 'msg-b', kind: 'chat', content: JSON.stringify({ sender: 'B', text: 'Second' }), timestamp: now() },
|
message: {
|
||||||
|
id: 'msg-b',
|
||||||
|
kind: 'chat',
|
||||||
|
content: JSON.stringify({ sender: 'B', text: 'Second' }),
|
||||||
|
timestamp: now(),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Both should be in the same session
|
// Both should be in the same session
|
||||||
@@ -231,8 +296,24 @@ describe('router', () => {
|
|||||||
|
|
||||||
describe('delivery', () => {
|
describe('delivery', () => {
|
||||||
it('should detect undelivered messages in session DB', () => {
|
it('should detect undelivered messages in session DB', () => {
|
||||||
createAgentGroup({ id: 'ag-1', name: 'Agent', folder: 'agent', is_admin: 0, agent_provider: null, container_config: null, created_at: now() });
|
createAgentGroup({
|
||||||
createMessagingGroup({ id: 'mg-test', channel_type: 'discord', platform_id: 'chan-test', name: 'Test', is_group: 0, admin_user_id: null, created_at: now() });
|
id: 'ag-1',
|
||||||
|
name: 'Agent',
|
||||||
|
folder: 'agent',
|
||||||
|
is_admin: 0,
|
||||||
|
agent_provider: null,
|
||||||
|
container_config: null,
|
||||||
|
created_at: now(),
|
||||||
|
});
|
||||||
|
createMessagingGroup({
|
||||||
|
id: 'mg-test',
|
||||||
|
channel_type: 'discord',
|
||||||
|
platform_id: 'chan-test',
|
||||||
|
name: 'Test',
|
||||||
|
is_group: 0,
|
||||||
|
admin_user_id: null,
|
||||||
|
created_at: now(),
|
||||||
|
});
|
||||||
|
|
||||||
const { session } = resolveSession('ag-1', 'mg-test', null, 'shared');
|
const { session } = resolveSession('ag-1', 'mg-test', null, 'shared');
|
||||||
|
|
||||||
@@ -245,7 +326,10 @@ describe('delivery', () => {
|
|||||||
VALUES ('out-1', datetime('now'), 0, 'chat', 'chan-123', 'discord', ?)`,
|
VALUES ('out-1', datetime('now'), 0, 'chat', 'chan-123', 'discord', ?)`,
|
||||||
).run(JSON.stringify({ text: 'Agent response' }));
|
).run(JSON.stringify({ text: 'Agent response' }));
|
||||||
|
|
||||||
const undelivered = db.prepare("SELECT * FROM messages_out WHERE delivered = 0").all() as Array<{ id: string; content: string }>;
|
const undelivered = db.prepare('SELECT * FROM messages_out WHERE delivered = 0').all() as Array<{
|
||||||
|
id: string;
|
||||||
|
content: string;
|
||||||
|
}>;
|
||||||
db.close();
|
db.close();
|
||||||
|
|
||||||
expect(undelivered).toHaveLength(1);
|
expect(undelivered).toHaveLength(1);
|
||||||
|
|||||||
@@ -89,12 +89,16 @@ async function sweepSession(session: Session): Promise<void> {
|
|||||||
|
|
||||||
for (const msg of staleMessages) {
|
for (const msg of staleMessages) {
|
||||||
if (msg.tries >= MAX_TRIES) {
|
if (msg.tries >= MAX_TRIES) {
|
||||||
db.prepare("UPDATE messages_in SET status = 'failed', status_changed = datetime('now') WHERE id = ?").run(msg.id);
|
db.prepare("UPDATE messages_in SET status = 'failed', status_changed = datetime('now') WHERE id = ?").run(
|
||||||
|
msg.id,
|
||||||
|
);
|
||||||
log.warn('Message marked as failed after max retries', { messageId: msg.id, sessionId: session.id });
|
log.warn('Message marked as failed after max retries', { messageId: msg.id, sessionId: session.id });
|
||||||
} else {
|
} else {
|
||||||
const backoffMs = BACKOFF_BASE_MS * Math.pow(2, msg.tries);
|
const backoffMs = BACKOFF_BASE_MS * Math.pow(2, msg.tries);
|
||||||
const backoffSec = Math.floor(backoffMs / 1000);
|
const backoffSec = Math.floor(backoffMs / 1000);
|
||||||
db.prepare(`UPDATE messages_in SET status = 'pending', status_changed = datetime('now'), process_after = datetime('now', '+${backoffSec} seconds') WHERE id = ?`).run(msg.id);
|
db.prepare(
|
||||||
|
`UPDATE messages_in SET status = 'pending', status_changed = datetime('now'), process_after = datetime('now', '+${backoffSec} seconds') WHERE id = ?`,
|
||||||
|
).run(msg.id);
|
||||||
log.info('Reset stale message with backoff', { messageId: msg.id, tries: msg.tries, backoffMs });
|
log.info('Reset stale message with backoff', { messageId: msg.id, tries: msg.tries, backoffMs });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,7 +106,16 @@ async function sweepSession(session: Session): Promise<void> {
|
|||||||
// 3. Handle recurrence for completed messages
|
// 3. Handle recurrence for completed messages
|
||||||
const completedRecurring = db
|
const completedRecurring = db
|
||||||
.prepare("SELECT * FROM messages_in WHERE status = 'completed' AND recurrence IS NOT NULL")
|
.prepare("SELECT * FROM messages_in WHERE status = 'completed' AND recurrence IS NOT NULL")
|
||||||
.all() as Array<{ id: string; kind: string; content: string; recurrence: string; process_after: string | null; platform_id: string | null; channel_type: string | null; thread_id: string | null }>;
|
.all() as Array<{
|
||||||
|
id: string;
|
||||||
|
kind: string;
|
||||||
|
content: string;
|
||||||
|
recurrence: string;
|
||||||
|
process_after: string | null;
|
||||||
|
platform_id: string | null;
|
||||||
|
channel_type: string | null;
|
||||||
|
thread_id: string | null;
|
||||||
|
}>;
|
||||||
|
|
||||||
for (const msg of completedRecurring) {
|
for (const msg of completedRecurring) {
|
||||||
try {
|
try {
|
||||||
@@ -118,7 +131,7 @@ async function sweepSession(session: Session): Promise<void> {
|
|||||||
).run(newId, msg.kind, nextRun, msg.recurrence, msg.platform_id, msg.channel_type, msg.thread_id, msg.content);
|
).run(newId, msg.kind, nextRun, msg.recurrence, msg.platform_id, msg.channel_type, msg.thread_id, msg.content);
|
||||||
|
|
||||||
// Remove recurrence from the completed message so it doesn't spawn again
|
// Remove recurrence from the completed message so it doesn't spawn again
|
||||||
db.prepare("UPDATE messages_in SET recurrence = NULL WHERE id = ?").run(msg.id);
|
db.prepare('UPDATE messages_in SET recurrence = NULL WHERE id = ?').run(msg.id);
|
||||||
|
|
||||||
log.info('Inserted next recurrence', { originalId: msg.id, newId, nextRun });
|
log.info('Inserted next recurrence', { originalId: msg.id, newId, nextRun });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -48,13 +48,20 @@ export async function routeInbound(event: InboundEvent): Promise<void> {
|
|||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
createMessagingGroup(mg);
|
createMessagingGroup(mg);
|
||||||
log.info('Auto-created messaging group', { id: mgId, channelType: event.channelType, platformId: event.platformId });
|
log.info('Auto-created messaging group', {
|
||||||
|
id: mgId,
|
||||||
|
channelType: event.channelType,
|
||||||
|
platformId: event.platformId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Resolve agent group via messaging_group_agents
|
// 2. Resolve agent group via messaging_group_agents
|
||||||
const agents = getMessagingGroupAgents(mg.id);
|
const agents = getMessagingGroupAgents(mg.id);
|
||||||
if (agents.length === 0) {
|
if (agents.length === 0) {
|
||||||
log.warn('No agent groups configured for messaging group', { messagingGroupId: mg.id, platformId: event.platformId });
|
log.warn('No agent groups configured for messaging group', {
|
||||||
|
messagingGroupId: mg.id,
|
||||||
|
platformId: event.platformId,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +86,12 @@ export async function routeInbound(event: InboundEvent): Promise<void> {
|
|||||||
content: event.message.content,
|
content: event.message.content,
|
||||||
});
|
});
|
||||||
|
|
||||||
log.info('Message routed', { sessionId: session.id, agentGroup: match.agent_group_id, kind: event.message.kind, created });
|
log.info('Message routed', {
|
||||||
|
sessionId: session.id,
|
||||||
|
agentGroup: match.agent_group_id,
|
||||||
|
kind: event.message.kind,
|
||||||
|
created,
|
||||||
|
});
|
||||||
|
|
||||||
// 5. Wake container
|
// 5. Wake container
|
||||||
const freshSession = getSession(session.id);
|
const freshSession = getSession(session.id);
|
||||||
|
|||||||
@@ -35,7 +35,12 @@ function generateId(): string {
|
|||||||
* Find or create a session for a messaging group + thread.
|
* Find or create a session for a messaging group + thread.
|
||||||
* Returns the session and whether it was newly created.
|
* 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 } {
|
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 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
|
// For per-thread mode, look for an active session with this specific thread
|
||||||
const lookupThreadId = sessionMode === 'shared' ? null : threadId;
|
const lookupThreadId = sessionMode === 'shared' ? null : threadId;
|
||||||
@@ -83,17 +88,21 @@ export function initSessionFolder(agentGroupId: string, sessionId: string): void
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Write a message to a session's messages_in table. */
|
/** Write a message to a session's messages_in table. */
|
||||||
export function writeSessionMessage(agentGroupId: string, sessionId: string, message: {
|
export function writeSessionMessage(
|
||||||
id: string;
|
agentGroupId: string,
|
||||||
kind: string;
|
sessionId: string,
|
||||||
timestamp: string;
|
message: {
|
||||||
platformId?: string | null;
|
id: string;
|
||||||
channelType?: string | null;
|
kind: string;
|
||||||
threadId?: string | null;
|
timestamp: string;
|
||||||
content: string;
|
platformId?: string | null;
|
||||||
processAfter?: string | null;
|
channelType?: string | null;
|
||||||
recurrence?: string | null;
|
threadId?: string | null;
|
||||||
}): void {
|
content: string;
|
||||||
|
processAfter?: string | null;
|
||||||
|
recurrence?: string | null;
|
||||||
|
},
|
||||||
|
): void {
|
||||||
const dbPath = sessionDbPath(agentGroupId, sessionId);
|
const dbPath = sessionDbPath(agentGroupId, sessionId);
|
||||||
const db = new Database(dbPath);
|
const db = new Database(dbPath);
|
||||||
db.pragma('journal_mode = WAL');
|
db.pragma('journal_mode = WAL');
|
||||||
|
|||||||
Reference in New Issue
Block a user