feat(v2): builder-agent self-modification WIP + container-config as per-group file

Checkpoints the builder-agent dev-agent/worktree/swap flow (create_dev_agent,
request_swap, classifier, deadman, promote) before pivoting to a unified
draft-activate approach with OS-level RO enforcement. Lifts container_config
out of the agent_groups row into groups/<folder>/container.json so install_packages,
add_mcp_server, and rebuild flows can eventually route through the same draft
path as source edits.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-04-15 18:42:10 +03:00
parent c54c779834
commit 75c2fde2b5
48 changed files with 4385 additions and 134 deletions

View File

@@ -21,7 +21,7 @@ import { getChannelAdapter } from './channels/channel-registry.js';
import { isMember } from './db/agent-group-members.js';
import { getMessagingGroupByPlatform, createMessagingGroup, getMessagingGroupAgents } from './db/messaging-groups.js';
import { upsertUser, getUser } from './db/users.js';
import { triggerTyping } from './delivery.js';
import { startTypingRefresh } from './delivery.js';
import { log } from './log.js';
import { resolveSession, writeSessionMessage } from './session-manager.js';
import { wakeContainer } from './container-runner.js';
@@ -148,8 +148,20 @@ export async function routeInbound(event: InboundEvent): Promise<void> {
created,
});
// 7. Show typing indicator while agent processes
triggerTyping(event.channelType, event.platformId, event.threadId);
// 7. Show typing indicator while the agent processes. Refresh on a short
// interval so platforms like Discord (which auto-expire typing after
// ~10s) keep showing it for the full thinking window. Gated on the
// heartbeat file's mtime after an initial grace period, so typing stops
// as soon as the agent goes idle — not when the container eventually
// exits. Container-runner also calls stopTypingRefresh on exit as a
// fast-path cleanup.
startTypingRefresh(
session.id,
session.agent_group_id,
event.channelType,
event.platformId,
event.threadId,
);
// 8. Wake container
const freshSession = getSession(session.id);
@@ -189,14 +201,26 @@ function extractAndUpsertUser(event: InboundEvent): string | null {
return null;
}
const senderId = typeof content.senderId === 'string' ? content.senderId : undefined;
const sender = typeof content.sender === 'string' ? content.sender : undefined;
const senderName = typeof content.senderName === 'string' ? content.senderName : undefined;
// chat-sdk-bridge serializes author info as a nested `author.userId` and
// does NOT populate top-level `senderId`. Older adapters (v1, native) put
// `senderId` or `sender` directly at the top level. Check all three.
const senderIdField = typeof content.senderId === 'string' ? content.senderId : undefined;
const senderField = typeof content.sender === 'string' ? content.sender : undefined;
const author = typeof content.author === 'object' && content.author !== null
? (content.author as Record<string, unknown>)
: undefined;
const authorUserId = typeof author?.userId === 'string' ? (author.userId as string) : undefined;
const senderName =
(typeof content.senderName === 'string' ? content.senderName : undefined) ??
(typeof author?.fullName === 'string' ? (author.fullName as string) : undefined) ??
(typeof author?.userName === 'string' ? (author.userName as string) : undefined);
const handle = senderId ?? sender;
if (!handle) return null;
const rawHandle = senderIdField ?? senderField ?? authorUserId;
if (!rawHandle) return null;
const userId = `${event.channelType}:${handle}`;
// If the raw handle already contains ':' it's pre-namespaced (the older
// adapters put it in that form). Otherwise prepend the channel type.
const userId = rawHandle.includes(':') ? rawHandle : `${event.channelType}:${rawHandle}`;
if (!getUser(userId)) {
upsertUser({
id: userId,