feat(agent): W4-E2 in-grant auto recovery for decisions with verified outcome

- settings.agent.autoInvestigateMode investigate|recover, investigateMaxAttempts, recoverVerifySec
- new capability download.recheck (execute, replay never): qB force recheck / resume, no source swap/delete
- autoInvestigate moves to agent/investigate.ts: recover mode runs execute with a recovery allowlist
  only when an active grant (published policy if bound) covers the recovery actions; otherwise degrades
  to read-only investigation and leaves suggestion + recheck option on the decision
- runAgent verifies recovery against business facts (download_task/task/source_release) before finalize;
  verified -> decision resolved by system:auto-recover, run summary '已恢复(系统核实)'; otherwise stays
  awaiting_user; per-root-cause attempt cap
- e2e E6: missingFiles -> download_failed -> recheck -> verified; no grant degrades; swap rejected;
  dedupe; attempts exhausted; budget exceeded keeps attention

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-27 22:35:03 +08:00
1 parent 23309042dc
commit 86af723b2a
13 files changed
+1072 -15

No files matched your search

+20 -1
View File
@@ -3,7 +3,7 @@ import { MediaAsset } from "./assets.ts";
import { CAPABILITY_CATALOG, type CapabilitySpec, type KnownCapabilityId } from "./capability.ts";
import { Id, JsonObject, NonNegInt } from "./common.ts";
import { MediaEntity, MediaKind } from "./content.ts";
import { AcquisitionPlan, DownloadTask, Task, TaskStatus } from "./execution.ts";
import { AcquisitionPlan, DownloadStatus, DownloadTask, Task, TaskStatus } from "./execution.ts";
import { ExternalEntityType, ExternalIdentity } from "./identity.ts";
import { ServerMapping } from "./ops.ts";
import { OrganizationAction, OrganizationRecord } from "./organization.ts";
@@ -121,6 +121,22 @@ export const OrganizePlanOutput = z.object({
export const OrganizeExecuteInput = z.object({ planId: Id, payloadHash: z.string().min(1), approvalId: Id.optional() });
export const OrganizeExecuteOutput = Accepted.extend({ record: OrganizationRecord.nullable() });
/** 授权内恢复:强制重新校验(missingFiles / error 之后)或恢复暂停;换源、删除、清理不在此能力内 */
export const DownloadRecheckAction = z.enum(["recheck", "resume"]);
export const DownloadRecheckInput = z.object({
downloadTaskId: Id,
action: DownloadRecheckAction.default("recheck"),
reason: z.string().max(500).optional(),
});
export const DownloadRecheckOutput = z.object({
downloadTaskId: Id,
action: DownloadRecheckAction,
statusBefore: DownloadStatus,
rawStateBefore: z.string().nullable(),
/** 已向下载器发出请求;是否恢复以后续同步到的 download_task 状态为准 */
requested: z.boolean(),
});
export const TaskInspectInput = z.object({ taskId: Id });
export const TaskInspectOutput = z.object({
task: Task,
@@ -178,6 +194,7 @@ export const CAPABILITY_IO = {
"identity.resolve": { input: IdentityResolveInput, output: IdentityResolveOutput },
"download.plan": { input: DownloadPlanInput, output: DownloadPlanOutput },
"download.submit": { input: DownloadSubmitInput, output: DownloadSubmitOutput },
"download.recheck": { input: DownloadRecheckInput, output: DownloadRecheckOutput },
"organize.plan": { input: OrganizePlanInput, output: OrganizePlanOutput },
"organize.execute": { input: OrganizeExecuteInput, output: OrganizeExecuteOutput },
"task.inspect": { input: TaskInspectInput, output: TaskInspectOutput },
@@ -223,6 +240,8 @@ export type OrganizeExecuteInput = z.infer<typeof OrganizeExecuteInput>;
export type OrganizeExecuteOutput = z.infer<typeof OrganizeExecuteOutput>;
export type TaskInspectInput = z.infer<typeof TaskInspectInput>;
export type TaskInspectOutput = z.infer<typeof TaskInspectOutput>;
export type DownloadRecheckInput = z.infer<typeof DownloadRecheckInput>;
export type DownloadRecheckOutput = z.infer<typeof DownloadRecheckOutput>;
export type TaskRetryInput = z.infer<typeof TaskRetryInput>;
export type TaskRetryOutput = z.infer<typeof TaskRetryOutput>;
export type TaskCancelInput = z.infer<typeof TaskCancelInput>;