refactor: move destinations from JSON file into inbound.db
The per-session destination map was being written as a sidecar JSON file (/workspace/.nanoclaw-destinations.json) — inconsistent with the rest of v2, where all host↔container IO goes through inbound.db / outbound.db. Move it into a `destinations` table in INBOUND_SCHEMA. The host writes it before every container wake AND on demand (e.g. after create_agent) so the creator sees the new child destination mid-session without a restart. The container queries the table live on every lookup — no cache, no staleness window. - src/db/schema.ts: add `destinations` table to INBOUND_SCHEMA. - src/session-manager.ts: writeDestinationsFile → writeDestinations, writes via DELETE + INSERT inside a transaction. - src/delivery.ts: create_agent handler calls writeDestinations on the creator's session after inserting the new destination rows. - container/agent-runner/src/destinations.ts: queries inbound.db directly in every findByName/getAllDestinations/findByRouting call. No more cache. No setDestinationsForTest (obsolete). No fs import. - container/agent-runner/src/index.ts and mcp-tools/index.ts: remove loadDestinations() calls — no longer needed. - Test helper initTestSessionDb creates the destinations table. Integration test inserts a row directly instead of mocking the cache. No backwards compatibility: sessions predating the schema update must be recreated. This is fine on the v2 branch. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,7 @@ import {
|
||||
markContainerRunning,
|
||||
markContainerStopped,
|
||||
sessionDir,
|
||||
writeDestinationsFile,
|
||||
writeDestinations,
|
||||
} from './session-manager.js';
|
||||
import type { AgentGroup, Session } from './types.js';
|
||||
|
||||
@@ -59,8 +59,8 @@ export async function wakeContainer(session: Session): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Refresh the destination map file so any admin changes take effect on wake
|
||||
writeDestinationsFile(agentGroup.id, session.id);
|
||||
// Refresh the destination map so any admin changes take effect on wake
|
||||
writeDestinations(agentGroup.id, session.id);
|
||||
|
||||
const mounts = buildMounts(agentGroup, session);
|
||||
const containerName = `nanoclaw-v2-${agentGroup.folder}-${Date.now()}`;
|
||||
|
||||
@@ -76,7 +76,7 @@ CREATE TABLE pending_questions (
|
||||
* outbound.db — container writes, host reads (read-only open)
|
||||
*/
|
||||
|
||||
/** Host-owned: inbound messages + delivery tracking. */
|
||||
/** Host-owned: inbound messages + delivery tracking + destination map. */
|
||||
export const INBOUND_SCHEMA = `
|
||||
CREATE TABLE messages_in (
|
||||
id TEXT PRIMARY KEY,
|
||||
@@ -101,6 +101,19 @@ CREATE TABLE delivered (
|
||||
status TEXT NOT NULL DEFAULT 'delivered',
|
||||
delivered_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- Destination map for this session's agent.
|
||||
-- Host overwrites on every container wake AND on demand (admin rewires, new child agents, etc.).
|
||||
-- Container queries this live on every lookup, so admin changes take effect
|
||||
-- mid-session without requiring a container restart.
|
||||
CREATE TABLE destinations (
|
||||
name TEXT PRIMARY KEY,
|
||||
display_name TEXT,
|
||||
type TEXT NOT NULL, -- 'channel' | 'agent'
|
||||
channel_type TEXT, -- for type='channel'
|
||||
platform_id TEXT, -- for type='channel'
|
||||
agent_group_id TEXT -- for type='agent'
|
||||
);
|
||||
`;
|
||||
|
||||
/** Container-owned: outbound messages + processing acknowledgments. */
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
sessionDir,
|
||||
inboundDbPath,
|
||||
resolveSession,
|
||||
writeDestinations,
|
||||
writeSessionMessage,
|
||||
writeSystemResponse,
|
||||
} from './session-manager.js';
|
||||
@@ -611,6 +612,10 @@ async function handleSystemAction(
|
||||
created_at: now,
|
||||
});
|
||||
|
||||
// Refresh the creator's destination map so the new child appears
|
||||
// immediately on the next query — no restart needed.
|
||||
writeDestinations(session.agent_group_id, session.id);
|
||||
|
||||
// Fire-and-forget notification back to the creator
|
||||
notifyAgent(
|
||||
session,
|
||||
|
||||
@@ -132,43 +132,73 @@ export function initSessionFolder(agentGroupId: string, sessionId: string): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the destination map file into the session folder.
|
||||
* Called before every container wake so admin changes take effect on next start.
|
||||
* The container loads this at startup to know what destinations exist.
|
||||
* Write the session's destination map into its inbound.db `destinations` table.
|
||||
*
|
||||
* Called before every container wake so admin changes take effect on next start —
|
||||
* but the container also re-queries on demand, so mid-session admin changes
|
||||
* (e.g. spawning a new child agent) can also call this to push the new map
|
||||
* without restarting the container.
|
||||
*
|
||||
* Uses DELETE + INSERT in a transaction for a clean overwrite.
|
||||
*/
|
||||
export function writeDestinationsFile(agentGroupId: string, sessionId: string): void {
|
||||
const dir = sessionDir(agentGroupId, sessionId);
|
||||
if (!fs.existsSync(dir)) return;
|
||||
export function writeDestinations(agentGroupId: string, sessionId: string): void {
|
||||
const dbPath = inboundDbPath(agentGroupId, sessionId);
|
||||
if (!fs.existsSync(dbPath)) return;
|
||||
|
||||
const rows = getDestinations(agentGroupId);
|
||||
const destinations: Array<Record<string, unknown>> = [];
|
||||
type DestRow = {
|
||||
name: string;
|
||||
display_name: string | null;
|
||||
type: 'channel' | 'agent';
|
||||
channel_type: string | null;
|
||||
platform_id: string | null;
|
||||
agent_group_id: string | null;
|
||||
};
|
||||
const resolved: DestRow[] = [];
|
||||
|
||||
for (const row of rows) {
|
||||
if (row.target_type === 'channel') {
|
||||
const mg = getMessagingGroup(row.target_id);
|
||||
if (!mg) continue;
|
||||
destinations.push({
|
||||
resolved.push({
|
||||
name: row.local_name,
|
||||
displayName: mg.name ?? row.local_name,
|
||||
display_name: mg.name ?? row.local_name,
|
||||
type: 'channel',
|
||||
channelType: mg.channel_type,
|
||||
platformId: mg.platform_id,
|
||||
channel_type: mg.channel_type,
|
||||
platform_id: mg.platform_id,
|
||||
agent_group_id: null,
|
||||
});
|
||||
} else if (row.target_type === 'agent') {
|
||||
const ag = getAgentGroup(row.target_id);
|
||||
if (!ag) continue;
|
||||
destinations.push({
|
||||
resolved.push({
|
||||
name: row.local_name,
|
||||
displayName: ag.name,
|
||||
display_name: ag.name,
|
||||
type: 'agent',
|
||||
agentGroupId: ag.id,
|
||||
channel_type: null,
|
||||
platform_id: null,
|
||||
agent_group_id: ag.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const filePath = path.join(dir, '.nanoclaw-destinations.json');
|
||||
fs.writeFileSync(filePath, JSON.stringify({ destinations }, null, 2));
|
||||
log.debug('Destination map written', { sessionId, count: destinations.length });
|
||||
const db = new Database(dbPath);
|
||||
db.pragma('journal_mode = DELETE');
|
||||
db.pragma('busy_timeout = 5000');
|
||||
try {
|
||||
const tx = db.transaction((entries: DestRow[]) => {
|
||||
db.prepare('DELETE FROM destinations').run();
|
||||
const stmt = db.prepare(
|
||||
`INSERT INTO destinations (name, display_name, type, channel_type, platform_id, agent_group_id)
|
||||
VALUES (@name, @display_name, @type, @channel_type, @platform_id, @agent_group_id)`,
|
||||
);
|
||||
for (const e of entries) stmt.run(e);
|
||||
});
|
||||
tx(resolved);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
log.debug('Destination map written', { sessionId, count: resolved.length });
|
||||
}
|
||||
|
||||
/** Write a message to a session's inbound DB (messages_in). Host-only. */
|
||||
|
||||
Reference in New Issue
Block a user