Skip to content

Commit 11bae6c

Browse files
committed
fix: tighten failed-start retry concurrency and kickoff handling
1 parent 3e57ed1 commit 11bae6c

4 files changed

Lines changed: 112 additions & 13 deletions

File tree

apps/web/src/app/(sandbox)/task/[taskId]/startup/StartupMessage.client.test.tsx

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/web/src/app/(sandbox)/task/[taskId]/startup/StartupMessage.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,17 @@ export const StartupFailureMessage = ({
218218
{promptText}
219219
</div>
220220
)}
221+
{promptImages.length > 0 && (
222+
<div className="flex flex-wrap gap-2">
223+
{promptImages.map((image) => (
224+
<Button key={image} variant="outline" size="sm" asChild>
225+
<a href={image} target="_blank" rel="noreferrer">
226+
View attachment
227+
</a>
228+
</Button>
229+
))}
230+
</div>
231+
)}
221232
</div>
222233
)}
223234
{retryAction && (

packages/cloud-agents/src/server/__tests__/enqueue-task.test.ts

Lines changed: 50 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/cloud-agents/src/server/task-run-queue.ts

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
type TaskPhase,
1919
RunStatus,
2020
TaskPayloadKind,
21+
TASK_KICKOFF_MESSAGE_SOURCE,
2122
activeRunStatuses,
2223
DEFAULT_DELEGATED_KEEPALIVE_MS,
2324
DEFAULT_KEEPALIVE_MS,
@@ -1600,24 +1601,17 @@ export async function enqueueTaskRelaunch(
16001601
);
16011602
}
16021603

1603-
const activeRun = await db.query.taskRuns.findFirst({
1604+
// Provider kickoff/start rows (for example Slack “started”) are written
1605+
// before provisioning and must not block restart after a failed start.
1606+
const priorHarnessMessage = await db.query.taskMessages.findFirst({
16041607
where: and(
1605-
eq(taskRuns.taskId, existingTask.id),
1606-
inArray(taskRuns.status, [...activeRunStatuses]),
1608+
eq(taskMessages.runId, sourceRun.id),
1609+
sql`coalesce(${taskMessages.metadata}->>'source', '') <> ${TASK_KICKOFF_MESSAGE_SOURCE}`,
16071610
),
16081611
columns: { id: true },
16091612
});
16101613

1611-
if (activeRun) {
1612-
throw new Error('This task already has an active run.');
1613-
}
1614-
1615-
const priorMessage = await db.query.taskMessages.findFirst({
1616-
where: eq(taskMessages.runId, sourceRun.id),
1617-
columns: { id: true },
1618-
});
1619-
1620-
if (priorMessage) {
1614+
if (priorHarnessMessage) {
16211615
throw new Error(
16221616
'Only failed environment starts can be restarted. This run already has task messages.',
16231617
);
@@ -1673,6 +1667,36 @@ export async function enqueueTaskRelaunch(
16731667
);
16741668

16751669
const taskRun = await db.transaction(async (tx) => {
1670+
// Serialize concurrent retries on the same task so two retries cannot both
1671+
// observe an empty active-run set and insert separate pending runs.
1672+
await tx.execute(
1673+
sql`SELECT id FROM tasks WHERE id = ${existingTask.id} FOR UPDATE`,
1674+
);
1675+
1676+
const activeRun = await tx.query.taskRuns.findFirst({
1677+
where: and(
1678+
eq(taskRuns.taskId, existingTask.id),
1679+
inArray(taskRuns.status, [...activeRunStatuses]),
1680+
),
1681+
columns: { id: true },
1682+
});
1683+
1684+
if (activeRun) {
1685+
throw new Error('This task already has an active run.');
1686+
}
1687+
1688+
const stillFailed = await tx.query.taskRuns.findFirst({
1689+
where: and(
1690+
eq(taskRuns.id, sourceRun.id),
1691+
eq(taskRuns.status, RunStatus.Failed),
1692+
),
1693+
columns: { id: true },
1694+
});
1695+
1696+
if (!stillFailed) {
1697+
throw new Error('Only failed task starts can be retried.');
1698+
}
1699+
16761700
const [insertedRun] = await tx
16771701
.insert(taskRuns)
16781702
.values({

0 commit comments

Comments
 (0)