feat: 支持 Queue 终态幂等重试

This commit is contained in:
root
2026-07-12 02:48:41 +02:00
parent 0386058f5b
commit 3eec6d6da2
13 changed files with 1405 additions and 33 deletions
+81
View File
@@ -115,6 +115,8 @@ async function dispatch(args: ParsedArgs): Promise<CliResult> {
if (group === "queue" && command === "read" && id) return readQueueTask(args, id);
if (group === "queue" && command === "cancel" && id) return cancelQueueTask(args, id);
if (group === "queue" && command === "dispatch" && id) return dispatchQueueTask(args, id);
if (group === "queue" && command === "retry" && id) return retryQueueTaskCli(args, id);
if (group === "queue" && command === "attempts" && id) return listQueueAttemptsCli(args, id);
if (group === "queue" && command === "refresh" && id) return refreshQueueTask(args, id);
if (group === "runs" && command === "create") return client(args).post("/api/v1/runs", await jsonFile(args));
if (group === "runs" && command === "show" && id) return showRun(args, id);
@@ -573,6 +575,61 @@ export function summarizeQueueDispatchResult(result: JsonValue, taskId: string):
};
}
export function summarizeQueueRetryResult(result: JsonValue, taskId: string): JsonRecord {
const record = jsonRecordValue(result);
if (!record) throw new AgentRunError("schema-invalid", "queue retry response must be an object", { httpStatus: 2 });
const task = jsonRecordValue(record.task);
const attempt = jsonRecordValue(record.attempt);
const run = jsonRecordValue(record.run);
const command = jsonRecordValue(record.command);
const runnerJob = jsonRecordValue(record.runnerJob);
const runId = stringValue(run?.id) ?? stringValue(attempt?.runId);
const commandId = stringValue(command?.id) ?? stringValue(attempt?.commandId);
const sessionId = stringValue(attempt?.sessionId) ?? stringValue(jsonRecordValue(task?.sessionRef)?.sessionId);
return {
action: "queue-retry-summary",
taskId: stringValue(task?.id) ?? taskId,
mutation: record.mutation === true,
idempotentReplay: record.idempotentReplay === true,
task: summarizeQueueTaskRecord(task, taskId),
attempt: summarizeAttemptRecord(attempt),
previousAttempt: summarizeAttemptRecord(jsonRecordValue(record.previousAttempt)),
allowedTransition: jsonRecordValue(record.allowedTransition),
run: summarizeRunRecord(run),
command: summarizeCommandRecord(command),
runnerJob: summarizeRunnerJobRecord(runnerJob),
fullResponseBytes: jsonByteLength(result),
valuesPrinted: false,
pollCommands: {
attempts: `./scripts/agentrun queue attempts ${taskId}`,
queue: `describe task/${taskId}`,
...(runId ? { run: `describe run/${runId}`, result: `result run/${runId}${commandId ? ` --command ${commandId}` : ""}`, events: `events run/${runId} --after-seq 0 --limit 100 --tail-summary` } : {}),
...(runId && commandId ? { command: `describe command/${commandId} --run ${runId}` } : {}),
...(sessionId ? { logs: `logs session/${sessionId} --tail 100` } : {}),
},
};
}
export function summarizeQueueAttemptListResult(result: JsonValue, taskId: string, limit: number): JsonRecord {
const record = jsonRecordValue(result);
if (!record) throw new AgentRunError("schema-invalid", "queue attempts response must be an object", { httpStatus: 2 });
const items = queueItems(record.items);
return {
action: "queue-attempt-list-summary",
taskId: stringValue(record.taskId) ?? taskId,
count: numberValue(record.count) ?? items.length,
cursor: stringValue(record.cursor),
limit,
items: items.slice(0, limit).map((attempt) => withoutFullRecordBytes(compactRecord(attempt, { keys: ["attemptId", "taskId", "retryIndex", "state", "idempotencyKey", "reason", "payloadHash", "previousAttemptId", "runId", "commandId", "runnerJobId", "sessionId", "sessionPath", "failureKind", "failureMessage", "createdAt", "updatedAt", "activatedAt", "terminalAt"] }))),
fullResponseBytes: jsonByteLength(result),
valuesPrinted: false,
drillDownCommands: {
full: `./scripts/agentrun queue attempts ${taskId} --full`,
next: stringValue(record.cursor) ? `./scripts/agentrun queue attempts ${taskId} --cursor ${String(record.cursor)} --limit ${limit}` : null,
},
};
}
function summarizeRunEvent(event: JsonRecord, summaryChars: number): JsonRecord {
const payload = jsonRecordValue(event.payload) ?? {};
const type = stringValue(event.type) ?? "unknown";
@@ -1604,6 +1661,28 @@ async function dispatchQueueTask(args: ParsedArgs, taskId: string): Promise<Json
return summarizeQueueDispatchResult(result, taskId);
}
async function retryQueueTaskCli(args: ParsedArgs, taskId: string): Promise<JsonValue> {
const idempotencyKey = optionalFlag(args, "idempotency-key");
const reason = optionalFlag(args, "reason");
if (!idempotencyKey) throw new AgentRunError("schema-invalid", "queue retry requires --idempotency-key", { httpStatus: 2 });
if (!reason) throw new AgentRunError("schema-invalid", "queue retry requires --reason", { httpStatus: 2 });
const body: JsonRecord = { idempotencyKey, reason, dryRun: args.flags.get("dry-run") === true };
const result = await client(args).post(`/api/v1/queue/tasks/${encodeURIComponent(taskId)}/retry`, body);
if (wantsExpandedOutput(args)) return result;
return summarizeQueueRetryResult(result, taskId);
}
async function listQueueAttemptsCli(args: ParsedArgs, taskId: string): Promise<JsonValue> {
const params = new URLSearchParams();
const cursor = optionalFlag(args, "cursor");
const limit = integerFlag(args, "limit", 20, { min: 1, max: 100 });
if (cursor) params.set("cursor", cursor);
params.set("limit", String(limit));
const result = await client(args).get(`/api/v1/queue/tasks/${encodeURIComponent(taskId)}/attempts?${params.toString()}`);
if (wantsExpandedOutput(args)) return result;
return summarizeQueueAttemptListResult(result, taskId, limit);
}
async function queueDispatchBody(args: ParsedArgs): Promise<JsonRecord> {
const body = await optionalJsonFile(args);
const copy = (flagName: string, key = flagName.replace(/-([a-z])/gu, (_, letter: string) => letter.toUpperCase())): void => {
@@ -2247,6 +2326,8 @@ function help(args: ParsedArgs, group?: string): JsonRecord {
"queue read <taskId> [--reader-id <reader>] [--dry-run] [--full|--raw]",
"queue cancel <taskId> [--reason <text>] [--dry-run] [--full|--raw]",
"queue dispatch <taskId> [--json-stdin|--json-file <dispatch.json>] [--idempotency-key <key>] [--image <image>] [--namespace <namespace>] [--dry-run] [--full|--raw]",
"queue retry <taskId> --idempotency-key <key> --reason <text> [--dry-run] [--full|--raw]",
"queue attempts <taskId> [--cursor <retry-index>] [--limit <n>] [--full|--raw]",
"queue refresh <taskId> [--dry-run] [--full|--raw]",
"aipod-specs list",
"aipod-specs show <name>",