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:
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>,
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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: {} });
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user