diff --git a/doc/execution-semantics.md b/doc/execution-semantics.md index b743e309a61..3dbbcc95637 100644 --- a/doc/execution-semantics.md +++ b/doc/execution-semantics.md @@ -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 diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index 43cd368ea00..7404d8c1026 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -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, diff --git a/server/src/services/native-runtime/completion-contracts.test.ts b/server/src/services/native-runtime/completion-contracts.test.ts index 7f54c74bc6c..bfd77b56a8e 100644 --- a/server/src/services/native-runtime/completion-contracts.test.ts +++ b/server/src/services/native-runtime/completion-contracts.test.ts @@ -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", diff --git a/server/src/services/native-runtime/completion-contracts.ts b/server/src/services/native-runtime/completion-contracts.ts index e26152692bb..72130a45a02 100644 --- a/server/src/services/native-runtime/completion-contracts.ts +++ b/server/src/services/native-runtime/completion-contracts.ts @@ -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;