mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-07-12 19:00:58 +08:00
fix(cli): provision companion rows on ncl groups create and ncl wirings create
`ncl groups create` only inserted into `agent_groups`, leaving no `container_configs` row — so the first spawn for that group threw "Container config not found" until the next host restart kicked in the backfill. `ncl wirings create` only inserted into `messaging_group_agents`, leaving no companion `agent_destinations` row — so the agent could receive messages on the wired channel but its replies got silently dropped by the delivery ACL. Both bugs share the same shape: the resources are declared in `src/cli/resources/*.ts` with `create: 'approval'` and no custom handler, so creation falls through to `genericCreate` in `src/cli/crud.ts`, which is a raw INSERT. The domain functions (`initGroupFilesystem`, `createMessagingGroupAgent`) wire the companion rows, but the generic path skips them. This adds a `postCreate` hook to `ResourceDef`. `genericCreate` calls it after the INSERT with the values that were written, so generated fields (`id`, `created_at`) are populated. `groups.ts` uses it to call `initGroupFilesystem`; `wirings.ts` uses it to create the destination row. The destination logic was extracted from `createMessagingGroupAgent` into a new exported `ensureAgentDestinationForWiring`, so the wirings hook doesn't have to re-INSERT the wiring row to get the destination side effect. Closes #2415 Closes #2389
This commit is contained in:
@@ -71,6 +71,15 @@ export interface ResourceDef {
|
||||
};
|
||||
/** Non-standard verbs (grant, revoke, add, remove, restart, etc.). */
|
||||
customOperations?: Record<string, CustomOperation>;
|
||||
/**
|
||||
* 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<string, unknown>) => Promise<void> | 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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
+31
-20
@@ -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,
|
||||
// <sessionId>)` 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, <sessionId>)` 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);
|
||||
|
||||
Reference in New Issue
Block a user