fix: deduplicate CI watchers and remove task ID from watcher prompts

Prevent duplicate CI completion notifications in paired rooms by checking
for existing active watchers with the same channel+provider+metadata before
creating a new one. Remove Task ID from watcher prompt construction to
avoid router secret redaction (task- IDs contain sk- pattern), passing
EJCLAW_RUNTIME_TASK_ID via env var instead and making cancel_task
auto-resolve when task_id is omitted.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eyejoker
2026-03-26 20:57:59 +09:00
parent b6dc500daf
commit 7b63ba859d
12 changed files with 74 additions and 71 deletions

View File

@@ -27,6 +27,7 @@ const chatJid = process.env.EJCLAW_CHAT_JID!;
const groupFolder = process.env.EJCLAW_GROUP_FOLDER!;
const isMain = process.env.EJCLAW_IS_MAIN === '1';
const agentType = process.env.EJCLAW_AGENT_TYPE || 'claude-code';
const runtimeTaskId = process.env.EJCLAW_RUNTIME_TASK_ID;
const allowGenericScheduling = agentType !== 'codex';
function writeIpcFile(dir: string, data: object): string {
@@ -299,7 +300,6 @@ server.tool(
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,
checkInstructions,
});
@@ -329,7 +329,7 @@ server.tool(
content: [
{
type: 'text' as const,
text: `CI watcher scheduled: ${taskId} (${pollSeconds}s)`,
text: `CI watcher scheduled for ${target} (${pollSeconds}s interval)`,
},
],
};
@@ -414,12 +414,20 @@ server.tool(
server.tool(
'cancel_task',
'Cancel and delete a scheduled task.',
{ task_id: z.string().describe('The task ID to cancel') },
async (args: { task_id: string }) => {
'Cancel and delete a scheduled task. If no task_id is provided, cancels the current task (for background watchers).',
{ task_id: z.string().optional().describe('The task ID to cancel. Omit to cancel the current task.') },
async (args: { task_id?: string }) => {
const resolvedId = args.task_id || runtimeTaskId;
if (!resolvedId) {
return {
content: [{ type: 'text' as const, text: 'No task_id provided and no current task context available.' }],
isError: true,
};
}
const data = {
type: 'cancel_task',
taskId: args.task_id,
taskId: resolvedId,
groupFolder,
isMain,
timestamp: new Date().toISOString(),
@@ -427,7 +435,7 @@ server.tool(
writeIpcFile(TASKS_DIR, data);
return { content: [{ type: 'text' as const, text: `Task ${args.task_id} cancellation requested.` }] };
return { content: [{ type: 'text' as const, text: `Task cancellation requested.` }] };
},
);

View File

@@ -10,7 +10,6 @@ export interface NormalizeWatchCiIntervalOptions {
}
export interface BuildCiWatchPromptArgs {
taskId: string;
target: string;
checkInstructions: string;
}
@@ -48,7 +47,6 @@ export function normalizeWatchCiIntervalSeconds(
}
export function buildCiWatchPrompt({
taskId,
target,
checkInstructions,
}: BuildCiWatchPromptArgs): string {
@@ -60,9 +58,6 @@ You are running as an EJClaw background CI watcher.
Watch target:
${target}
Task ID:
${taskId}
Check instructions:
${checkInstructions}
@@ -81,7 +76,7 @@ Rules:
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.
7. Call \`cancel_task\` (no arguments needed) 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.

View File

@@ -10,14 +10,13 @@ import {
describe('watch-ci helpers', () => {
it('builds a self-cancelling CI watch prompt', () => {
const prompt = buildCiWatchPrompt({
taskId: 'task-123',
target: 'PR #42 checks',
checkInstructions:
'Use gh pr checks 42 and summarize only terminal results.',
});
expect(prompt).toContain('PR #42 checks');
expect(prompt).toContain('task-123');
expect(prompt).not.toContain('Task ID:');
expect(prompt).toContain('cancel_task');
expect(prompt).toContain('send_message');
expect(prompt).toContain('gh pr checks 42');