Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/scripts/keepalive_loop.js
Original file line number Diff line number Diff line change
Expand Up @@ -2967,6 +2967,9 @@ async function updateKeepaliveLoopSummary({ github: rawGithub, context, core, in
// tasks off. Re-derive the counter here with the authoritative counts.
// When force_retry is active, honour the evaluate-step's reset to 0 and
// do not overwrite it — the human explicitly wants a fresh start.
// After a review action, reset to 0 so the next evaluate triggers a run
// instead of another review (the review already provided course-correction
// feedback — the agent needs a chance to act on it).
if (isForceRetry) {
if (roundsWithoutTaskCompletion !== 0) {
core?.info?.(
Expand All @@ -2975,6 +2978,12 @@ async function updateKeepaliveLoopSummary({ github: rawGithub, context, core, in
);
roundsWithoutTaskCompletion = 0;
}
} else if (action === 'review') {
core?.info?.(
`[summary] review action completed — resetting rounds_without_task_completion ` +
`from ${roundsWithoutTaskCompletion} to 0 so next iteration runs the agent`,
);
roundsWithoutTaskCompletion = 0;
} else {
const prevTasks = previousState?.tasks || {};
const prevUncheckedForCounter = toNumber(prevTasks.unchecked, tasksUnchecked);
Expand Down
162 changes: 145 additions & 17 deletions .github/workflows/agents-auto-pilot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1886,23 +1886,91 @@ jobs:
return;
}

// Force-dispatch Codex belt dispatcher to create the branch
try {
await withRetry((client) => client.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'agents-71-codex-belt-dispatcher.yml',
ref: baseBranch,
inputs: {
agent_key: agentKey,
force_issue: issueNumber.toString(),
dry_run: 'false'
// Force-dispatch Codex belt dispatcher to create the branch.
// Retry up to 3 times because GitHub Actions can silently cancel
// queued runs before they receive a runner (observed on issue #34).
const maxDispatchAttempts = 3;
let dispatchSucceeded = false;
for (let attempt = 1; attempt <= maxDispatchAttempts; attempt++) {
const dispatchedAt = new Date();
try {
await withRetry((client) => client.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'agents-71-codex-belt-dispatcher.yml',
ref: baseBranch,
inputs: {
agent_key: agentKey,
force_issue: issueNumber.toString(),
dry_run: 'false'
}
}));
core.info(
`Dispatched belt dispatcher (agent: ${agentKey}) ` +
`for issue #${issueNumber} (attempt ${attempt}/${maxDispatchAttempts})`
);
} catch (dispatchError) {
core.warning(
`Belt dispatch attempt ${attempt} failed: ${dispatchError?.message}`
);
if (attempt < maxDispatchAttempts) {
await new Promise(r => setTimeout(r, attempt * 5000));
}
}));
const prefix = `Dispatched belt dispatcher (agent: ${agentKey}) for issue`;
core.info(`${prefix} #${issueNumber}`);
} catch (dispatchError) {
core.warning(`Could not dispatch belt dispatcher: ${dispatchError?.message}`);
continue;
}

// Wait briefly then verify the dispatched run is queued/in_progress
// (not cancelled before receiving a runner). Only consider runs
// created after the dispatch timestamp to avoid matching stale runs
// for other issues.
await new Promise(r => setTimeout(r, 15000));
try {
const { data: runs } = await withRetry((client) =>
client.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'agents-71-codex-belt-dispatcher.yml',
per_page: 5,
})
);
const recentRuns = runs.workflow_runs.filter(
r => new Date(r.created_at) >= dispatchedAt &&
r.event === 'workflow_dispatch'
);
const alive = recentRuns.find(
r => r.status === 'queued' || r.status === 'in_progress'
);
if (alive) {
core.info(`Belt dispatcher run ${alive.id} is ${alive.status}`);
dispatchSucceeded = true;
break;
}
const succeeded = recentRuns.find(
r => r.conclusion === 'success'
);
if (succeeded) {
core.info(`Belt dispatcher run ${succeeded.id} already succeeded`);
dispatchSucceeded = true;
break;
}
const latest = recentRuns[0] || runs.workflow_runs[0];
core.warning(
`Belt dispatcher run not alive after attempt ${attempt}` +
` (latest: ${latest?.id} ${latest?.conclusion}); ` +
(attempt < maxDispatchAttempts ? 'retrying…' : 'no more attempts.')
);
} catch (checkError) {
core.warning(
`Could not verify dispatcher run after attempt ${attempt}: ` +
`${checkError?.message}; status unknown, will retry.`
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message at line 1965 always says "will retry" regardless of which attempt it is. On the final attempt (attempt 3), this message is misleading because there won't be another retry. Consider making this message conditional like the one at line 1960, showing either "will retry" or "no more attempts" based on whether attempt is less than maxDispatchAttempts.

Suggested change
`${checkError?.message}; status unknown, will retry.`
`${checkError?.message}; ` +
(attempt < maxDispatchAttempts ? 'status unknown, will retry.' : 'status unknown, no more attempts.')

Copilot uses AI. Check for mistakes.
);
}
}
if (!dispatchSucceeded) {
core.warning(
`Belt dispatcher could not be confirmed after ${maxDispatchAttempts} attempts ` +
`for issue #${issueNumber}. The branch-check loop will re-dispatch if needed.`
);
}

- name: Metrics - End capability check timer
Expand Down Expand Up @@ -2304,14 +2372,74 @@ jobs:
const actualBackoffMs = Math.min(backoffMs, maxBackoffMs);
const actualMinutes = Math.round(actualBackoffMs / 60000);

// Re-dispatch the belt if no recent dispatcher run is active
// for this issue. Only consider runs created in the last 30
// minutes to avoid matching stale runs for other issues.
let redispatched = false;
try {
const { data: runs } = await withRetry((client) =>
client.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'agents-71-codex-belt-dispatcher.yml',
per_page: 10,
})
);
const cutoff = new Date(Date.now() - 30 * 60 * 1000);
const recentRuns = runs.workflow_runs.filter(
r => new Date(r.created_at) >= cutoff &&
r.event === 'workflow_dispatch'
);
const alive = recentRuns.find(
r => r.status === 'queued' || r.status === 'in_progress'
);
if (!alive) {
core.info(
`No active belt dispatcher run in last 30m ` +
`(${recentRuns.length} recent runs checked); re-dispatching`
);
const { data: repoInfo } = await withRetry((client) =>
client.rest.repos.get({
owner: context.repo.owner,
repo: context.repo.repo,
})
);
const dispatchRef = repoInfo.default_branch || 'main';
await withRetry((client) =>
client.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'agents-71-codex-belt-dispatcher.yml',
ref: dispatchRef,
inputs: {
agent_key: agentKey,
force_issue: String(issueNumber),
dry_run: 'false',
},
})
);
redispatched = true;
core.info(`Re-dispatched belt for issue #${issueNumber}`);
} else {
core.info(
`Belt dispatcher run ${alive.id} still ${alive.status}; skipping re-dispatch`
);
}
Comment on lines +2375 to +2427
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The re-dispatch logic checks for any active belt dispatcher run in the last 30 minutes but doesn't verify it's for the current issue. The comment at line 2376 states "Re-dispatch the belt if no recent dispatcher run is active for this issue", but the code at lines 2393-2395 checks for any run with status 'queued' or 'in_progress' without filtering by the issue number.

This could cause the re-dispatch to be skipped when another issue's belt dispatcher is running, even though the current issue might need its own dispatcher to be re-dispatched. Consider filtering the recentRuns to only include runs for the current issue, possibly by checking the workflow run's inputs or associated pull request.

Copilot uses AI. Check for mistakes.
} catch (redispatchErr) {
core.warning(`Belt re-dispatch check failed: ${redispatchErr?.message}`);
}

const redispatchNote = redispatched
? ' Re-dispatched belt dispatcher.'
: '';
core.info(`Applying branch-creation backoff: waiting ${actualMinutes} minutes`);
await withRetry((client) => client.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: `🤖 **Auto-pilot**: Backoff delay (${actualMinutes}m)

Branch not created yet. Attempt ${stallCount + 1}/${maxStallRetries}.
Branch not created yet. Attempt ${stallCount + 1}/${maxStallRetries}.${redispatchNote}
Waiting before retry...`
}));

Expand Down
Loading