mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-15 10:21:41 +01:00
fix(chat): route company creation, decline retries and question cancellation correctly (#21035)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Chat agents follow the chat directive and discover API routes with search_api and call_api > - Full chat cohort 4 (OpenCode with DeepSeek v4.1 flash) showed four guidance gaps > - The missing-name rule overrode the board-only company-creation boundary; a repeated request after a decline did not post a new approval card; search_api showed the board-only question cancel route as open to agents (and withdraw as board-only); write calls carried `responseText` beside or instead of `body` > - This pull request fixes the guidance, the route authorization metadata and the pre-send refusal > - The benefit is that agents take the supported path the first time instead of asking for the wrong input, refusing a valid retry, or calling a route that returns 403 ## Linked Issues or Issue Description **What happened?** - onboarding.new-company: asked to "set up a new company for my website business", the CEO cited the missing-name rule (#21007) and asked for a business name with a question card. Creating a company is a board action, so the agent should send the person to the creation flow. - app-permissions.ask-before-action: the person declined a write card and then repeated the same request. The CEO asked the person to tell it to resubmit instead of posting a new approval card. The gateway already allows a fresh request after a new human message (`isNewHumanIntentAfterToolRejection`), but the decline text said "Do not retry the same call". - questions-and-approvals.multi-part-question: search_api listed `POST /api/issues/{id}/interactions/{interactionId}/cancel` as `board_or_agent`. The route rejects agents. The agent route, `/withdraw`, was listed as board-only although its route admits the card's creator agent and the task's assignee. - Several cases: write calls through call_api carried `responseText` with no body (for example "Received arguments: operationId, pathParams, responseText"). **Expected behavior** The agent links the company-creation flow; it posts a fresh approval card for a repeated request; search_api shows the true authorization of cancel and withdraw; a write with `responseText` is refused before it is sent, with the same guidance #21002 gave `saveResponse`. **Steps to reproduce** Run the four chat cases above with OpenCode and DeepSeek v4.1 flash on master. Refs #21002, #21007 ## What Changed - `AGENT_CHAT_DIRECTIVE`: the missing-name rule applies to work the agent creates itself and never comes before a board-only boundary. For a new company the agent links [Create organization](/onboarding) (no company prefix) and repeats the business details. A repeated request after a declined approval card is an explicit retry: make the same call so a fresh card is posted. A clarification question card puts candidate links in its description, because labels show as plain text. - Decline instructions in `tool-action-delivery.ts` and `tool-gateway.ts` say that a later request from the person for the same action is an explicit retry. - OpenAPI: `POST /api/issues/{id}/interactions/{interactionId}/cancel` is board-only and its summary names the `/withdraw` route for agents. `/withdraw` is no longer marked board-only, which matches its route. - `call_api`: a new write that carries `responseText` is refused before it is sent; the message says `responseText` never carries request fields and names the allowed body fields. Older saved calls still validate for recovery. `doc/runner-api-tools.md` is updated. ## Verification - `server/src/__tests__/agent-conversations.test.ts`: new directive tests for the board-only boundary order, the decline retry and card links. - `server/src/__tests__/openapi-routes.test.ts`: cancel is `board`, withdraw is `board_or_agent`, the cancel summary names withdraw. - `server/src/services/native-runtime/runner-api.test.ts`: a write with `responseText` is refused before sending (three window shapes), the same write without it is sent once, and saved calls still validate. - `tool-gateway.test.ts`, `tool-gateway-service.test.ts`, `issue-thread-interaction-routes.test.ts`, `tool-action-replay-intent.test.ts`: passed. `server` `tsc --noEmit`: passed. - Paid run (8 cells, OpenCode + DeepSeek v4.1 flash) together with the eval PR: see the PR comment for results. ## Risks - A model that used to send a write with `responseText` and a valid body now gets one refusal and must resend without it. Nothing is sent before the refusal, so there is no duplicate side effect. - The directive grows by a few sentences, which adds a small number of prompt tokens per chat turn. - Agents that read `/withdraw` as board-only before now see that they can withdraw their own card; the route already enforced who may. ## Model Used - Anthropic Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with tool use, shell execution and code editing. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [ ] I will address all Greptile and reviewer comments before requesting merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
aa94939988
commit
0dfbbdeb31
9 files changed
+67
-12
No files matched your search
@@ -1001,6 +1001,27 @@ describe("chat prompt policy", () => {
|
||||
expect(prompt).toContain("/<prefix>/agents/<urlKey or id>");
|
||||
});
|
||||
|
||||
// Full chat cohort 4 (onboarding.new-company): the CEO applied the missing-name rule and asked for a business name
|
||||
// instead of sending the person to the board-only company-creation flow.
|
||||
it("keeps the missing-name rule behind the board-only company-creation boundary", () => {
|
||||
expect(AGENT_CHAT_DIRECTIVE).toContain("This name rule is for work you create yourself. It never comes before a board-only boundary");
|
||||
expect(AGENT_CHAT_DIRECTIVE).toContain("Otherwise link [Create organization](/onboarding), which opens the new-organization wizard");
|
||||
// Paperclip Cloud creates organizations in the Cloud portfolio; a bare /onboarding there opens the existing company's agent step.
|
||||
expect(AGENT_CHAT_DIRECTIVE).toContain("when its response has a cloud field, this is a Paperclip Cloud stack, organizations are created in the person's Paperclip Cloud portfolio rather than in this app, so say that and do not link /onboarding");
|
||||
expect(AGENT_CHAT_DIRECTIVE).toContain("/onboarding takes no company prefix; /<prefix>/onboarding only adds an agent to this company");
|
||||
const nameRule = AGENT_CHAT_DIRECTIVE.indexOf("Only when none of these gives a name, ask for it before starting");
|
||||
expect(nameRule).toBeGreaterThan(-1);
|
||||
expect(AGENT_CHAT_DIRECTIVE.indexOf("It never comes before a board-only boundary")).toBeGreaterThan(nameRule);
|
||||
});
|
||||
// Full chat cohort 4 (app-permissions.ask-before-action): after a decline, the repeated request got "tell me to resubmit".
|
||||
it("treats a repeated request after a declined approval card as an explicit retry that posts a fresh card", () => {
|
||||
expect(AGENT_CHAT_DIRECTIVE).toContain("the new message is an explicit retry, not an ambiguous request: make the same call again so Paperclip posts a fresh approval card");
|
||||
expect(AGENT_CHAT_DIRECTIVE).toContain("Do not ask them to confirm the retry, tell them to resubmit, or treat the earlier decline as final.");
|
||||
});
|
||||
// Full chat cohort 4 (tasks.ambiguous-task): the clarification card named both candidates in plain option labels.
|
||||
it("puts candidate links in a clarification card's description, which renders links", () => {
|
||||
expect(AGENT_CHAT_DIRECTIVE).toContain("If you ask with a question card instead, put the candidate links in the card's description");
|
||||
});
|
||||
it("tells chat that instruction saves keep no history, so an undo with no listed revision asks instead of reconstructing", () => {
|
||||
expect(AGENT_CHAT_DIRECTIVE).toContain("Saving instructions does not record earlier versions");
|
||||
expect(AGENT_CHAT_DIRECTIVE).toContain("an empty history means no earlier text is retained");
|
||||
|
||||
@@ -410,6 +410,16 @@ describe("openapi routes", () => {
|
||||
for (const code of ["200", "401", "403"]) expect(operation.responses[code]).toBeDefined();
|
||||
});
|
||||
|
||||
// Full chat cohort 4: search_api listed the cancel route as open to agents, so an agent tried it and got 403.
|
||||
it("documents question cancellation as board-only and points agents to withdraw", () => {
|
||||
const spec = buildOpenApiSpec() as any;
|
||||
const cancel = spec.paths["/api/issues/{id}/interactions/{interactionId}/cancel"].post;
|
||||
expect(cancel["x-paperclip-authorization"]).toEqual({ actor: "board" });
|
||||
expect(cancel.security).toEqual([{ BoardSessionAuth: [] }, { BoardApiKeyAuth: [] }]);
|
||||
expect(cancel.summary).toContain("POST /api/issues/{id}/interactions/{interactionId}/withdraw");
|
||||
expect(spec.paths["/api/issues/{id}/interactions/{interactionId}/withdraw"].post["x-paperclip-authorization"]).toEqual({ actor: "board_or_agent" });
|
||||
});
|
||||
|
||||
it("documents the owned pending-question read as canonical attention items without client scope overrides", () => {
|
||||
const spec = buildOpenApiSpec() as any;
|
||||
const operation = spec.paths["/api/issues/{id}/pending-questions"].get;
|
||||
|
||||
Reference in new issue
Block a user