fix: authorize final answer selection for continued chat work

Share the origin-check predicate with the live completion caller. Prove that a real same-agent dispatch selects its final answer over an earlier bookkeeping comment without treating candidacy as publication authority.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-20 21:13:22 -05:00
1 parent a90304ef8c
commit b9c8677635
4 files changed
+44 -15

No files matched your search

@@ -7,6 +7,7 @@ import {
projectHistoricalHeartbeatRunComment,
findHeartbeatRunCompletionComment,
isExternalChatPresentationContext,
isChatOriginPublicationCandidate,
mergeHeartbeatRunResultJson,
readCompletedAssistantMessageCandidate,
resolveHeartbeatRunResponse,
@@ -61,6 +62,19 @@ describe("scheduled continuation presentation", () => {
});
});
describe("continuation origin verification", () => {
it.each(["native_status_decision", "issue.monitor", "tool_action_review", "issue.comment", "issue.comment.created", "issue.update"])
("checks %s without treating candidacy as publication authority", source => {
expect(isChatOriginPublicationCandidate({ source })).toBe(true);
expect(isExternalChatPresentationContext({ source })).toBe(false);
expect(isExternalChatPresentationContext({ source }, true)).toBe(true);
});
it("keeps internal work internal", () => {
expect(isChatOriginPublicationCandidate({ source: "issue.assigned" })).toBe(false);
expect(isExternalChatPresentationContext({ source: "issue.assigned" }, true)).toBe(false);
});
});
describe("selectHeartbeatRunFinalAgentMessage", () => {
const substantive = {
seq: 80,
@@ -8,7 +8,7 @@ import {
} from "@paperclipai/db";
import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase } from "../__tests__/helpers/embedded-postgres.js";
import { enqueueChatRunMilestones, resolveChatRunPresentationAuthorizationReason } from "./chat-run-publications.js";
import { CHAT_RUN_PRESENTATION_AUTHORIZATION_REASON } from "./heartbeat-run-summary.js";
import { CHAT_RUN_PRESENTATION_AUTHORIZATION_REASON, isChatOriginPublicationCandidate, isExternalChatPresentationContext, resolveHeartbeatRunResponse } from "./heartbeat-run-summary.js";
import { resolveChatOriginPublicationBindings } from "./issues.js";
import { NativeRunCoordinatorStore } from "./native-runtime/native-run-coordinator-store.js";
import { finalizeNativeRun, repairCommittedNativeChatResponse } from "./native-runtime/native-run-finalizer.js";
@@ -196,6 +196,21 @@ if (!support.supported) console.warn(`Monitor publication database unavailable:
expect(await db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, f.source.id))).toHaveLength(1);
});
it("selects the continued chat run's completed answer over an earlier bookkeeping comment", async () => {
const f = await continuedTaskFixture();
const authorization = isChatOriginPublicationCandidate(f.successor.contextSnapshot)
? await resolveChatRunPresentationAuthorizationReason(db, { companyId: f.company.id, issueId: f.issue.id, runId: f.successor.id }) : null;
const context = isExternalChatPresentationContext(f.successor.contextSnapshot, authorization === CHAT_RUN_PRESENTATION_AUTHORIZATION_REASON);
const answer = "Migration QA is complete. The verified report is available at the attached result.";
const resolved = resolveHeartbeatRunResponse({ resultJson: {},
existingComment: { id: "earlier-comment", body: "The checkout is ready; QA is underway." },
finalAgentMessage: { text: answer, channel: "final", sourceEventId: "completed-final" },
preferFinalResponseOverExistingComment: context,
});
expect(resolved.text).toBe(answer);
expect(resolved.decision).toMatchObject({ chosenSource: "final_agent_message", commentAction: "create" });
});
it.each(["effect", "decision", "wake", "dispatch", "requester", "agent"])("rejects automatic continuation when %s authority is no longer valid", async fault => {
const f = await continuedTaskFixture();
if (fault === "effect") await db.update(statusDecisionEffects).set({ targetId: randomUUID() }).where(eq(statusDecisionEffects.companyId, f.company.id));
+12 -7
View File
@@ -192,6 +192,11 @@ function record(value: unknown): Record<string, unknown> {
: {};
}
function requiresChatOriginProof(source: string): boolean {
return source === "tool_action_review" || source === "issue.monitor" || source === "native_status_decision" ||
source.startsWith("issue.comment") || source === "issue.update";
}
export function isExternalChatPresentationContext(
contextSnapshot: unknown,
verifiedToolReviewChatOrigin = false,
@@ -202,13 +207,7 @@ export function isExternalChatPresentationContext(
typeof context.source === "string" ? context.source.trim() : "";
// Approvals and Board comments also resume ordinary internal tasks. Only
// durable source-run or mirrored-comment proof authorizes an external reply.
if (
source === "tool_action_review" ||
source === "issue.monitor" ||
source === "native_status_decision" ||
source.startsWith("issue.comment") ||
source === "issue.update"
)
if (requiresChatOriginProof(source))
return verifiedToolReviewChatOrigin;
return (
source.startsWith("chat:") ||
@@ -217,6 +216,12 @@ export function isExternalChatPresentationContext(
);
}
export function isChatOriginPublicationCandidate(contextSnapshot: unknown): boolean {
const context = record(contextSnapshot);
const source = typeof context.source === "string" ? context.source.trim() : "";
return requiresChatOriginProof(source) || isExternalChatPresentationContext(contextSnapshot);
}
/**
* Read only completed assistant prose that can participate in final-response
* presentation. A canonical `final` is authoritative; `unknown` is retained
@@ -20,7 +20,7 @@ import { HttpError } from "../../errors.js";
import { logger } from "../../middleware/logger.js";
import { normalizeResponsibleUserDenialCode } from "../responsible-user-denial-run-outcomes.js";
import { parseObject } from "../../adapters/utils.js";
import { CHAT_RUN_PRESENTATION_AUTHORIZATION_REASON, isExternalChatPresentationContext, mergeHeartbeatRunResultJson, resolveHeartbeatRunResponse, type RunPresentationDecision } from "../heartbeat-run-summary.js";
import { CHAT_RUN_PRESENTATION_AUTHORIZATION_REASON, isChatOriginPublicationCandidate, isExternalChatPresentationContext, mergeHeartbeatRunResultJson, resolveHeartbeatRunResponse, type RunPresentationDecision } from "../heartbeat-run-summary.js";
import { normalizeMaxTurnStopReason } from "../heartbeat-stop-metadata.js";
import { CHAT_CONTROL_RECOVERY_UNRESOLVED_CODE } from "../chat-control-recovery-stop.js";
import { logActivity } from "../activity-log.js";
@@ -789,12 +789,7 @@ export function createHeartbeatRunCompletion(db: Db, dependencies: HeartbeatRunC
livenessRun.id,
livenessRun.companyId,
));
const externalChatPresentationCandidate =
isExternalChatPresentationContext(livenessRun.contextSnapshot) ||
parseObject(livenessRun.contextSnapshot).source === "issue.monitor" ||
parseObject(livenessRun.contextSnapshot).source === "tool_action_review" ||
String(parseObject(livenessRun.contextSnapshot).source ?? "").startsWith("issue.comment") ||
parseObject(livenessRun.contextSnapshot).source === "issue.update";
const externalChatPresentationCandidate = isChatOriginPublicationCandidate(livenessRun.contextSnapshot);
const externalChatPresentationAuthorization =
issueId && externalChatPresentationCandidate
? await measureRunPhase("response.authorize", () => resolveChatRunPresentationAuthorizationReason(db, {