Fix mission decision log revisions

This commit is contained in:
Dotta committed 2026-04-22 11:05:48 -05:00
1 parent 1b9800907e
commit e551b857ca
6 files changed
+80 -26

No files matched your search

@@ -487,20 +487,33 @@ async function appendDecisionLog(
ctx: PluginContext,
companyId: string,
issueId: string,
existingBody: string,
entry: { marker: string; body: string },
) {
const nextBody = appendDecisionLogEntry(existingBody, entry);
if (nextBody === existingBody.trim()) return nextBody;
await ctx.issues.documents.upsert({
issueId,
companyId,
key: "decision-log",
title: "Decision Log",
body: nextBody,
changeSummary: "Recorded mission decision",
});
return nextBody;
for (let attempt = 0; attempt < 2; attempt += 1) {
const existing = await ctx.issues.documents.get(issueId, "decision-log", companyId);
const existingBody = existing?.body ?? "";
const nextBody = appendDecisionLogEntry(existingBody, entry);
if (nextBody === existingBody.trim()) return nextBody;
try {
await ctx.issues.documents.upsert({
issueId,
companyId,
key: "decision-log",
title: "Decision Log",
body: nextBody,
changeSummary: "Recorded mission decision",
baseRevisionId: existing?.latestRevisionId ?? null,
});
return nextBody;
} catch (error) {
const message = stringifyError(error);
if (attempt === 0 && message.includes("updated by someone else")) continue;
throw error;
}
}
throw new Error("Unable to append mission decision log entry");
}
async function ensureFixIssue(
@@ -590,7 +603,7 @@ export async function advanceMission(
actorRunId: input.actorRunId ?? null,
};
const actorLabel = buildActorLabel(actor);
const { mission, summary, featuresDocument, decisionLogBody } = await buildSummaryInternal(
const { mission, summary, featuresDocument } = await buildSummaryInternal(
ctx,
input.companyId,
input.issueId,
@@ -626,7 +639,6 @@ export async function advanceMission(
ctx,
input.companyId,
input.issueId,
decisionLogBody,
buildMissionEventEntry({
markerKey: `advance:paused:${stopReason}:${summary.latestValidationRound}`,
title: `Advance Paused ${summary.missionIdentifier ?? input.issueId}`,
@@ -655,13 +667,11 @@ export async function advanceMission(
createdFixIssues.push(fixIssue);
}
let decisionLogBodyAfterFixes = decisionLogBody;
if (createdFixIssues.length > 0) {
decisionLogBodyAfterFixes = await appendDecisionLog(
await appendDecisionLog(
ctx,
input.companyId,
input.issueId,
decisionLogBodyAfterFixes,
buildMissionEventEntry({
markerKey: `advance:fixes:${createdFixIssues.map((issue) => issue.id).join(",")}`,
title: `Advance Created Fixes ${summary.missionIdentifier ?? input.issueId}`,
@@ -699,7 +709,6 @@ export async function advanceMission(
ctx,
input.companyId,
input.issueId,
decisionLogBodyAfterFixes,
buildMissionEventEntry({
markerKey: `advance:wakeup:${wokenIssueIds.join(",")}`,
title: `Advance Woke Issues ${summary.missionIdentifier ?? input.issueId}`,
@@ -765,7 +774,7 @@ export async function waiveMissionFinding(
actorRunId: input.actorRunId ?? null,
};
const actorLabel = buildActorLabel(actor);
const { summary, decisionLogBody } = await buildSummaryInternal(ctx, input.companyId, input.issueId);
const { summary } = await buildSummaryInternal(ctx, input.companyId, input.issueId);
const finding = summary.findings.find((candidate) => candidate.id === input.findingId);
if (!finding) {
throw new Error(`Mission finding not found: ${input.findingId}`);
@@ -778,7 +787,7 @@ export async function waiveMissionFinding(
actorLabel,
createdAt,
});
await appendDecisionLog(ctx, input.companyId, input.issueId, decisionLogBody, waiverEntry);
await appendDecisionLog(ctx, input.companyId, input.issueId, waiverEntry);
await ctx.state.set(
{ scopeKind: "issue", scopeId: input.issueId, stateKey: `waiver:${input.findingId}` },
{
@@ -527,6 +527,19 @@ describe("missions plugin worker", () => {
});
}
const originalUpsert = harness.ctx.issues.documents.upsert;
const documentUpdates: Array<{ key: string; baseRevisionId: string | null | undefined }> = [];
harness.ctx.issues.documents.upsert = async (input) => {
const existing = await harness.ctx.issues.documents.get(input.issueId, input.key, input.companyId);
if (existing && input.baseRevisionId !== existing.latestRevisionId) {
throw new Error(
input.baseRevisionId ? "Document was updated by someone else" : "Document update requires baseRevisionId",
);
}
documentUpdates.push({ key: input.key, baseRevisionId: input.baseRevisionId });
return originalUpsert(input);
};
const first = await harness.performAction<{ createdFixIssueIds: string[] }>("advance", {
companyId,
issueId: rootIssueId,
@@ -542,12 +555,35 @@ describe("missions plugin worker", () => {
const generated = await harness.ctx.issues.list({ companyId, limit: 50 });
expect(generated.filter((item) => item.originKind === "plugin:paperclip.missions:fix")).toHaveLength(1);
await expect(harness.performAction("waive", {
const summaryResponse = await plugin.definition.onApiRequest?.({
routeKey: "summary",
method: "GET",
path: `/issues/${rootIssueId}/missions/summary`,
params: { issueId: rootIssueId },
query: {},
body: null,
actor: { actorType: "agent", actorId: workerAgentId, agentId: workerAgentId, runId: "run-test" },
companyId,
issueId: rootIssueId,
findingId: "FINDING-MISSION-001",
rationale: "Accepted temporarily for test coverage.",
})).resolves.toMatchObject({
headers: {},
});
expect(summaryResponse?.body).toMatchObject({
validationSummary: expect.objectContaining({
findings: [expect.objectContaining({ id: "FINDING-MISSION-001", computedStatus: "fix_created" })],
}),
});
const waiverResponse = await plugin.definition.onApiRequest?.({
routeKey: "waive",
method: "POST",
path: `/issues/${rootIssueId}/missions/findings/FINDING-MISSION-001/waive`,
params: { issueId: rootIssueId, findingKey: "FINDING-MISSION-001" },
query: {},
body: { rationale: "Accepted temporarily for test coverage." },
actor: { actorType: "agent", actorId: workerAgentId, agentId: workerAgentId, runId: "run-test" },
companyId,
headers: {},
});
expect(waiverResponse?.body).toMatchObject({
findingId: "FINDING-MISSION-001",
waived: true,
});
@@ -555,5 +591,10 @@ describe("missions plugin worker", () => {
const decisionLog = await harness.ctx.issues.documents.get(rootIssueId, "decision-log", companyId);
expect(decisionLog?.body).toContain("paperclip:mission-finding-waiver:FINDING-MISSION-001");
expect(decisionLog?.body).toContain("Accepted temporarily for test coverage.");
expect(documentUpdates.filter((update) => update.key === "decision-log")).toEqual(
expect.arrayContaining([
expect.objectContaining({ baseRevisionId: expect.any(String) }),
]),
);
});
});
+1
View File
@@ -776,6 +776,7 @@ export interface WorkerToHostMethods {
title?: string;
format?: string;
changeSummary?: string;
baseRevisionId?: string | null;
},
result: IssueDocument,
];
+2 -1
View File
@@ -878,7 +878,7 @@ export interface PluginIssueDocumentsClient {
*
* Requires the `issue.documents.write` capability.
*
* @param input - Document data including issueId, key, body, and optional title/format/changeSummary
* @param input - Document data including issueId, key, body, and optional title/format/changeSummary/baseRevisionId
*/
upsert(input: {
issueId: string;
@@ -888,6 +888,7 @@ export interface PluginIssueDocumentsClient {
title?: string;
format?: string;
changeSummary?: string;
baseRevisionId?: string | null;
}): Promise<IssueDocument>;
/**
@@ -775,6 +775,7 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost
title: input.title,
format: input.format,
changeSummary: input.changeSummary,
baseRevisionId: input.baseRevisionId,
});
},
@@ -1558,6 +1558,7 @@ export function buildHostServices(
title: params.title ?? null,
format: params.format ?? "markdown",
changeSummary: params.changeSummary ?? null,
baseRevisionId: params.baseRevisionId ?? null,
});
await logPluginActivity({
companyId,