feat: agent-to-agent communication, dynamic agent creation, self-modification tools

Agent-to-agent: host routes messages with channel_type='agent' to target
agent's inbound.db, enriches with sender info, wakes target container.
Bidirectional routing works via inherited routing context.

Dynamic agents: create_agent MCP tool + system action handler creates
agent groups, folders, and optional CLAUDE.md on the fly.

Self-modification: install_packages (apt/npm, requires admin approval),
add_mcp_server (no approval), request_rebuild (builds per-agent-group
Docker image with approved packages). Approval flow reuses interactive
card infrastructure with pending_approvals table.

Also includes fixes from prior session: attachment download, reply context
extraction, message editing (platform message ID tracking), delivery retry
limits, and card update on button click.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-04-10 01:10:34 +03:00
parent 9af9bc947a
commit d8fbd3b239
24 changed files with 1025 additions and 78 deletions

View File

@@ -90,8 +90,10 @@ export function initTestSessionDb(): { inbound: Database.Database; outbound: Dat
content TEXT NOT NULL
);
CREATE TABLE delivered (
message_out_id TEXT PRIMARY KEY,
delivered_at TEXT NOT NULL
message_out_id TEXT PRIMARY KEY,
platform_message_id TEXT,
status TEXT NOT NULL DEFAULT 'delivered',
delivered_at TEXT NOT NULL
);
`);

View File

@@ -70,16 +70,37 @@ export function writeMessageOut(msg: WriteMessageOut): number {
/**
* Look up a message's platform ID by seq number.
* Searches both inbound and outbound DBs since seq spans both.
*
* For inbound messages, the Chat SDK message ID is already the platform message ID
* (e.g., "6037840640:42" for Telegram).
*
* For outbound messages, the internal ID (msg-xxx) won't work for edits/reactions.
* Instead, look up the platform_message_id from the delivered table (host writes this
* after successful delivery).
*/
export function getMessageIdBySeq(seq: number): string | null {
const inRow = getInboundDb().prepare('SELECT id FROM messages_in WHERE seq = ?').get(seq) as
const inbound = getInboundDb();
// Inbound messages: ID is already the platform message ID
const inRow = inbound.prepare('SELECT id FROM messages_in WHERE seq = ?').get(seq) as
| { id: string }
| undefined;
if (inRow) return inRow.id;
// Outbound messages: look up platform message ID from delivered table
const outRow = getOutboundDb().prepare('SELECT id FROM messages_out WHERE seq = ?').get(seq) as
| { id: string }
| undefined;
return outRow?.id ?? null;
if (!outRow) return null;
// Check if host has stored the platform message ID after delivery
const deliveredRow = inbound
.prepare('SELECT platform_message_id FROM delivered WHERE message_out_id = ?')
.get(outRow.id) as { platform_message_id: string | null } | undefined;
if (deliveredRow?.platform_message_id) return deliveredRow.platform_message_id;
// Fallback to internal ID (edits/reactions on undelivered messages won't work)
return outRow.id;
}
/** Get undelivered messages (for host polling — reads from outbound.db). */