diff --git a/src/cli/crud.ts b/src/cli/crud.ts index 18b52793b..e8a86acb9 100644 --- a/src/cli/crud.ts +++ b/src/cli/crud.ts @@ -71,6 +71,15 @@ export interface ResourceDef { }; /** Non-standard verbs (grant, revoke, add, remove, restart, etc.). */ customOperations?: Record; + /** + * Runs after a successful `create` INSERT, with the row that was just + * written. Used to wire in side effects that the central row alone + * doesn't trigger — e.g. creating a `container_configs` row when a new + * agent group is added, or the companion `agent_destinations` row when a + * wiring is added. The hook receives the same `values` object that was + * inserted, so generated fields like `id` and `created_at` are populated. + */ + postCreate?: (row: Record) => Promise | void; } // --------------------------------------------------------------------------- @@ -162,6 +171,7 @@ function genericCreate(def: ResourceDef) { getDb() .prepare(`INSERT INTO ${def.table} (${colNames.join(', ')}) VALUES (${placeholders.join(', ')})`) .run(values); + if (def.postCreate) await def.postCreate(values); return values; }; } diff --git a/src/cli/resources/groups.ts b/src/cli/resources/groups.ts index 27da6417f..3a64b2642 100644 --- a/src/cli/resources/groups.ts +++ b/src/cli/resources/groups.ts @@ -12,6 +12,7 @@ import { updateContainerConfigScalars, updateContainerConfigJson, } from '../../db/container-configs.js'; +import { initGroupFilesystem } from '../../group-init.js'; import { createAgentFromTemplate } from '../../templates/create-agent.js'; import type { AgentGroup, ContainerConfigRow } from '../../types.js'; import { registerResource } from '../crud.js'; @@ -90,6 +91,15 @@ registerResource({ created_at: new Date().toISOString(), }; createAgentGroup(group); + // Provision the workspace folder and the `container_configs` row that + // `getContainerConfig` and the spawn path require. Without this, a + // group created via `ncl groups create` would throw "Container config + // not found" on first spawn and stay broken until the host restart + // backfill ran (#2415). The template branch above provisions its own + // config + folder in `createAgentFromTemplate`; this covers the bare + // path. Mirrors what `setup/register.ts` does after creating an agent + // group via the setup flow. + initGroupFilesystem(group); return group; }, }, diff --git a/src/cli/resources/wirings.ts b/src/cli/resources/wirings.ts index d52f8b152..c7de00e5d 100644 --- a/src/cli/resources/wirings.ts +++ b/src/cli/resources/wirings.ts @@ -1,3 +1,5 @@ +import { ensureAgentDestinationForWiring } from '../../db/messaging-groups.js'; +import type { MessagingGroupAgent } from '../../types.js'; import { registerResource } from '../crud.js'; registerResource({ @@ -67,4 +69,13 @@ registerResource({ { name: 'created_at', type: 'string', description: 'Auto-set.', generated: true }, ], operations: { list: 'open', get: 'open', create: 'approval', update: 'approval', delete: 'approval' }, + postCreate: (row) => { + // Create the companion `agent_destinations` row so the agent has a + // local name it can address this chat by. Without this, the agent + // generates a response, but delivery's ACL drops the outbound message + // (no destination matches the target) and the reply is silently lost. + // `createMessagingGroupAgent` does this automatically; the generic + // CRUD path doesn't, hence this hook. See issue #2389. + ensureAgentDestinationForWiring(row as unknown as MessagingGroupAgent); + }, }); diff --git a/src/db/messaging-groups.ts b/src/db/messaging-groups.ts index 93aace0a2..a728f8683 100644 --- a/src/db/messaging-groups.ts +++ b/src/db/messaging-groups.ts @@ -183,26 +183,37 @@ export function createMessagingGroupAgent(mga: MessagingGroupAgent): void { ) .run(mga); - // Auto-create an agent_destinations row so delivery's ACL doesn't block - // outbound messages that target this chat. Guarded: when the agent-to-agent - // module isn't installed the table doesn't exist — skip silently. Without - // the module, the ACL check in delivery is also skipped (same guard), so - // channel sends still work. - // - // ⚠️ DESTINATION PROJECTION NOTE: this function only writes the central - // `agent_destinations` row. It does NOT project into any running - // agent's session inbound.db (see top-of-file invariant in - // src/modules/agent-to-agent/db/agent-destinations.ts). In practice this - // is fine because the only real callers are one-shot setup scripts - // (setup/register.ts, scripts/init-first-agent.ts, /manage-channels - // skill) that run in a separate process from the host. Any already- - // running container for `mga.agent_group_id` will keep serving the - // stale projection until its next wake (idle timeout or next inbound - // message) at which point spawnContainer's writeDestinations call - // refreshes from central. If you call this from code that runs INSIDE - // the host process and need the refresh to happen immediately, - // explicitly call the module's `writeDestinations(mga.agent_group_id, - // )` afterwards. + ensureAgentDestinationForWiring(mga); +} + +/** + * Create the `agent_destinations` row that lets the agent address this chat + * as a delivery target. Idempotent — no-op when the destination already + * exists, the agent-to-agent module isn't installed, or the messaging group + * has been deleted out from under the wiring. + * + * Split out from `createMessagingGroupAgent` so callers that already wrote + * the `messaging_group_agents` row directly (e.g. the generic ncl CRUD path) + * can still get the companion destination without re-inserting the wiring. + * + * ⚠️ DESTINATION PROJECTION NOTE: this function only writes the central + * `agent_destinations` row. It does NOT project into any running agent's + * session inbound.db (see top-of-file invariant in + * src/modules/agent-to-agent/db/agent-destinations.ts). In practice this is + * fine because the only real callers are one-shot setup paths + * (setup/register.ts, scripts/init-first-agent.ts, /manage-channels skill, + * ncl wirings create) that run in a separate process from the host. Any + * already-running container for `mga.agent_group_id` will keep serving the + * stale projection until its next wake (idle timeout or next inbound + * message) at which point spawnContainer's writeDestinations call refreshes + * from central. If you call this from code that runs INSIDE the host + * process and need the refresh to happen immediately, explicitly call the + * module's `writeDestinations(mga.agent_group_id, )` afterwards. + */ +export function ensureAgentDestinationForWiring(mga: MessagingGroupAgent): void { + // Guarded: when the agent-to-agent module isn't installed the table + // doesn't exist — skip silently. Without the module, the ACL check in + // delivery is also skipped (same guard), so channel sends still work. if (!hasTable(getDb(), 'agent_destinations')) return; const existing = getDestinationByTarget(mga.agent_group_id, 'channel', mga.messaging_group_id);