Q109: a declined deliverable reopens its task and tells the next session why

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-08 17:54:28 -04:00
1 parent 34c5557a32
commit e90b15a7a2
4 files changed
+59 -7

No files matched your search

+13 -2
View File
@@ -1,10 +1,11 @@
import type { DatabaseSync } from "node:sqlite";
import type { CreateApprovalInput, DecideApprovalInput } from "../../shared/commands";
import { APPROVAL_STATE, ROLE_KEY } from "../../shared/domain";
import { APPROVAL_STATE, ROLE_KEY, TASK_STATUS } from "../../shared/domain";
import { createId } from "../../shared/ids";
import { AuditService } from "./audit-service";
import { settleHire } from "./agent-admission";
import { settleHeldDeliverable } from "./held-deliverables";
import { OrganizationService } from "./organization-service";
import { IdentityAccessService } from "./identity-access-service";
import { OrgGovernanceService } from "./org-governance-service";
@@ -104,10 +105,20 @@ export class ApprovalService {
});
// Last, so nothing after it can fail once a file is on disk.
if (approval.source_entity_type === "held_deliverable" && approval.source_entity_id) {
settleHeldDeliverable(this.db, approval.source_entity_id, input.state === APPROVAL_STATE.APPROVED);
const approved = input.state === APPROVAL_STATE.APPROVED;
const taskId = settleHeldDeliverable(this.db, approval.source_entity_id, approved);
if (!approved && taskId) this.reopenForRework(input.orgId, taskId);
}
}
/** Declined work goes back to its task's assignee: the task returns to in progress, wherever it had got to. */
private reopenForRework(orgId: string, taskId: string): void {
const task = this.db.prepare("select status from tasks where id = ? and org_id = ?").get(taskId, orgId) as { status: string } | undefined;
if (!task || task.status === TASK_STATUS.IN_PROGRESS) return;
new OrganizationService(this.db).updateTaskStatus(orgId, taskId, TASK_STATUS.IN_PROGRESS);
console.info(`[deliverables] task ${taskId} reopened for rework after a declined deliverable`);
}
private assertRequesterBelongsToOrg(input: CreateApprovalInput): void {
if (input.requestedByType === "employee") {
if (!input.requestedById) throw new Error("Requesting employee is required.");
+7 -5
View File
@@ -41,9 +41,10 @@ export function holdDeliverable(
db: DatabaseSync,
input: { id: string; approvalId: string; orgId: string; agentId: string; taskId: string | null; goalId: string | null; projectId: string | null; target: HeldTarget; content: string },
): void {
const target = input.target;
// A file path that could never be written is refused now, not at approval.
if (target.kind === "file") resolveWorkspacePath(target.workspaceRoot, target.relativePath);
// A file path that could never be written is refused now, not at approval; one spelling of each path is kept.
const target = input.target.kind === "file"
? { ...input.target, relativePath: resolveWorkspacePath(input.target.workspaceRoot, input.target.relativePath).relativePosix }
: input.target;
db.prepare(`
insert into held_deliverables(
id, org_id, approval_id, kind, agent_id, task_id, goal_id, project_id, workspace_root, relative_path,
@@ -64,14 +65,15 @@ export function holdDeliverable(
console.info(`[deliverables] held ${target.kind} ${input.id} for review ${input.approvalId}`);
}
/** Writes an approved deliverable where it was meant to go; a declined one is never written. */
export function settleHeldDeliverable(db: DatabaseSync, heldId: string, approved: boolean): void {
/** Writes an approved deliverable where it was meant to go; a declined one is never written. Returns the task it belongs to. */
export function settleHeldDeliverable(db: DatabaseSync, heldId: string, approved: boolean): string | null {
const row = db.prepare("select * from held_deliverables where id = ? and state = 'held'").get(heldId) as HeldRow | undefined;
if (!row) throw new Error("The held deliverable of this review was not found.");
db.prepare("update held_deliverables set state = ? where id = ?").run(approved ? "published" : "declined", heldId);
// Publication is the last write, so a file reaches disk only once every database change before it has succeeded.
if (approved) publish(db, row);
console.info(`[deliverables] ${row.kind} ${heldId} ${approved ? "published" : "declined"}`);
return row.task_id;
}
function publish(db: DatabaseSync, row: HeldRow): void {
+14
View File
@@ -9,6 +9,9 @@ const READ_ONLY_TOOLS = new Set([
]);
const JOURNAL_LINES = 40;
const FIELD_CHARS = 160;
const DECLINED_LINES = 5;
/** What a held deliverable would become, over a `held_deliverables` row alias: one file, one existing document, or a new document by type and title. */
const heldTarget = (row: string) => `(case ${row}.kind when 'file' then ${row}.workspace_root || '/' || ${row}.relative_path when 'document_update' then ${row}.document_id else ${row}.doc_type || ':' || ${row}.title end)`;
/** Records a call that tried to change something, with its result, so later sessions build on it instead of repeating it. */
export function recordRunAction(db: DatabaseSync, input: { orgId: string; runId: string; taskId: string; call: ToolCall; output: string }): void {
@@ -35,6 +38,17 @@ export function runJournalLines(db: DatabaseSync, taskId: string): string[] {
lines.push("", "Earlier sessions on this task already made these calls; each shows its result. Do not redo the ones that succeeded; retry or fix only what failed:");
for (const action of actions) lines.push(`- ${action.tool_name}(${action.arguments_excerpt}) -> ${action.result_excerpt}`);
}
const declined = db.prepare(`
select coalesce(h.title, h.relative_path) as name, a.decision_notes as notes
from held_deliverables h join approvals a on a.id = h.approval_id
where h.task_id = ? and h.state = 'declined'
and not exists (
select 1 from held_deliverables later
where later.task_id = h.task_id and later.rowid > h.rowid and later.kind = h.kind and ${heldTarget("later")} = ${heldTarget("h")}
)
order by h.rowid desc limit ?
`).all(taskId, DECLINED_LINES) as Array<{ name: string; notes: string | null }>;
for (const row of declined) lines.push("", `The board declined ${row.name}${row.notes ? `: ${row.notes.trim().replace(/[.。!!]+$/, "")}` : ""}. Redo it before closing the task.`);
if (lastFailure?.error_message) {
lines.push("", `The last session on this task failed with: ${lastFailure.error_message}. Pick up from there instead of starting over.`);
}
+25
View File
@@ -8,6 +8,7 @@ import { OrganizationService } from "../../src/main/services/organization-servic
import { ChatService } from "../../src/main/services/chat-service";
import { dispatchAgentTool, type AgentToolContext } from "../../src/main/services/agent-tools";
import { ApprovalService } from "../../src/main/services/approval-service";
import { runJournalLines } from "../../src/main/services/run-journal";
import { executionApprovalProblem, requireAssignedAgent } from "../../src/main/services/task-run-preparation";
import { TASK_PRIORITY } from "../../src/shared/domain";
import type { TaskExecutionTaskRow } from "../../src/main/services/task-prompts";
@@ -114,6 +115,30 @@ describe("execution modes", () => {
finish();
});
test("a declined deliverable sends its task back to work, with the board's reason in the next session", () => {
const h = harness("ai_cofounder");
const db = h.connection.db;
const employeeId = (db.prepare("select id from employees where org_id = ?").get(h.orgId) as { id: string }).id;
const taskId = h.connection.transaction(() => h.deps.organization.createTask({
orgId: h.orgId, title: "Write the plan", description: "d", status: "in_progress", assigneeAgentId: h.ceoId, risk: "low",
projectId: null, goalId: null, workspaceId: null, priority: TASK_PRIORITY.NORMAL, requiresFilesystem: false,
}));
dispatchAgentTool(h.deps, { ...h.ctx, taskId }, call("write_deliverable", { path: "plan.md", content: "draft" }));
// The operator closed the task while the draft waited; declining it still reopens the work.
h.connection.transaction(() => h.deps.organization.updateTaskStatus(h.orgId, taskId, "done"));
const review = (db.prepare("select id from approvals where org_id = ? and kind = 'deliverable_review'").get(h.orgId) as { id: string }).id;
h.connection.transaction(() => new ApprovalService(db).decide({ orgId: h.orgId, approvalId: review, state: "declined", decidedByEmployeeId: employeeId, notes: "Cite the market data." }));
expect(db.prepare("select status from tasks where id = ?").get(taskId)).toEqual({ status: "in_progress" });
expect(runJournalLines(db, taskId).join("\n")).toContain("The board declined plan.md: Cite the market data. Redo it before closing the task.");
// Another deliverable under the same name elsewhere does not answer the decline; a resubmission of the same one does.
const elsewhere = join(h.ctx.workspace!.localPath, "other");
mkdirSync(elsewhere);
dispatchAgentTool(h.deps, { ...h.ctx, taskId, workspace: { id: "wsp_other", localPath: elsewhere, writeAccess: true } }, call("write_deliverable", { path: "plan.md", content: "x" }));
expect(runJournalLines(db, taskId).join("\n")).toContain("The board declined plan.md");
dispatchAgentTool(h.deps, { ...h.ctx, taskId }, call("write_deliverable", { path: "./plan.md", content: "with data" }));
expect(runJournalLines(db, taskId).join("\n")).not.toContain("The board declined plan.md");
});
test("an approved file whose registration fails leaves the workspace as it was", () => {
const h = harness("ai_cofounder");
const db = h.connection.db;