mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-06-04 10:14:47 +08:00
v2: SQLite state adapter, admin commands, compact feedback
- Replace in-memory Chat SDK state with SqliteStateAdapter — thread subscriptions now persist across restarts - Add migration 002 for chat_sdk_kv, subscriptions, locks, lists tables - Handle /clear in agent-runner (reset sessionId) — SDK has supportsNonInteractive:false for this command - Pass /compact, /context, /cost, /files through to SDK as admin commands - Skip admin commands in follow-up poll so they start fresh queries - Emit compact_boundary events as user-visible feedback messages - Pass NANOCLAW_ADMIN_USER_ID and NANOCLAW_ASSISTANT_NAME to containers Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@ import type { MessageInRow } from './db/messages-in.js';
|
||||
*/
|
||||
export type CommandCategory = 'admin' | 'filtered' | 'passthrough' | 'none';
|
||||
|
||||
const ADMIN_COMMANDS = new Set(['/remote-control', '/clear', '/compact']);
|
||||
const ADMIN_COMMANDS = new Set(['/remote-control', '/clear', '/compact', '/context', '/cost', '/files']);
|
||||
const FILTERED_COMMANDS = new Set(['/help', '/login', '/logout', '/doctor', '/config']);
|
||||
|
||||
export interface CommandInfo {
|
||||
|
||||
@@ -81,7 +81,6 @@ export async function runPollLoop(config: PollLoopConfig): Promise<void> {
|
||||
|
||||
if (cmdInfo.category === 'admin') {
|
||||
if (!adminUserId || cmdInfo.senderId !== adminUserId) {
|
||||
// Not admin — send error, mark completed
|
||||
log(`Admin command denied: ${cmdInfo.command} from ${cmdInfo.senderId} (msg: ${msg.id})`);
|
||||
writeMessageOut({
|
||||
id: generateId(),
|
||||
@@ -94,7 +93,24 @@ export async function runPollLoop(config: PollLoopConfig): Promise<void> {
|
||||
commandIds.push(msg.id);
|
||||
continue;
|
||||
}
|
||||
// Admin user — format as system command
|
||||
// Handle admin commands directly
|
||||
if (cmdInfo.command === '/clear') {
|
||||
log('Clearing session (resetting sessionId)');
|
||||
sessionId = undefined;
|
||||
resumeAt = undefined;
|
||||
writeMessageOut({
|
||||
id: generateId(),
|
||||
kind: 'chat',
|
||||
platform_id: routing.platformId,
|
||||
channel_type: routing.channelType,
|
||||
thread_id: routing.threadId,
|
||||
content: JSON.stringify({ text: 'Session cleared.' }),
|
||||
});
|
||||
commandIds.push(msg.id);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Other admin commands — pass through to agent
|
||||
normalMessages.push(msg);
|
||||
continue;
|
||||
}
|
||||
@@ -174,25 +190,16 @@ function formatMessagesWithCommands(messages: MessageInRow[]): string {
|
||||
for (const msg of messages) {
|
||||
if (msg.kind === 'chat' || msg.kind === 'chat-sdk') {
|
||||
const cmdInfo = categorizeMessage(msg);
|
||||
if (cmdInfo.category === 'passthrough') {
|
||||
if (cmdInfo.category === 'passthrough' || cmdInfo.category === 'admin') {
|
||||
// Flush normal batch first
|
||||
if (normalBatch.length > 0) {
|
||||
parts.push(formatMessages(normalBatch));
|
||||
normalBatch.length = 0;
|
||||
}
|
||||
// Pass raw command text (no XML wrapping)
|
||||
// Pass raw command text (no XML wrapping) — SDK handles it natively
|
||||
parts.push(cmdInfo.text);
|
||||
continue;
|
||||
}
|
||||
if (cmdInfo.category === 'admin') {
|
||||
// Format admin command as a system command block
|
||||
if (normalBatch.length > 0) {
|
||||
parts.push(formatMessages(normalBatch));
|
||||
normalBatch.length = 0;
|
||||
}
|
||||
parts.push(`[SYSTEM COMMAND: ${cmdInfo.command}]\n${cmdInfo.text}`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
normalBatch.push(msg);
|
||||
}
|
||||
@@ -218,8 +225,15 @@ async function processQuery(query: AgentQuery, routing: RoutingContext, config:
|
||||
const pollHandle = setInterval(() => {
|
||||
if (done) return;
|
||||
|
||||
// Skip system messages — they're responses for MCP tools (e.g., ask_user_question)
|
||||
const newMessages = getPendingMessages().filter((m) => m.kind !== 'system');
|
||||
// Skip system messages (MCP tool responses) and admin commands (need fresh query)
|
||||
const newMessages = getPendingMessages().filter((m) => {
|
||||
if (m.kind === 'system') return false;
|
||||
if (m.kind === 'chat' || m.kind === 'chat-sdk') {
|
||||
const cmd = categorizeMessage(m);
|
||||
if (cmd.category === 'admin') return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (newMessages.length > 0) {
|
||||
const newIds = newMessages.map((m) => m.id);
|
||||
markProcessing(newIds);
|
||||
|
||||
@@ -212,6 +212,10 @@ export class ClaudeProvider implements AgentProvider {
|
||||
yield { type: 'error', message: 'API retry', retryable: true };
|
||||
} else if (message.type === 'system' && (message as { subtype?: string }).subtype === 'rate_limit_event') {
|
||||
yield { type: 'error', message: 'Rate limit', retryable: false, classification: 'quota' };
|
||||
} else if (message.type === 'system' && (message as { subtype?: string }).subtype === 'compact_boundary') {
|
||||
const meta = (message as { compact_metadata?: { pre_tokens?: number } }).compact_metadata;
|
||||
const detail = meta?.pre_tokens ? ` (${meta.pre_tokens.toLocaleString()} tokens compacted)` : '';
|
||||
yield { type: 'result', text: `Context compacted${detail}.` };
|
||||
} else if (message.type === 'system' && (message as { subtype?: string }).subtype === 'task_notification') {
|
||||
const tn = message as { summary?: string };
|
||||
yield { type: 'progress', message: tn.summary || 'Task notification' };
|
||||
|
||||
Reference in New Issue
Block a user