Q157: a run can be cancelled before it starts, and a failed or stopped one retried or resumed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-09 02:11:11 -04:00
1 parent 71ac5085cd
commit a075f931d5
12 files changed
+114 -22

No files matched your search

@@ -47,6 +47,7 @@ export const organizationContracts = {
deleteTask: z.strictObject({ orgId: id, taskId: id }) satisfies z.ZodType<C.DeleteTaskInput>,
setTaskBlocked: z.strictObject({ orgId: id, taskId: id, reason: z.string().nullable() }) satisfies z.ZodType<C.SetTaskBlockedInput>,
taskDeleteImpact: z.strictObject({ orgId: id, taskId: id }),
cancelQueuedRun: z.strictObject({ orgId: id, runId: id }),
updateTaskStatus: z.strictObject({ orgId: id, taskId: id, status: z.enum(TASK_STATUS) }) satisfies z.ZodType<C.UpdateTaskStatusInput>,
runTask: z.strictObject({ orgId: id, taskId: id }) satisfies z.ZodType<C.RunTaskInput>,
createGoal: z.strictObject({ orgId: id, title: z.string(), description: z.string(), ownerAgentId: nullableId }) satisfies z.ZodType<C.CreateGoalInput>,
+2
View File
@@ -37,6 +37,7 @@ import { coalesce } from "./coalesce";
import { normalizeExternalBrowserUrl } from "./external-url";
import { resumeGoal } from "../services/goal-review-service";
import { stopAllRuns, stopRun } from "../services/run-control";
import { cancelQueuedRun } from "../services/run-cancel";
export interface IpcDeps {
connection: DatabaseConnection;
@@ -166,6 +167,7 @@ export function registerIpcHandlers(deps: IpcDeps): void {
handle("updateTask", (input) => mutate(() => organization.updateTask(input)));
handle("setTaskBlocked", (input) => mutate(() => organization.setTaskBlocked(input)));
handle("taskDeleteImpact", (input) => ok(organization.taskDeleteImpact(input.orgId, input.taskId)));
handle("cancelQueuedRun", (input) => mutate(() => cancelQueuedRun(deps.connection.db, input.orgId, input.runId)));
handle("deleteTask", (input) => mutate(() => organization.deleteTask(input)));
handle("updateTaskStatus", (input) => mutate(() => organization.updateTaskStatus(input.orgId, input.taskId, input.status)));
handle("runTask", async (input) => {
+14
View File
@@ -0,0 +1,14 @@
import type { DatabaseSync } from "node:sqlite";
import { TASK_STATUS } from "../../shared/domain";
import { OrganizationService } from "./organization-service";
/** A chat-wake task only carries its reply run, so the run ending closes it; left open it would strand forever. */
export function closeChatWakeTask(db: DatabaseSync, orgId: string, runId: string): void {
const run = db.prepare("select r.task_id, t.origin from task_runs r join tasks t on t.id = r.task_id where r.id = ? and r.org_id = ?")
.get(runId, orgId) as { task_id: string; origin: string | null } | undefined;
if (!run || run.origin !== "chat_wake") return;
const task = db.prepare("select status from tasks where id = ? and org_id = ?")
.get(run.task_id, orgId) as { status: string } | undefined;
if (!task || (task.status !== TASK_STATUS.BACKLOG && task.status !== TASK_STATUS.IN_PROGRESS)) return;
new OrganizationService(db).updateTaskStatus(orgId, run.task_id, TASK_STATUS.DONE);
}
+15
View File
@@ -0,0 +1,15 @@
import type { DatabaseSync } from "node:sqlite";
import { AuditService } from "./audit-service";
import { closeChatWakeTask } from "./chat-wake-close";
/** Cancels a run that has not started: not a failure, no retry budget spent; a chat reply it carried closes. */
export function cancelQueuedRun(db: DatabaseSync, orgId: string, runId: string): void {
const result = db.prepare(`
update task_runs set state = 'stopped', completed_at = ?, error_message = 'Cancelled before it started.'
where id = ? and org_id = ? and state = 'queued'
`).run(new Date().toISOString(), runId, orgId);
if (result.changes === 0) throw new Error("That run is no longer queued.");
console.info(`[task-run] run ${runId} cancelled before it started`);
closeChatWakeTask(db, orgId, runId);
new AuditService(db).record({ orgId, entityType: "task_run", entityId: runId, actorType: "employee", actorId: null, action: "cancelled", payload: {} });
}
+4 -14
View File
@@ -1,3 +1,4 @@
import { closeChatWakeTask } from "./chat-wake-close";
import type { DatabaseSync } from "node:sqlite";
import type { RunTaskInput } from "../../shared/commands";
import { TASK_STATUS, WRITING_MODE } from "../../shared/domain";
@@ -436,18 +437,7 @@ export class TaskExecutionService {
set state = ?, provider_request_id = coalesce(?, provider_request_id), completed_at = ?, error_message = null
where id = ? and org_id = ?
`).run(TASK_RUN_STATE.COMPLETED, providerRequestId, new Date().toISOString(), runId, orgId);
if (!checkpoint) this.closeChatWakeTask(orgId, runId);
}
/** A chat-wake task only carries its reply run, so the run completing closes it; left open it would strand forever. */
private closeChatWakeTask(orgId: string, runId: string): void {
const run = this.db.prepare("select r.task_id, t.origin from task_runs r join tasks t on t.id = r.task_id where r.id = ? and r.org_id = ?")
.get(runId, orgId) as { task_id: string; origin: string | null } | undefined;
if (!run || run.origin !== "chat_wake") return;
const task = this.db.prepare("select status from tasks where id = ? and org_id = ?")
.get(run.task_id, orgId) as { status: string } | undefined;
if (!task || (task.status !== TASK_STATUS.BACKLOG && task.status !== TASK_STATUS.IN_PROGRESS)) return;
this.organization.updateTaskStatus(orgId, run.task_id, TASK_STATUS.DONE);
if (!checkpoint) closeChatWakeTask(this.db, orgId, runId);
}
/** The operator stopped the run: not a failure, no retry budget spent, no memory recorded. */
@@ -456,7 +446,7 @@ export class TaskExecutionService {
update task_runs set state = ?, provider_request_id = coalesce(?, provider_request_id), completed_at = ?, error_message = ?
where id = ? and org_id = ?
`).run(TASK_RUN_STATE.STOPPED, providerRequestId, new Date().toISOString(), reason, runId, orgId);
this.closeChatWakeTask(orgId, runId);
closeChatWakeTask(this.db, orgId, runId);
this.audit.record({ orgId, entityType: "task_run", entityId: runId, actorType: "system", actorId: null, action: "stopped", payload: { ...payload, reason } });
}
@@ -466,7 +456,7 @@ export class TaskExecutionService {
console.info(`[task-run] run ${runId} ${message}`);
this.db.prepare("update task_runs set state = ?, completed_at = ?, error_message = ? where id = ? and org_id = ?")
.run(TASK_RUN_STATE.SUPERSEDED, new Date().toISOString(), message, runId, orgId);
this.closeChatWakeTask(orgId, runId);
closeChatWakeTask(this.db, orgId, runId);
this.audit.record({
orgId,
entityType: "task_run",