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

+2 -1
View File
@@ -2529,7 +2529,8 @@ wake from a server-verified human card response references that entry in
`humanResponses`, whose answer is already present in the current request context.
Agent/tool outcomes and generated summaries are not promoted to human direction.
Standing agent chats complete the current response stage. Each pending request
Standing agent chats, identified by both conversation agent and user IDs through
the shared conversation predicate, complete the current response stage. Each pending request
keeps its criterion identity and source, with instructions to address it within
that stage. Authorized chat actions require verified results. Delegation requires
a verified handoff and a reply; the worker's task remains separate. An unavailable
+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;