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

@@ -70,6 +70,7 @@ function buildBaseRunnerEnv(args: {
groupSessionsDir: string;
agentType: AgentType;
envVars: Record<string, string>;
runtimeTaskId?: string;
}): Record<string, string> {
const cleanEnv = { ...(process.env as Record<string, string>) };
for (const [key, value] of Object.entries(args.envVars)) {
@@ -103,6 +104,9 @@ function buildBaseRunnerEnv(args: {
EJCLAW_IS_MAIN: args.isMain ? '1' : '0',
EJCLAW_AGENT_TYPE: args.agentType,
CLAUDE_CONFIG_DIR: args.groupSessionsDir,
...(args.runtimeTaskId
? { EJCLAW_RUNTIME_TASK_ID: args.runtimeTaskId }
: {}),
};
}
@@ -422,6 +426,7 @@ export function prepareGroupEnvironment(
groupSessionsDir,
agentType,
envVars,
runtimeTaskId,
});
if (agentType === 'codex') {

View File

@@ -469,9 +469,6 @@ describe('task CRUD', () => {
Watch target:
cleanup
Task ID:
task-cleanup
Check instructions:
Check the run.
`.trim(),

View File

@@ -936,6 +936,25 @@ export function getTaskById(id: string): ScheduledTask | undefined {
| undefined;
}
/**
* Find an existing active/paused CI watcher for the same channel + provider + metadata.
* Used to prevent duplicate watchers when both agents register for the same CI run.
*/
export function findDuplicateCiWatcher(
chatJid: string,
ciProvider: string,
ciMetadata: string,
): ScheduledTask | undefined {
return db
.prepare(
`SELECT * FROM scheduled_tasks
WHERE chat_jid = ? AND ci_provider = ? AND ci_metadata = ?
AND status IN ('active', 'paused')
LIMIT 1`,
)
.get(chatJid, ciProvider, ciMetadata) as ScheduledTask | undefined;
}
export function getTasksForGroup(
groupFolder: string,
agentType?: AgentType,

View File

@@ -62,9 +62,6 @@ describe('github-ci helpers', () => {
Watch target:
GitHub Actions run 123456
Task ID:
task-github
Check instructions:
Managed by host-driven watcher.
`.trim(),
@@ -128,9 +125,6 @@ Managed by host-driven watcher.
Watch target:
GitHub Actions run 654321
Task ID:
task-github
Check instructions:
Managed by host-driven watcher.
`.trim(),

View File

@@ -563,9 +563,6 @@ describe('schedule_task schedule types', () => {
Watch target:
GitHub Actions run 123456
Task ID:
task-watch-ci
Check instructions:
Check the run.
`.trim(),
@@ -598,9 +595,6 @@ Check the run.
Watch target:
GitHub Actions run 654321
Task ID:
task-watch-github
Check instructions:
Managed by host-driven watcher.
`.trim(),

View File

@@ -11,7 +11,13 @@ import {
} from './config.js';
import { readJsonFile } from './utils.js';
import { AvailableGroup } from './agent-runner.js';
import { createTask, deleteTask, getTaskById, updateTask } from './db.js';
import {
createTask,
deleteTask,
findDuplicateCiWatcher,
getTaskById,
updateTask,
} from './db.js';
import { isValidGroupFolder } from './group-folder.js';
import { logger } from './logger.js';
import {
@@ -394,6 +400,28 @@ export async function processTaskIpc(
nextRun = date.toISOString();
}
// Deduplicate CI watchers: if another agent already watches the same
// channel + provider + run, skip creation to avoid duplicate notifications.
if (data.ci_provider && data.ci_metadata) {
const existing = findDuplicateCiWatcher(
resolvedTargetJid,
data.ci_provider,
data.ci_metadata as string,
);
if (existing) {
logger.info(
{
existingTaskId: existing.id,
existingAgentType: existing.agent_type,
ciProvider: data.ci_provider,
sourceGroup,
},
'Duplicate CI watcher skipped — another agent already watches this run',
);
break;
}
}
const taskId =
data.taskId ||
`task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;

View File

@@ -326,9 +326,6 @@ describe('task scheduler', () => {
Watch target:
GitHub Actions run 123456
Task ID:
task-watch-group
Check instructions:
Check the run.
`.trim(),
@@ -890,9 +887,6 @@ Check the run.
Watch target:
GitHub Actions run 654321
Task ID:
task-watch-immediate
Check instructions:
Check the run.
`.trim(),
@@ -928,9 +922,6 @@ Check the run.
Watch target:
GitHub Actions run 123456
Task ID:
task-watch-runtime
Check instructions:
Check the run.
`.trim(),
@@ -1004,9 +995,6 @@ Check the run.
Watch target:
GitHub Actions run 123456
Task ID:
task-github-running
Check instructions:
Managed by host-driven watcher.
`.trim(),
@@ -1082,9 +1070,6 @@ Managed by host-driven watcher.
Watch target:
GitHub Actions run 654321
Task ID:
task-github-complete
Check instructions:
Managed by host-driven watcher.
`.trim(),
@@ -1149,9 +1134,6 @@ Managed by host-driven watcher.
Watch target:
GitHub Actions run 222222
Task ID:
task-github-backoff
Check instructions:
Managed by host-driven watcher.
`.trim(),
@@ -1220,9 +1202,6 @@ Managed by host-driven watcher.
Watch target:
GitHub Actions run 333333
Task ID:
task-github-pause
Check instructions:
Managed by host-driven watcher.
`.trim(),
@@ -1284,9 +1263,6 @@ Managed by host-driven watcher.
Watch target:
GitHub Actions run 999999
Task ID:
task-watch-expired
Check instructions:
Check the run.
`.trim(),
@@ -1389,9 +1365,6 @@ Check the run.
Watch target:
GitHub Actions run 123456
Task ID:
task-123
Check instructions:
Check the run.
`.trim();
@@ -1428,9 +1401,6 @@ Check the run.
Watch target:
GitHub Actions run 123456
Task ID:
task-123
Check instructions:
Check the run.
`.trim();
@@ -1463,9 +1433,6 @@ Check the run.
Watch target:
GitHub Actions run 123456
Task ID:
task-watch-status
Check instructions:
Check the run.
`.trim(),
@@ -1527,9 +1494,6 @@ Check the run.
Watch target:
PR #77 checks
Task ID:
task-watch-status-edit-fail
Check instructions:
Check the run.
`.trim(),

View File

@@ -27,7 +27,7 @@ export function isTaskStatusControlMessage(content: string): boolean {
}
export function extractWatchCiTarget(prompt: string): string | null {
const match = prompt.match(/Watch target:\n([\s\S]*?)\n\nTask ID:/);
const match = prompt.match(/Watch target:\n([\s\S]*?)\n\nCheck instructions:/);
return match?.[1]?.trim() || null;
}

View File

@@ -10,17 +10,17 @@ describe('summarizeWatcherTasks', () => {
const summary = summarizeWatcherTasks([
{
prompt:
'[BACKGROUND CI WATCH]\n\nWatch target:\nGitHub Actions run 1\n\nTask ID:\na',
'[BACKGROUND CI WATCH]\n\nWatch target:\nGitHub Actions run 1\n\nCheck instructions:\na',
status: 'active',
},
{
prompt:
'[BACKGROUND CI WATCH]\n\nWatch target:\nGitHub Actions run 2\n\nTask ID:\nb',
'[BACKGROUND CI WATCH]\n\nWatch target:\nGitHub Actions run 2\n\nCheck instructions:\nb',
status: 'paused',
},
{
prompt:
'[BACKGROUND CI WATCH]\n\nWatch target:\nGitHub Actions run 3\n\nTask ID:\nc',
'[BACKGROUND CI WATCH]\n\nWatch target:\nGitHub Actions run 3\n\nCheck instructions:\nc',
status: 'completed',
},
{