fix: use canonical conversation identity for native contracts

This commit is contained in:
Dotta committed 2026-10-31 17:37:49 -05:00
1 parent 0d8ac2855e
commit cdfd701d89
4 files changed
+24 -4

No files matched your search

+1
View File
@@ -2576,6 +2576,7 @@ export function heartbeatService(
priority: issueContext.priority,
workMode: issueContext.workMode,
conversationAgentId: issueContext.conversationAgentId,
conversationUserId: issueContext.conversationUserId,
reviewPolicy: issueContext.reviewPolicy,
description: issueContext.description,
projectId: issueContext.projectId,
@@ -12,7 +12,7 @@ import {
describe("buildNativeCompletionContract", () => {
it("scopes standing-chat criteria to the response while preserving every current request and source", () => {
const issue = { id: "chat", title: "Chat with CEO", description: null, conversationAgentId: "ceo" };
const issue = { id: "chat", title: "Chat with CEO", description: null, conversationAgentId: "ceo", conversationUserId: "operator" };
const comments = [
{ id: "operator-request", body: "Restart this instance." },
{ id: "handoff-request", body: "Have Morgan draft the update and send it here when finished." },
@@ -43,6 +43,22 @@ describe("buildNativeCompletionContract", () => {
.toEqual([{ id: "objective", requirement: request }]);
});
it.each([undefined, null])("keeps an agent-only issue in task and plan scope when its conversation user is %s", conversationUserId => {
const request = "Write the launch plan and wait for approval.";
const contract = buildNativeCompletionContract({ title: "Chat with CEO", description: null,
conversationAgentId: "ceo", conversationUserId }, { immediateRequest: request });
expect(contract.criteria).toEqual([{ id: "objective", requirement: request }]);
expect(contract.objective).toContain("propose or save the requested plan and wait before executing");
});
it.each([" Help plan the launch. ", null])("scopes a conversation's first turn without a follow-up (%s)", description => {
const contract = buildNativeCompletionContract({ title: "Chat with CEO", description,
conversationAgentId: "ceo", conversationUserId: "operator" });
expect(contract.objective).toMatch(/current conversation response/i);
expect(contract.criteria).toEqual([{ id: "objective", requirement:
`Address this request in the current authorized conversation response: ${description?.trim() || "Respond to the current conversation context."}` }]);
});
it("uses the task description as the single initial criterion", () => {
expect(buildNativeCompletionContract({
title: "Ship the runner",
@@ -6,6 +6,7 @@ import { completionContracts } from "@paperclipai/db";
import type { NativeCompletionSource, StrictCompletionContractInput } from "../../vendor/paperclip-runner/index.js";
import { nativeSha256 } from "./canonical.js";
import { isConversation, type ConversationIdentity } from "../agent-conversations.js";
export const NATIVE_COMPLETION_CONTRACT_SCHEMA = "paperclip.completion-contract.v1";
export const NATIVE_COMPLETION_POLICY_VERSION = "phase6-v4";
@@ -87,7 +88,7 @@ export function resolveNativeCompletionPolicy(issue: {
}
export function buildNativeCompletionContract(
issue: { title: string; description: string | null; conversationAgentId?: string | null },
issue: { title: string; description: string | null } & ConversationIdentity,
options: {
readonly revision?: number;
readonly immediateRequest?: string | null;
@@ -104,7 +105,7 @@ export function buildNativeCompletionContract(
.filter((request) => request.length > 0);
const humanResponseId = options.humanResponseId?.trim();
const hasFollowUp = immediateRequests.length > 0 || Boolean(humanResponseId);
const conversation = Boolean(issue.conversationAgentId);
const conversation = isConversation(issue);
const requirement = (request: string) => conversation
? `Address this request in the current authorized conversation response: ${request}`
: request;
@@ -165,6 +166,7 @@ export async function ensureNativeCompletionContract(input: {
description: string | null;
reviewPolicy?: string | null;
conversationAgentId?: string | null;
conversationUserId?: string | null;
};
actorId: string;
immediateRequest?: string | null;