fix: restore regressions from discord-only refactor
This commit is contained in:
@@ -511,6 +511,18 @@ async function runQuery(
|
||||
NANOCLAW_IS_MAIN: containerInput.isMain ? '1' : '0',
|
||||
},
|
||||
},
|
||||
...(process.env.MEMENTO_MCP_SSE_URL
|
||||
? {
|
||||
'memento-mcp': {
|
||||
command: process.env.MEMENTO_MCP_REMOTE_PATH || 'mcp-remote',
|
||||
args: [
|
||||
process.env.MEMENTO_MCP_SSE_URL,
|
||||
'--header',
|
||||
`Authorization:Bearer ${process.env.MEMENTO_ACCESS_KEY || ''}`,
|
||||
],
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
hooks: {
|
||||
PreCompact: [{ hooks: [createPreCompactHook(containerInput.assistantName)] }],
|
||||
@@ -539,7 +551,22 @@ async function runQuery(
|
||||
if (message.type === 'result') {
|
||||
resultCount++;
|
||||
const textResult = 'result' in message ? (message as { result?: string }).result : null;
|
||||
const isError = message.subtype?.startsWith('error');
|
||||
log(`Result #${resultCount}: subtype=${message.subtype}${textResult ? ` text=${textResult.slice(0, 200)}` : ''}`);
|
||||
if (isError) {
|
||||
// Log full error details for debugging
|
||||
const msg = message as Record<string, unknown>;
|
||||
const errorDetail = JSON.stringify({
|
||||
subtype: message.subtype,
|
||||
result: textResult?.slice(0, 500),
|
||||
errors: msg.errors,
|
||||
stop_reason: msg.stop_reason,
|
||||
duration_ms: msg.duration_ms,
|
||||
duration_api_ms: msg.duration_api_ms,
|
||||
session_id: msg.session_id,
|
||||
});
|
||||
log(`Error result detail: ${errorDetail}`);
|
||||
}
|
||||
writeOutput({
|
||||
status: 'success',
|
||||
result: textResult || null,
|
||||
@@ -747,7 +774,11 @@ async function main(): Promise<void> {
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||
const errorStack = err instanceof Error ? err.stack : undefined;
|
||||
const errorCause = err instanceof Error && err.cause ? String(err.cause) : undefined;
|
||||
log(`Agent error: ${errorMessage}`);
|
||||
if (errorStack) log(`Stack: ${errorStack}`);
|
||||
if (errorCause) log(`Cause: ${errorCause}`);
|
||||
writeOutput({
|
||||
status: 'error',
|
||||
result: null,
|
||||
|
||||
@@ -10,6 +10,11 @@ import { z } from 'zod';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { CronExpressionParser } from 'cron-parser';
|
||||
import {
|
||||
buildCiWatchPrompt,
|
||||
DEFAULT_WATCH_CI_CONTEXT_MODE,
|
||||
normalizeWatchCiIntervalSeconds,
|
||||
} from './watch-ci.js';
|
||||
|
||||
const IPC_DIR = process.env.NANOCLAW_IPC_DIR || '/workspace/ipc';
|
||||
const MESSAGES_DIR = path.join(IPC_DIR, 'messages');
|
||||
@@ -152,6 +157,90 @@ SCHEDULE VALUE FORMAT (all times are LOCAL timezone):
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
'watch_ci',
|
||||
'Schedule a background CI watcher that checks until a run or check reaches a terminal state, then sends one message and cancels itself.',
|
||||
{
|
||||
target: z
|
||||
.string()
|
||||
.describe(
|
||||
'What to watch, for example "PR #123 checks" or "GitHub Actions run 987654321".',
|
||||
),
|
||||
check_instructions: z
|
||||
.string()
|
||||
.describe(
|
||||
'Exact steps or commands to check status and what details matter when it finishes.',
|
||||
),
|
||||
poll_interval_seconds: z
|
||||
.number()
|
||||
.int()
|
||||
.min(30)
|
||||
.max(3600)
|
||||
.default(60)
|
||||
.describe('How often to poll in seconds. Default 60, minimum 30.'),
|
||||
context_mode: z
|
||||
.enum(['group', 'isolated'])
|
||||
.default(DEFAULT_WATCH_CI_CONTEXT_MODE)
|
||||
.describe(
|
||||
'group=runs with chat history and memory, isolated=fresh session (include all context in check_instructions). Default: isolated.',
|
||||
),
|
||||
target_group_jid: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'(Main group only) JID of the group to schedule the watcher for. Defaults to the current group.',
|
||||
),
|
||||
},
|
||||
async (args) => {
|
||||
let pollSeconds: number;
|
||||
try {
|
||||
pollSeconds = normalizeWatchCiIntervalSeconds(args.poll_interval_seconds);
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const targetJid =
|
||||
isMain && args.target_group_jid ? args.target_group_jid : chatJid;
|
||||
const taskId = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
const prompt = buildCiWatchPrompt({
|
||||
taskId,
|
||||
target: args.target,
|
||||
checkInstructions: args.check_instructions,
|
||||
});
|
||||
|
||||
const data = {
|
||||
type: 'schedule_task',
|
||||
taskId,
|
||||
prompt,
|
||||
schedule_type: 'interval' as const,
|
||||
schedule_value: String(pollSeconds * 1000),
|
||||
context_mode: args.context_mode || DEFAULT_WATCH_CI_CONTEXT_MODE,
|
||||
targetJid,
|
||||
createdBy: groupFolder,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
writeIpcFile(TASKS_DIR, data);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: `CI watcher scheduled: ${taskId} (${pollSeconds}s)`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
'list_tasks',
|
||||
"List all scheduled tasks. From main: shows all tasks. From other groups: shows only that group's tasks.",
|
||||
|
||||
72
runners/agent-runner/src/watch-ci.ts
Normal file
72
runners/agent-runner/src/watch-ci.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
export const DEFAULT_WATCH_CI_INTERVAL_SECONDS = 60;
|
||||
export const MIN_WATCH_CI_INTERVAL_SECONDS = 30;
|
||||
export const MAX_WATCH_CI_INTERVAL_SECONDS = 3600;
|
||||
export const DEFAULT_WATCH_CI_CONTEXT_MODE = 'isolated';
|
||||
|
||||
export interface BuildCiWatchPromptArgs {
|
||||
taskId: string;
|
||||
target: string;
|
||||
checkInstructions: string;
|
||||
}
|
||||
|
||||
export function normalizeWatchCiIntervalSeconds(seconds?: number): number {
|
||||
if (seconds === undefined) {
|
||||
return DEFAULT_WATCH_CI_INTERVAL_SECONDS;
|
||||
}
|
||||
|
||||
if (!Number.isInteger(seconds)) {
|
||||
throw new Error('poll_interval_seconds must be an integer.');
|
||||
}
|
||||
|
||||
if (
|
||||
seconds < MIN_WATCH_CI_INTERVAL_SECONDS ||
|
||||
seconds > MAX_WATCH_CI_INTERVAL_SECONDS
|
||||
) {
|
||||
throw new Error(
|
||||
`poll_interval_seconds must be between ${MIN_WATCH_CI_INTERVAL_SECONDS} and ${MAX_WATCH_CI_INTERVAL_SECONDS}.`,
|
||||
);
|
||||
}
|
||||
|
||||
return seconds;
|
||||
}
|
||||
|
||||
export function buildCiWatchPrompt({
|
||||
taskId,
|
||||
target,
|
||||
checkInstructions,
|
||||
}: BuildCiWatchPromptArgs): string {
|
||||
return `
|
||||
[BACKGROUND CI WATCH]
|
||||
|
||||
You are running as an EJClaw background CI watcher.
|
||||
|
||||
Watch target:
|
||||
${target}
|
||||
|
||||
Task ID:
|
||||
${taskId}
|
||||
|
||||
Check instructions:
|
||||
${checkInstructions}
|
||||
|
||||
Rules:
|
||||
- Use the watch target and check instructions in this prompt as the source of truth for what to inspect.
|
||||
- On each run, check whether the target is still queued, pending, running, in progress, or otherwise non-terminal.
|
||||
- If it is still not finished, send no visible message and end this run quietly.
|
||||
- If it reached a terminal state such as success, failure, cancelled, timed out, neutral, skipped, or action required:
|
||||
1. Send exactly one concise completion message with \`send_message\`.
|
||||
2. Format it as a short multiline summary when possible, not one long paragraph.
|
||||
3. Preferred shape:
|
||||
- First line: \`CI 완료: <target>\`
|
||||
- Second line: \`판정: <one-line conclusion>\`
|
||||
- Then 2-4 flat bullet points with only the most important metrics, errors, or comparisons.
|
||||
- Optional final line: \`다음: <next action>\` if a concrete follow-up is needed.
|
||||
4. Adapt the content to the specific CI. Do not invent fixed fields when they do not fit.
|
||||
5. Avoid tables unless they are clearly the shortest readable format.
|
||||
6. Keep the message compact and easy for other agents to parse.
|
||||
7. Call \`cancel_task\` with task_id "${taskId}" so this watcher stops itself.
|
||||
- If you hit a transient problem such as a rate limit, network issue, or temporary auth failure, send no visible message and leave the task active for the next retry.
|
||||
- Prefer no normal final response. Use \`send_message\` for the completion message, and keep any non-user-facing notes inside \`<internal>\` tags if needed.
|
||||
- Do not claim continued monitoring after you cancel the task.
|
||||
`.trim();
|
||||
}
|
||||
Reference in New Issue
Block a user