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

+40 -4
View File
@@ -4,9 +4,19 @@ import { defineTask, type OutboxRouteFn, type TaskRegistry } from "@umt/tasks";
import { z } from "zod";
import type { ServerContext } from "../types.ts";
import { retryTask } from "./capabilities.ts";
import {
annotateRunSummary,
applyRecoveryOutcome,
autoInvestigate,
checkRecovery,
isInvestigationRun,
type RecoveryCheck,
type RecoveryOutcome,
recoveryActions,
} from "./investigate.ts";
import { buildGoalReport } from "./report.ts";
import { type AgentEnv, type DriveResult, drive, goalSettled } from "./runner.ts";
import { autoInvestigate, finalize } from "./service.ts";
import { finalize } from "./service.ts";
// 持久任务:runAgent(驱动一次有界 Agent 会话 → 等目标关联的下载/入库落定 → 以业务表汇报)
// 与 resolveDecision(decision.created → 自动调查;decision.updated(resolved)→ 执行所选处理)。
@@ -37,10 +47,31 @@ export function registerAgentTasks(registry: TaskRegistry, env: () => AgentEnv)
let settled: AgentGoalReport | null = null;
const run = await e.ctx.db
.selectFrom("agent_run")
.select(["mode"])
.select(["mode", "dedupe_key"])
.where("id", "=", runId)
.executeTakeFirstOrThrow();
if (res.status === "completed" && run.mode === "execute") {
// 后台授权内恢复:核实业务事实(下载 / 任务 / 身份状态)后再收尾;不等"目标入库"
const recovering = run.mode === "execute" && isInvestigationRun(run);
let recovery: RecoveryOutcome | null = null;
if (recovering) {
const acts = await t.step.run("recover-actions", () => recoveryActions(e.ctx, runId));
let check: RecoveryCheck | null = null;
if (res.status === "completed" && acts.actions.some((a) => a.status === "succeeded")) {
const s = await e.ctx.settings.get("agent");
const pollSec = Math.min(e.opts.goalPollSec ?? s.goalPollSec, 30);
check = await t.step.waitForFact<RecoveryCheck>("recover-verified", {
check: async () => {
const c = await checkRecovery(e.ctx, acts.actions);
return c.state === "pending" ? null : c;
},
timeout: `${pollSec}s`,
maxRounds: Math.max(1, Math.min(900, Math.ceil(s.recoverVerifySec / pollSec))),
});
// 超时:用最后一次观测作为证据(state 仍是 pending → unverified)
check ??= await t.step.run("recover-last-check", () => checkRecovery(e.ctx, acts.actions));
}
recovery = await t.step.run("recover-outcome", () => applyRecoveryOutcome(e.ctx, runId, res, acts, check));
} else if (res.status === "completed" && run.mode === "execute") {
const s = await e.ctx.settings.get("agent");
const pollSec = e.opts.goalPollSec ?? s.goalPollSec;
const rounds = Math.max(1, Math.min(900, Math.ceil((s.goalWaitHours * 3600) / pollSec)));
@@ -59,7 +90,12 @@ export function registerAgentTasks(registry: TaskRegistry, env: () => AgentEnv)
maxRounds: rounds,
});
}
return t.step.run("finalize", () => finalize(e, runId, res, settled));
const out = await t.step.run("finalize", () => finalize(e, runId, res, settled));
if (recovery) {
const rec = recovery;
return t.step.run("recover-summary", () => annotateRunSummary(e.ctx, runId, rec));
}
return out;
},
}),
defineTask({