Skip to content
Closed
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
160 changes: 143 additions & 17 deletions .github/workflows/agents-auto-pilot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1886,23 +1886,90 @@ 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
);
Comment on lines +1936 to +1938
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 post-dispatch verification uses created_at >= dispatchedAt (runner clock) and only checks for any queued/in_progress run. This can produce false negatives (clock skew or GitHub timestamp lag) and false positives (matching a different workflow run created after dispatchedAt). Filter more defensively (e.g., allow a small negative skew window and require event == 'workflow_dispatch' and head_branch == baseBranch), so dispatchSucceeded reflects the run you just dispatched.

Suggested change
const recentRuns = runs.workflow_runs.filter(
r => new Date(r.created_at) >= dispatchedAt
);
const skewMs = 5000; // allow small negative skew / timestamp lag
const skewedDispatchedAt = new Date(dispatchedAt.getTime() - skewMs);
const baseBranch =
(context.payload.pull_request && context.payload.pull_request.base && context.payload.pull_request.base.ref) ||
(context.ref ? context.ref.replace('refs/heads/', '') : 'main');
const recentRuns = runs.workflow_runs.filter(r => {
const createdAt = new Date(r.created_at);
const createdAfterDispatch = createdAt >= skewedDispatchedAt;
const isWorkflowDispatch = r.event === 'workflow_dispatch';
const matchesBranch = r.head_branch === baseBranch;
return createdAfterDispatch && isWorkflowDispatch && matchesBranch;
});

Copilot uses AI. Check for mistakes.
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.`
);
}
}
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 +2371,73 @@ 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
);
const alive = recentRuns.find(
r => r.status === 'queued' || r.status === 'in_progress'
);
Comment on lines +2379 to +2393
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 guard treats any recent belt-dispatcher run in queued/in_progress as “alive”, without confirming it corresponds to this issue/dispatch (no event/head_branch/time correlation). If another issue has an active dispatcher run, this issue may never re-dispatch and will keep backing off. Tighten the “alive” check (at least event === 'workflow_dispatch' and expected ref/branch) or incorporate issue-specific correlation before skipping re-dispatch.

Copilot uses AI. Check for mistakes.
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`
);
}
} 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
1 change: 1 addition & 0 deletions .github/workflows/agents-keepalive-loop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -859,6 +859,7 @@ jobs:
- evaluate
- run-codex
- run-claude
- progress-review
# Run if PR exists, handle skipped/failed agent jobs gracefully
# run-codex will be skipped when action != run/fix/conflict, which is expected
if: |
Expand Down
Loading