mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-10 12:37:55 +01:00
c5da0b88ad4217abbf46ae2df0583de8df2dcc9f
7138
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c5da0b88ad |
fix(native): take over a remote runner at once when its old controller is proven gone (#20240)
## Thinking Path - Restart recovery should be fast and fault-proof; "restarts are product failures". Remote (Daytona) runs must recover as quickly as local ones once it is safe. - Phase 3's Daytona K13 storm (`w-20261109T100435-s9503`) showed Codex `restart_recovery_control_plane` p50 34.9 s, p95 61.1 s. That's Paperclip's own share, before any provider load. - The run logs show why. After a hard restart, each slow run sat in "Native restart recovery is waiting for safe ownership evidence" with reason `remote_controller_lease_live` for about 51 s, until the dead controller's 60 s remote-controller lease expired. - On the remote path, `evaluateNativeControllerTakeover` deliberately skips every process proof and waits for the lease. That's correct when the previous controller might live in another container or on another host. When it recorded this exact process space, the shared prover (pid, process start, boot id) can prove it gone, which is the same proof the local path already trusts. ## Linked Issues or Issue Description **What happened?** After a hard control-plane restart, a remote (Daytona) native run could not be recovered until the dead controller's 60 s remote lease expired. Recovery took about 35–60 s, even though the old controller process was provably gone. **Steps to reproduce** 1. Run a Codex task in a Daytona environment. 2. Hard-kill the control plane (SIGKILL) while the run is active, then restart it. 3. The run logs `native.recovery.transition` "waiting for safe ownership evidence" (`remote_controller_lease_live`) and resumes only when the lease expires. **Expected behavior** When the previous controller ran in this same process space and is proven gone, recovery takes over the remote runner immediately. It still waits for the lease whenever the previous controller cannot be proven gone. ## What Changed - `server/src/services/native-runtime/native-restart-recovery.ts`, `evaluateNativeControllerTakeover` with `remoteLeaseFenced`: - An expired or released lease is allowed, as before. - A live lease whose holder recorded this exact process space (`controllerHostHash`, neither missing nor unknown) and a controller PID goes through the shared owner-gone prover, with `requireStoppedController`. A positive proof allows the takeover, with reason `remote_lease_<proof reason>`, for example `remote_lease_controller_process_dead`. - The one-release null-host fallback is never accepted for a remote runner. - A holder in another process space is never probed (the existing "must not probe another container's PID" test still holds). It waits for the lease, as before. - `native-restart-recovery.test.ts`: a new test, red on master, covers: - takeover when this-process-space controller is dead; - waiting while it is alive; - waiting without a probe when the process space differs. ## Verification - The 7 server test files that cover the takeover decision pass (351 tests): `native-restart-recovery`, `native-controller-host-recovery`, `native-remote-retry-admission`, `native-recovered-owner-followup`, `native-runner-restart-diskless.integration`, `heartbeat/recovery` and `controller-instance-lease`. - Evidence: kept-instance DB copy from the Daytona K13 wave. In run `ba194ae7`, the `awaiting_evidence` transition `remote_controller_lease_live` lasted from 04:26:06.8 to 04:26:57.9, then `bootstrap_incomplete` restart. - A Daytona K13 rerun will measure the before and after. ## Risks - **Takeover soundness** rests on the shared prover, which the local path already uses for the same controllers: same process space, PID dead or definitely restarted, and boot-id proof where needed. If the old controller is alive, the decision still waits for the lease. - **Multi-host or containerized controllers** record a different process space, so they are unaffected and still fenced by the lease. - **The remote runner** keeps its own reconnect grace (7 min). The new controller reattaches through the existing durable-authority path; only the wait before that changes. ## Model Used Claude Opus 5.5 (`claude-opus-5-5`, Anthropic), 1M context window, tool use and code execution, via Claude Code. ## 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 - [x] 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> |
||
|
|
bd83715a6e |
fix(recovery): bootstrap a remote follow-up again when a restart cut it off after retiring its finished owner (#20241)
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - A restart must resume in-progress runs on its own. Restart recovery
classifies each claimed native run and the executor acts on that claim.
> - A C1 follow-up on a remote sandbox first retires the finished owner
of the previous turn (it archives the previous controller state), then
launches a new runner.
> - A restart in that window left the run with an inherited session
checkpoint but no runner for this run. Recovery read the inherited
checkpoint as proof of a live remote runner and claimed reattach. The
executor then found no runner identity and failed with
`durable_identity_unreadable`, so the user had to press Retry.
> - This pull request makes recovery recognize that nothing reached a
runner for this run, and makes the executor bootstrap again over the
empty root.
> - The benefit is that this restart window resumes the follow-up by
itself.
## Linked Issues or Issue Description
**What happened?**
In restart-chaos Daytona K13 storm wave seed 9503, task t03 (codex, C1)
graded R3 retry_needed. Follow-up run 0d8b7258 retired its previous
owner at 10:07:32.988 ("Run 4ee1c19a finished without a saved runner
checkpoint; starting a new runner"). A K1 kill at 10:07:34 cut it off
before any runner start was requested. On restart, recovery claimed
`reattach_remote_runner` and the executor failed with
`runner_state_identity_mismatch: durable_identity_unreadable`. The run
ended with `native_continuation_requires_reconciliation` and a Retry
offer.
**Expected behavior**
Recovery sees that no runner was ever requested for this run, claims
`bootstrap_incomplete`, and the executor starts the new runner the first
attempt was about to start.
**Steps to reproduce**
1. Run a remote (Daytona) C1 follow-up whose previous turn finished.
2. Kill the server after the follow-up retires the finished owner and
before it records `native.process_start_requested`.
3. Restart the server. See the `reattach_remote_runner` claim and the
`durable_identity_unreadable` failure.
**Paperclip version or commit**
master at
|
||
|
|
465da4227a |
fix(runner): drop a tool target path that breaks a line so the controller can acknowledge its event (#20238)
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - The Paperclip Runner turns provider tool calls into durable PRP
events. The controller acknowledges each event only after it validates
and commits it.
> - The controller's event schema forbids line terminators in a safe
path such as `tool.execution` `target`.
> - The ACPX sidecar and the Rust runner accepted a location path that
contained a newline. Claude sometimes emits a malformed Write call that
puts the file content into the path.
> - The controller refused that event without an acknowledgement. The
runner redelivered the same bytes, and the run stopped making progress
for 14 minutes (2,848 reconnects).
> - This pull request makes both producers drop such a path, so the
event carries a null target and the controller accepts it.
> - The benefit is that a malformed provider path can no longer wedge a
run's event delivery.
## Linked Issues or Issue Description
**What happened?**
In restart-chaos wave seed 9807 (claude, R1, local), the run stopped
receiving events after runner source seq 33. The runner reconnected
2,848 times in 14 minutes. Offline replay of the saved runner outbox
through the controller's `validatePrpEvent` showed that seq 34 fails
`/payload/target` (the safePath pattern), because the target is
`packages/shared/src/chaos/slug-2c5c3c8f.ts\n<parameter
name="content">export function chaosSlug…`. Seq 35, 39 and 40 (the same
tool call) fail the same way.
**Expected behavior**
The runner never emits an event that the controller's schema refuses. A
malformed tool path becomes a null target, and the tool event still
arrives.
**Steps to reproduce**
1. Have an ACPX provider send a tool call whose location path contains a
newline (for example, a Write call with the content in the path).
2. See that the runner emits `tool.execution.progressed` with that
target.
3. See that the controller closes the connection without an
acknowledgement on every redelivery.
**Paperclip version or commit**
master at
|
||
|
|
3963d38560 |
test(chaos): drop the R1 clone's remote so agents cannot push to the operator's checkout (#20228)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - The restart chaos harness runs real agents against a local Paperclip and grades how they survive restarts > - The R1 workload clones `--workspace-repo` (usually the operator's own Paperclip checkout) and has agents change code in that clone > - The clone kept `origin` pointing at the operator's checkout over `file://` > - An agent in an R1 workspace could therefore `git push` branches or commits into the operator's real repository > - This pull request removes the clone's remote right after the pinned checkout > - The benefit is that agent code in an R1 wave can no longer write to the operator's checkout ## Linked Issues or Issue Description Refs #20111 (review thread on the R1 workload) **What happened?** A review of #20111 noted that the cloned R1 workspace still has `origin = file://<operator checkout>`. Project worktrees created from that clone inherit the remote, so an agent can push into the operator's repository. **Steps to reproduce** 1. Run a wave with `--workloads R1 --workspace-repo <checkout>`. 2. In a task worktree, run `git remote get-url origin`. It prints `file://<checkout>`. **Expected behavior** The workspace has no path back to the operator's checkout. **Actual behavior** `git push origin HEAD:refs/heads/x` from the workspace would create a branch in the operator's checkout. ## What Changed - `cloneWorkspaceRepository` runs `git remote remove origin` after the clone and any pinned-commit fetch. - The clone test asserts that both the plain and the pinned clone have no remote, that `git push origin` fails, and that no branch appears in the source. ## Verification - `npx vitest run --config tests/restart-chaos/vitest.config.ts tests/restart-chaos/seed.test.ts`: the new assertions fail before the change (`expected 'origin' to be ''`) and pass after it (8/8). - `node scripts/typecheck-test-projects.mjs tests/restart-chaos`: clean. - Audit of the three R1 waves that already ran (w-20261108T215822-s9805, w-20261109T030542-s9805, w-20261109T081456-s9901): no reflog entry of type push on any local ref in the operator checkout during the wave windows, no `CHA-*` or R1 branch there, unchanged hooks, and no push in the kept clones' logs. Nothing was pushed. - A short local R1 wave on this branch confirms workspace provisioning works without the remote (result in a comment below). ## Risks - If Paperclip workspace provisioning needed `origin` in the project repository, R1 tasks would fail to provision. The confirming R1 wave covers this. ## Model Used - Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code execution in Claude Code. ## 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 - [x] 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> |
||
|
|
780f378f60 |
fix(recovery): settle a local workspace command whose server process died mid-command (#20233)
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - A run prepares its execution workspace with local commands
(`workspace_provision`, `worktree_prepare`, teardown, cleanup). The
server records each command as a `workspace_operations` row.
> - A restart can stop the server while such a command runs. The row
then stays `running` forever, because no process or recovery path owns
it.
> - The restart-chaos harness found this. A K2 drain interrupted a run,
then the server started `bash ./scripts/provision-worktree.sh` for the
run and exited before it finished. The retry succeeded, but the old row
stayed `running`.
> - The ownership reconciler already settles finalize attempts whose
process died. Local command operations had no owner stamp and no
ownership kind.
> - This pull request stamps each local command operation with the
process that runs it and adds a `workspace_command_operation` ownership
kind that fails an abandoned one.
> - The benefit is that a restart never leaves a stuck workspace
operation behind.
## Linked Issues or Issue Description
**What happened?**
In restart-chaos wave seed 9807 (local, R1, faults K1/K2), tasks t03 and
t04 graded R6 with `running_workspace_operations`. A K2 drain
interrupted the run with `server_shutdown_interrupted`. A
`workspace_provision` operation started 0.3 s later in the same process
and never finished. The retry run succeeded. The orphan operation stayed
`running` forever.
**Expected behavior**
When the process that ran a local workspace command is gone, recovery
closes the operation. It does not change the run or its outcome.
**Steps to reproduce**
1. Start a run that provisions a worktree with a provision command.
2. Drain or restart the server while the command runs.
3. After the restart, see that the `workspace_operations` row for the
command stays `running`.
**Paperclip version or commit**
master at
|
||
|
|
267f54ef72 |
fix(chat): make task-not-found, unreachable-MCP and board denials final for agents (#20208)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - People talk to their agents in Agent Chat, and the agents act through the Paperclip API > - A board-only denial now tells the agent that it is final and where the person acts (#20151, #20182). Other refusals and errors still give no sign that they are final > - So the agent keeps trying until the reply deadline, and the person gets no answer. In chat evaluations this happened on a task lookup that returned 404, on an MCP server that Paperclip could not reach, on a model question, and after a final budget denial > - This pull request makes those answers say that they are final and what the person must do, and it tightens the chat guidance after a board-only denial > - The benefit is a prompt, correct reply instead of a long search or forbidden workarounds ## Linked Issues or Issue Description Refs #20151, #20182 (same remedy for board-only denials). **What happened** In four Agent Chat evaluation cases on a slower model, the agent did not reply before its deadline: - Asked to open a task from an old link, the agent got `404 {"error":"Issue not found"}`. It quoted the private-task guidance, then searched archived projects, activity and the old host for 3 more minutes. - Asked to connect an MCP server at an unreachable URL, the agent got `502 {"error":"fetch failed","code":"runtime_error"}` from the health check. It retried, then used `curl` on the host and gateway IP addresses to look for the server. - Asked which coding models an agent can use, the agent got the runtime's model list in its first minute. The catalog did not say how to list OpenRouter models or that these lists answer the question, so it enumerated OpenRouter with the shell until the deadline. - Asked to raise a budget, the agent got the final board-only denial. The chat guidance said "Complete other permitted work", so it probed approvals and other routes and replied too late. **Expected behavior** The answer the agent receives says whether it is final and what the person must do, with a link when the page is known. The agent replies at once. It does not search other routes, retry, or contact other network addresses. **Steps to reproduce** 1. Open Agent Chat with a CEO agent. 2. Ask it to open a deleted task by its old link, or to connect an MCP server at a closed local port. 3. The agent calls `GET /api/issues/{id}` (404) or `POST /api/tool-connections/{id}/health-check` (502 "fetch failed"). 4. The body says nothing about finality. The agent keeps searching or probing and replies late or not at all. ## What Changed - `GET /api/issues/:id`: for an agent, the 404 body adds `code: "issue_not_visible"` and a `remediation`. It says that the lookup is final, that the agent must not search other routes or open a similarly named task, and that it should reply now and offer sharing or pasting. A missing task, a private task and another company's task get the same body, so the body reveals nothing. People and other routes keep the plain body. - MCP health check and catalog refresh: when Paperclip cannot reach the server (connection refused, DNS, network, reset, timeout or TLS), an agent's error adds a `remediation`. It says the person must correct the URL or start the server, that the agent must not contact other hosts, ports or network addresses, and it links Connectors (`/<prefix>/apps`). Status, `error` and `code` do not change. - `AGENT_CHAT_DIRECTIVE`: after a board-only denial, the agent replies now instead of making more calls. "Complete other permitted work" is removed. - API catalog: `POST /api/companies/{companyId}/tools/connections` says it saves without a check and points MCP-by-URL setup at `tools/apps/connect`, which checks the server first and saves nothing for a fresh setup that cannot reach it. `GET /api/companies/{companyId}/adapters/{type}/models` says which lists answer a model question (the runtime's list with `provider`, `acpxAgent` for ACPX agents and `environmentId`, and `opencode_local` with `provider=openrouter` for OpenRouter keys), that the OpenRouter list is not a qualification, and that with those inputs these calls answer the question. `openrouter` is added to the documented `provider` values. - API catalog authorization: budget policy upsert, budget incident resolution, company and agent budget updates, and approval approve/reject are now listed as board-only. Their routes already call `assertBoard`, but the catalog said `board_or_agent`, so an agent explored them as its own action before it got the 403. - Tests: new `agent-refusal-finality.test.ts` (including the catalog actor for the budget and approval routes); the private-task access test now checks that a private and a missing task give an agent the same 404 body; the guidance test checks the new wording. ## Verification - `cd server && npx vitest run src/__tests__/agent-refusal-finality.test.ts src/__tests__/board-access-denial.test.ts src/__tests__/issue-access-grants-routes.test.ts src/__tests__/authz-existence-oracle-guard.test.ts`: all pass (embedded Postgres included). - `cd server && npx vitest run src/__tests__/error-handler.test.ts src/__tests__/generic-mcp-connection.test.ts src/__tests__/openapi-routes.test.ts src/__tests__/remote-http-rebinding.test.ts src/__tests__/cloud-portfolio-error.test.ts src/__tests__/database-backup-catalog-guidance.test.ts`: 183 tests pass. - `npx tsc --noEmit -p server`: passes. - Real chat journeys on the chat evaluation branch (runner-opencode): the results will follow in a comment. ## Risks - Low risk. HTTP status, `error` and `code` stay the same for each changed error. Agents get extra top-level fields (`code` on the task 404, `remediation`). A client that compares a whole agent 404 body for `GET /api/issues/:id` exactly will see a change. - The task 404 body is the same for missing, private and other-company tasks, so it adds no existence oracle. Other issue routes keep their plain body. - The guidance change can make an agent stop after a board-only denial in a request that had other, permitted parts. The agent still reports what it finished. ## Model Used - Anthropic Claude Opus 5.5 (`claude-opus-5-5`), with tool use and code execution in Claude Code. ## 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 - [x] 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>canary/v2026.1109.0-canary.10 |
||
|
|
d2fe85e57c |
fix(secrets): delete a secret that a binding proposal referenced (#20193)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Agents can propose a secret binding. A person approves or rejects it on a review card. > - Each binding proposal row refers to one source: a secret or a pending secret proposal. A check constraint enforces this. > - The secret reference used `ON DELETE SET NULL`. When a person deletes the secret, Postgres sets the reference to null. The check constraint then rejects the row. > - So a person cannot delete any secret that a binding proposal ever referenced. The API returns 500. > - This pull request deletes the proposal rows together with the secret, as the other source reference already does. Before the delete, it rejects pending proposals so their cards close with a reason. > - The benefit is that secret deletion works again and no review card points at a removed proposal. ## Linked Issues or Issue Description Refs #15644 (related: block secret deletion while bindings still exist). **What happened** `DELETE /api/secrets/:id` returned 500 for a secret that a binding proposal referenced. The server log showed `new row for relation "company_secret_proposals" violates check constraint "company_secret_proposals_shape_check"`. This happened for pending, rejected and approved proposals. It also happened after the person removed the agent's binding. **Expected behavior** The person can delete the secret. Proposals that referenced it go with it. A pending review card closes as rejected and tells why. **Steps to reproduce** 1. An agent proposes a binding for an existing company secret (`POST /api/agents/me/secret-proposals` with `secretId`). 2. Approve or reject the proposal, or leave it pending. 3. Delete the secret with `DELETE /api/secrets/:id`. 4. The response is 500 and the secret stays. ## What Changed - `packages/db`: `company_secret_proposals.secret_id` now uses `ON DELETE CASCADE`, the same as `secret_proposal_id`. Migration `0452_secret_proposal_secret_cascade.sql` replaces the foreign key (`DROP CONSTRAINT IF EXISTS`, then `ADD CONSTRAINT`). - `server/src/services/secrets.ts`: `remove` accepts an optional `beforeDelete` hook. The hook runs after the secret is marked deleted and before its row is removed. - `server/src/services/secret-proposals.ts`: new `rejectPendingBindingsForSecret`. It rejects pending binding proposals for a secret with a reason and returns them. This closes their review cards and writes the normal activity entries. `createBinding` now holds the secret row `FOR SHARE` in its insert transaction and refuses a deleted secret. So no new pending proposal can start after the delete marks the secret deleted. - `server/src/routes/secrets.ts`: the delete route rejects pending proposals in `beforeDelete`. For each one it sends the normal rejection notice and wake (`notifySecretProposalResolution`), so the proposing agent continues. - `server/src/__tests__/secret-proposals-routes.test.ts`: a regression test deletes a secret with a pending, rejected and approved binding proposal through the route. It checks the secret and proposal are gone, the card status is correct, and only the pending case sends one rejection notice and one wake. A second test checks that a binding proposal for a secret that the delete already marked deleted fails with 404. ## Verification - Red first: the new test failed on `origin/master` with the shape-check violation for all three cases. - `cd server && vitest run --config vitest.config.ts src/__tests__/secret-proposals-routes.test.ts`: 47 passed. - `vitest run src/__tests__/chat-run-milestone-governed-work.test.ts src/__tests__/agent-secrets-routes.test.ts src/__tests__/secrets-service.test.ts`: 112 passed. - `tsc --noEmit` for `server` and `packages/db`: pass. `pnpm --filter @paperclipai/db run check:migrations`: pass. - Found by the `secrets.secret-proposal` chat evaluation. Its cleanup deletes the fixture secret and failed on this error in runs 42563093640 and 42571127911. ## Risks - The migration only replaces one foreign key. It does not rewrite rows. Existing rows already have a non-null `secret_id` or `secret_proposal_id`, so nothing else changes. - Deleting a secret now also deletes its binding proposal rows. The activity log keeps the proposal history (`secret.proposal.*` and `secret.deleted`). Secret-kind proposal rows that created the secret keep `created_secret_id ON DELETE SET NULL`, so their history stays. - Other paths that delete secrets (company deletion, user secret cleanup) now cascade instead of failing on the same constraint. They do not reject pending cards first. ## Model Used Anthropic Claude Opus 5.5 (`claude-opus-5-5`), used with tool use and code execution in Claude Code. ## 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 - [x] 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> |
||
|
|
d7d0f96f3f |
perf(runner): save provider state once per poll instead of once per frame (#20217)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Native OpenCode, Codex and Cursor Cloud chat turns run through runnerd. runnerd reads provider frames in order and keeps the run's provider state in a durable file > - For every frame that it reads, runnerd rewrote the whole provider state file and called `fsync` on the file and on its directory. The state holds every tool result kept in the turn, so each save got more expensive as the turn went on > - A tool call request is just the next frame. It waited behind one full save for every frame before it. #20209 measured tool-call turnaround that grew from about 1.4 s to about 15 s in one turn > - #20209 stopped the OpenCode proxy from sending frames that runnerd discards. Every frame that runnerd still reads still paid a full save, and other proxies (for example Cursor Cloud) still send such frames > - This pull request makes runnerd save the frames of one poll once, before anything derived from them leaves runnerd. A save that would write the bytes already on disk is skipped > - The benefit is that the number of saves before a tool call no longer grows with the number of streamed frames. In a local measurement, a tool call after 400 frames with 830 KB of kept state took 0.57 s instead of more than 30 s ## Linked Issues or Issue Description Refs #20209 Related: #17514 (the same per-event full-state rewrite, on the server control-plane side) **What happened** During a native OpenCode chat turn, tool-call turnaround grew from about 1.4 s to about 15 s as the turn went on. runnerd saved its complete durable provider state, with `fsync`, after every provider frame. That state includes the tool results kept so far in the turn. A tool call request waited behind one full save for each earlier frame. **Expected behavior** The time to deliver a tool call must not grow with the number of assistant-text, reasoning or item frames that came before it. A crash or restart must still never lose state that a resumed run needs. **Steps to reproduce** 1. Start a native OpenCode (or Codex app-server or Cursor Cloud) turn that keeps a few tool results and then streams many text or reasoning frames before its next tool call. 2. Measure the time from the tool call frame to the `semantic_tool.input` event. 3. See that the time grows with the number of earlier frames and with the size of the kept state. Locally, `cargo test -p paperclip-runner-core --lib measure_streamed_frame -- --ignored --nocapture` on `master` reports 102 saves and 9.3 s for 100 frames with 830 KB of kept state, and more than 30 s for 400 frames. ## What Changed - `packages/paperclip-runner/runner/crates/runner-core/src/provider_backend.rs` and `codex_provider.rs`: - The provider frame loop moves into `poll_provider_frames`. An ordinary notification frame and a descendant notification frame no longer save the state. They mark it as unsaved. - After the loop, `poll_current_provider_with_control` saves the unsaved state once. It also saves when a frame fails, before it returns the error. - Frames that already saved at once still do so: turn terminals, Cursor Cloud terminal deliveries, `reconciliation_required`, protocol failures, MCP permission and runtime requests, process exit, and new tool calls. - A tool call that runnerd answers at once (an exact completed replay or the receipt-limit stop) saves unsaved state before it writes to the provider. A rejected tool call already saved first. - `retained_events`, `execute` and `host_park_ready` save unsaved state first, so no event, command result or park decision can expose state that is not on disk. - Courtesy rejections that `CodexProvider::poll` writes by itself (unsupported requests, descendant requests, delayed requests after a terminal, full runtime-request capacity) are held while runnerd reads a batch (`hold_courtesy_responses`). runnerd sends them only after it saves the state that covers every earlier frame (`save_before_provider_writes`). A terminal frame also saves and releases them before runnerd asks the provider for goal state or drains the accounting tail. Released courtesy replies are best-effort, as courtesy replies were before: a provider that already closed stdin cannot turn a received terminal into a polling failure. The receipt-limit stop sends held cancellations before its `turn/interrupt` request, so their order to the provider does not change. - The trace records for runnerd's normalization (`rust_durable_normalization`), which name emitted event ids, are kept until the state that holds those ids is saved. The trace therefore only names saved events. Saved mappings are written before an exited or failed provider is dropped, so they are not lost. Raw frame records still go to the trace when a frame is read; they name no event ids. - `save_unsaved_frame_state` does not write when the serialized state is byte-for-byte the same as the saved file. All other saves write as before. - Serialization moves to `serialize_provider_state`, and the file write moves to `write_state_bytes`. The write path (private temporary file, `fsync`, rename, directory `fsync`, `0600`) does not change. - New tests in `packages/paperclip-runner/runner/crates/runner-core/src/tests/provider_frame_save_batching.rs`: - `streamed_frames_do_not_multiply_durable_saves_before_a_tool_call`: 8 and 48 streamed frame pairs before a tool call need the same number of saves (1). On `master` they need 18 and 98. - `batched_frame_state_is_saved_before_events_leave_and_survives_a_restart`: at every poll, the saved file is equal to the in-memory state and holds exactly the delivered events. A new executor loads the file after the old one stops and gets the same state, the same unacknowledged events and the pending tool call. - `batched_frame_state_is_saved_when_a_later_frame_fails`: a frame that fails after 20 batched frames still leaves those 20 frames on disk. - `previous_release_state_is_rewritten_by_the_first_batched_save`: a state file in an older shape (compact JSON without newer default fields) loads, and the first batched save rewrites it. Unchanged state is not rewritten. Changed state is. - `courtesy_rejections_reach_the_provider_only_after_earlier_frames_are_saved`: when the courtesy rejection reaches the provider, the saved file (what a restarted runnerd would load) already holds the 20 earlier frames and equals the in-memory state. It fails if the hold is removed. - `trace_interpretations_name_only_saved_events`: every event id that a trace record names is in saved state at the moment the record is written. It fails if records are written before the save. - `a_crash_inside_an_unsaved_batch_leaves_saved_state_and_provider_output_unchanged`: after 122 frames are read and not yet saved, the saved file is byte-for-byte unchanged and runnerd has written nothing to the provider. A reload gets the pre-batch event sequence and no pending events. - `a_held_cancellation_that_cannot_reach_the_provider_keeps_the_completed_turn`: the provider closes stdin and then completes the turn with a tool call still pending. The held cancellation cannot be written, and the turn still completes and is saved. - `trace_interpretations_survive_a_provider_exit_in_the_same_poll`: the trace keeps the mappings for frames that arrived in the same poll as the provider exit. - `measure_streamed_frame_cost_before_a_tool_call` (ignored): prints saves and tool-call latency for different frame counts and kept-state sizes. ## Verification - `cargo test --locked -p paperclip-runner-core` (debug, macOS arm64): library 595 passed, 1 failed; all 35 integration test binaries passed (`--no-fail-fast`). The one library failure is `task_memory::tests::monitor_bounds_notices`. It also fails when run alone, takes about 9 minutes, and waits on file-watch timing in `task_memory.rs`, which this PR does not touch. In one earlier run, two ACPX park tests in `acpx_park_preparation` failed. The same binary also failed on unchanged `master` (with a different test), and it passed in the final run. - `cargo test -p paperclip-runner-core --lib measure_streamed_frame -- --ignored --nocapture`, debug build, macOS APFS (`sync_all` is `F_FULLFSYNC`). Each row is one turn: frames streamed, then one tool call. | Kept state | Frames | Saves (master) | Tool-call latency (master) | Saves (this PR) | Tool-call latency (this PR) | | --- | --- | --- | --- | --- | --- | | 4 KB | 100 | 102 | 946 ms | 1 | 18 ms | | 4 KB | 400 | 405 | 3,791 ms | 7 | 70 ms | | 211 KB | 100 | 102 | 3,207 ms | 1 | 37 ms | | 211 KB | 400 | 405 | 12,803 ms | 7 | 193 ms | | 833 KB | 100 | 102 | 9,251 ms | 1 | 93 ms | | 833 KB | 400 | about 405 | more than 30 s (test deadline) | 7 | 567 ms | With 400 frames, the remaining saves are one per poll of up to 128 frames and one per controller acknowledgement. - With the debug `paperclip-runnerd` and `fake-codex-app-server` binaries from this branch: `npx vitest run` on `src/live/runnerd-opencode-streaming-tools`, `runnerd-opencode-assigned-tools`, `runnerd-codex-transport`, `runnerd-codex-transport-settlement`, `runnerd-codex-transport-cold-restore`, `runnerd-restart-continuation`, `runnerd-saved-continuation`, `runnerd-detach-durability`, and `src/cli/opencode-proxy-output`, `opencode-proxy-events`, `opencode-app-server-proxy`: 11 files, 353 tests passed. ## Risks - Durability. The rule is: nothing derived from a frame leaves runnerd before the state that holds that frame is on disk. That covers delivered events, command results, park decisions, tool results and rejections, courtesy rejections, Cursor Cloud terminal acknowledgements, and trace records that name event ids. The tests above check each of these. - The unsaved window is at most one poll (128 frames). Every frame in it is undelivered: no event from it reached the controller, and runnerd wrote nothing to the provider while it was unsaved. `a_crash_inside_an_unsaved_batch_leaves_saved_state_and_provider_output_unchanged` proves this. A crash in the window therefore leaves the saved state and everything the controller and provider have seen exactly as a crash just before those frames were read. The resumed run starts from the same event sequence, so it cannot deliver an event id twice. Its normal recovery (thread resume and `thread/read` reconciliation) handles those frames in the same way as frames still in the provider pipe. On `master`, a crash could already lose the one frame being processed. - One provider write can still happen in the window: `thread/read`, which `CodexProvider::poll` sends to check the lineage of an unknown helper thread. It is a read-only query that waits for its answer, and it changes nothing on the provider side. - Courtesy rejections now reach the provider at the end of the poll instead of during it. The delay is one batch. The provider gets the same replies. - Previous-release story: the saved file shape does not change, and no new field is saved. A file from the previous release loads as before. The skip check compares exact bytes, so an older file is never treated as already saved. The first save after an upgrade rewrites it in the current shape. A deploy that lands mid-turn sees a file that the previous release saved after each frame. That file is valid input for this code. - The ACPX backend has its own frame loop and is not changed here. ## Model Used - Anthropic Claude Opus 5.5 (`claude-opus-5-5`), used through Claude Code with tool use (shell, file editing) and extended thinking. ## 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 - [x] 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> |
||
|
|
df4562ab38 |
fix(native): an inherited checkpoint with a pending runtime request is evidence (#20226)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - #20153 and #20214 let restart recovery set aside a remote follow-up's admission-rebound checkpoint when the checkpoint is blank and nothing of the run reached a runner. Recovery then restarts the bootstrap instead of blocking > - "Blank" checked the cursor, terminal turns, active turn, semantic result and terminal, but not pending runtime requests > - A rebound checkpoint that still carried an outstanding runtime request would therefore be treated as no evidence. The run would restart as a fresh bootstrap and drop that request > - This pull request also requires no pending runtime requests before the checkpoint counts as a blank admission checkpoint > - The benefit is that an outstanding request always keeps the run on its reconciling path ## Linked Issues or Issue Description Refs #20214 **What happened?** After #20214 merged, ponytail review noted that `admissionCheckpointOnly` (shared by #20153's `remoteRunnerNeverReceivedCommands` and #20214's `inheritedCheckpointOnly`) did not inspect `pendingRuntimeRequests`. A rebound checkpoint with a pending request would be demoted to "no evidence", and the run restarted as `remote_execution_never_claimed`, dropping the request. **Expected behavior** A checkpoint with any pending runtime request is evidence. The run is not restarted as a fresh bootstrap. **Steps to reproduce** 1. Claim restart recovery for an unclaimed remote follow-up whose rebound checkpoint carries a pending runtime request and no other state. 2. Before this change, recovery claims `bootstrap_incomplete`. **Paperclip version or commit** Current master after #20214. ## What Changed - `native-restart-recovery.ts`: `admissionCheckpointOnly` additionally requires `pendingRuntimeRequests` to be absent or empty. - `native-runner-restart-recovery.integration.test.ts`: a held case, an unclaimed remote follow-up whose inherited checkpoint carries a pending runtime request. ## Verification - Red/green: on master the held case is restarted as `bootstrap_incomplete`. With the change it is not, and the other cases are unchanged. - `vitest run` of `native-runner-restart-recovery.integration.test.ts` and `native-restart-recovery.test.ts`: 2 files, 201 tests passed (14 skipped). - `PAPERCLIP_TYPECHECK_LOW_MEMORY=1 pnpm typecheck` (`server`): passed. ## Risks - Minimal. The change only narrows when a checkpoint counts as blank. Every run it affects keeps the existing, more conservative classification. ## Model Used - Claude Opus 5.5 (`claude-opus-5-5`), via Claude Code with tool use (shell, file edits, test execution). ## 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 - [x] 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> |
||
|
|
8525bb534c |
fix(chat): post a turn's answer when a card it did not create is pending (#20220)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - People talk to agents in Agent Chat. The agent's final message becomes the chat reply. > - A chat thread can hold a pending card that the board addressed to another person, for example a human-only confirmation for Riley. > - When Alex asks the agent to act on that card, the agent correctly gets `403 interaction_human_only` and writes a final answer that explains the denial. > - The shared answered rule (`conversationTurnAnswered()`, from #20179 and #20204) counted any pending human-only card in the thread as the turn's own response. So the answer was suppressed, and the chat stayed `active` with no reply. > - This pull request limits that rule to cards that the turn's own run created. Other cards still hold the task status. > - The benefit is that the person always gets the turn's answer, unless the turn itself asked a question with a card. Live completion and restart recovery stay consistent. ## Linked Issues or Issue Description Refs #20179, #20204 (the shared answered rule this PR corrects). **What happened** In an Agent Chat, the board created a pending `request_confirmation` card with `resolverPolicy: human_only`, addressed to another user (Riley). The card has no source run. Alex wrote "I approve Riley's request." The agent tried to accept the card and got `403 interaction_human_only`. It then wrote a correct final answer: the card needs Riley. The run succeeded with a `yielded` result and a `response_wake` continuation. Run completion recorded `presentationDecision: none`. No comment was created. The conversation stayed `active`, so Alex never got a reply. Cause: `conversationTurnAnswered()` reused the status-gate query `pendingNativeInteraction()`. In conversation mode, that query excludes another run's card only when it is an ordinary question or an `anyone` confirmation. A human-only card addressed to someone else is kept. The rule then treated that card as the card that "presents the turn", although this run did not create it. **Expected behavior** A successful chat turn posts its final answer exactly once, and the conversation moves to `waiting`. Only a pending question or confirmation card that the turn's own run created presents the turn in place of its prose. A card from an earlier turn, or one the board addressed to another person, can hold the task status, but it does not suppress the answer. **Steps to reproduce** 1. Open an Agent Chat as Alex. 2. As the board, create a pending `request_confirmation` card in the chat with `resolverPolicy: human_only` and `addresseeUserId` set to another person. 3. Send "I approve Riley's request." 4. Let the agent try to accept the card, get `403 interaction_human_only`, and end its turn with a final answer. 5. See that no reply appears, `presentationDecision.commentAction` is `none`, and `conversationState` stays `active`. ## What Changed - `server/src/services/native-runtime/native-governance.ts`: `conversationTurnAnswered()` now checks only pending cards whose `sourceRunId` is the turn's run. `pendingNativeInteraction()` gets an optional `createdByRun` filter for this. The status gate (`pendingNativeGovernance()`) does not change, so another person's card still holds the task. - Both callers get the fix through the one shared rule `conversationTurnPresentsAnswer()`: live run completion (`run-completion.ts`) and background presentation recovery (`recoverCommittedNativeChatResponse`). - `server/src/services/heartbeat/run-completion.test.ts`: new live cases "someone else's confirmation card" (board-created, human-only, addressed to another user, no source run) and "earlier turn's question card" (created by an earlier run of the same agent). Each case now also checks that the conversation moves to `waiting`. - `server/src/services/native-runtime/agent-chat-response-recovery.test.ts`: new restart case for the board-created card, plus a `waiting` check after recovery. - `doc/execution-semantics.md`: state that only the turn's own card presents the turn. ## Verification - Thesis: the reply is suppressed because the answered rule counts any pending human-only card in the thread, not only this run's card. If this is true, a turn with only a board-created card addressed to someone else posts no comment on master, and restricting the rule to `sourceRunId = runId` posts exactly one comment and settles the conversation to `waiting`, with the own-card cases unchanged. - Failing first: without the fix, the two new live cases failed (`expected [] to deeply equal [answer]`). The new recovery case also failed (no comment). - After the fix: - `cd server && npx vitest run src/services/heartbeat/run-completion.test.ts -t "posts a waiting conversation turn"`: 9 passed (includes the two own-card cases, which still post nothing). - `cd server && npx vitest run src/services/native-runtime/agent-chat-response-recovery.test.ts`: 62 passed. - Both suites use a real embedded PostgreSQL database. - Turn start: no change. The rule runs only after the provider turn ends. ## Risks - Low. Only the presentation rule changes. The status gate, the conversation settle rule (it already required `sourceRunId = run.id`) and the external-chat path do not change. - Behavior shift: a turn now posts its answer while another run's or the board's card is pending. Before, the answer was lost. A card that the turn itself created still presents the turn, as before. - Previous-release state: no persisted shape change. A run that the previous release already recorded as `none` keeps that decision. Recovery does not re-present it. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - Anthropic Claude Opus 5.5 (`claude-opus-5-5`), with tool use (shell, file edits, test runs) in Claude Code. ## 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 - [x] 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> |
||
|
|
fe72bc5f81 |
fix(native): roll back a warm rotation cut off before its new control plane once its runner is proven dead (#20218)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - A warm follow-up rotates authority before it attaches. It archives the previous epoch (suspended runner state and settled controller) under `authority-epochs/`, then writes its own control plane > - #20146 taught recovery to roll back a rotation whose new control plane held only an undelivered `run.attach`, once its runner was proven dead > - A hard kill can land one step earlier, after the archive and before the new control plane exists. The root then holds only the archive, with no active control plane and no runner state. The executor read "no durable identity" and failed with `runner_state_identity_mismatch: durable_identity_unreadable` > - This pull request applies the same proven rollback to that earlier point of the rotation > - The benefit is that a hard kill anywhere in the rotation window resumes the follow-up from the previous turn ## Linked Issues or Issue Description Refs #20146 **What happened?** Local mixed restart-chaos wave w-20261109T081456-s9901 (master |
||
|
|
ad57d5cc12 |
fix(native): restart an unclaimed remote follow-up that carries only its inherited checkpoint (#20214)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - A remote per-turn follow-up inherits the previous run's session checkpoint at admission, rebound to its own identity. Its session root still holds the previous owner's remote marker until the follow-up rotates authority > - If a restart lands before the follow-up's controller ever claims execution, recovery should restart its bootstrap: nothing of this run reached any runner > - Recovery's unclaimed-bootstrap check required no checkpoint and no provider evidence. The inherited checkpoint failed both, so recovery claimed `reattach_remote_runner` > - Even under a bootstrap claim, the executor skipped the per-turn projection restore for every restart claim. It therefore never rehydrated the previous owner's external marker, and failed with `runner_state_identity_mismatch: durable_identity_unreadable` > - This pull request does not count the inherited checkpoint as evidence for an unclaimed remote run, and lets a bootstrap claim restore the previous owner's projection, as a fresh follow-up start does > - The benefit is that a follow-up drained in its first second on Daytona restarts on its own ## Linked Issues or Issue Description Refs #20153 **What happened?** Daytona per-turn restart-chaos K13 storm w-20261109T074128-s9503 (revision |
||
|
|
98e0f1fc0d |
fix(native-runtime): a stopped run cannot close a task another run continues; deliver a pre-restart comment at once (#20216)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - When the board stops a run and then posts a comment asking the agent to continue, that comment starts a new run, and only that new run should decide when the task is done > - In restart chaos wave `w-20261109T081456-s9901`, t16 (claude S1, K1) closed the task as done without the line the user asked for, and t10 (codex S1, K6) never woke a run for the continue comment, so it needed a Retry > - In t16, the stopped run's saved "done" result was accepted 10 s after the continue run started. In t10, a restart killed the server 21 ms after the comment committed, before its wake was written, and the comment backstop then waited out its 30 s grace > - This pull request stops a run from closing a task that another run now holds, and makes the backstop deliver at once a comment that predates the server process > - The benefit is that a user's latest direction after a Stop is neither overridden by the stopped run nor lost to a restart ## Linked Issues or Issue Description Refs #19831. Restart chaos wave `w-20261109T081456-s9901` (master `09e24dd12c`), tasks t16 and t10. **t16, from the kept database (CHA-20):** - 08:29:19: the board stopped run 1 (`6e33d123`). Its saved result reported done, but finalization failed on the stop (`finalization_failed_claim_preserved`, a retryable failure). - 08:29:41: the user's continue comment started run 2 (`2cb71c5b`). Run 2 held the task's execution and saw the comment. - 08:29:51: the finalization reconciler retried run 1's saved result and committed `completion_claim_policy_accepted` for run 1. The task closed as done. - From then on, every Paperclip tool call from run 2 was refused (`paperclip_runner_tool_binding_not_authorized`), so it could not add the `CONTINUE-…` line. Its result (blocked) was then preserved as `terminal_status_preserved`. - Run 2 ending `succeeded` with `native_session_lease_lost` follows from this: its result was accepted before the lease was lost. The comment was delivered; the model saw it. **t10 (CHA-14):** - The Stop moved the task to blocked. The user's comment reopened it to todo at 08:26:28.633, and K6 killed the server at 08:26:28.654, before the route wrote the comment's wake. No wake request exists for that comment. - This is not caused by the Stop, #20066 or #20180: the wake is written after the comment commits, and the kill landed in between. - The I4 backstop ("a user comment without a wake") matched the comment, but treated it as possibly still getting its normal wake for 30 s. The pass interval then pushed the check to 02:27:16, and the harness clicked Retry at 02:26:59. ## What Changed - `status-arbiter.ts`: - New `heldByAnotherRun(issue, runId)`. - `supersedeStaleOwnerCompletion` also keeps status, with reason `completion_run_superseded`, when the issue's `executionRunId` is a different run. A run that was reassigned away still gets `completion_owner_superseded`. - `native-run-finalizer.ts` and `native-status-reassessment.ts` pass the run id to that guard. - `status-decision-committer.ts` rechecks the same rule under the issue lock: a done decision for a non-conversation issue held by another run raises `NativeStatusRaceError`. The finalizer then re-arbitrates and keeps status. - `ownership-invariant-report.ts`: the comment-without-wake grace now only applies to comments created after this server process started (`getServerProcessStartedAt`). Separate live server replicas are not supported, and the wake is written by the process that accepted the comment, so an older comment is owed at once. The startup pass now delivers it. `runOwnershipReconciliationPass` takes an optional `processStartedAt`. ## Verification Red/green: - `native-stale-owner-completion.test.ts`: three new tests. - The unit policy: another run holding the task keeps status; the same run, or none, still closes it. - "does not let a stopped run's saved done result close the task a newer run is continuing" (the t16 shape). - "does not close the task when a newer run takes it after arbitration but before the status commit" (only the committer recheck covers this). - On master: 3 failed. With this PR: 13 passed. - `ownership-reconciliation-report.test.ts`: "finds at once a young user comment whose wake a restart lost after the board stopped the run" (the t10 shape). It fails on master and passes with this PR; the same comment in a process that accepted it still waits for its grace deadline. The file: 27 passed. Regression and static checks: - 16 nearby native finalization, status projection, workspace finalizer and arbiter suites: 439 passed. - `tsc --noEmit` passes for `server`, and for the two test files. ## Risks - A done result now waits when a different run holds the task. That run reaches its own decision; the earlier result stays as history. If the holder is stale, the existing execution-lock recovery clears it, and a later reassessment can then close the task. - After a restart, a user comment newer than the task's last run, with no wake, is delivered at the startup pass rather than 30 s later. A replica that is still live and writing the same wake is not a supported topology. - No schema change and no change to persisted run state. ## Model Used - Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with tool use (shell, file edits). ## 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 - [x] 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> |
||
|
|
088bdf7cfa |
fix(daytona): wait out a sandbox state change before starting it for workspace realize (#20212)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - On Daytona, every run realizes its workspace in the leased sandbox. Realizing it first makes sure the sandbox is started > - Right after a control-plane restart, the sandbox can still be mid-transition, for example stopping or starting from the previous lifecycle step > - The plugin called `sandbox.start` straight away. Daytona rejects that with "Sandbox state change in progress", so the run failed `setup_failed` and needed a Retry > - This pull request waits out a sandbox's state change before starting it, and retries a start the provider rejected for that reason, within the existing start deadline > - The benefit is that a run whose sandbox is briefly transitioning starts on its own ## Linked Issues or Issue Description Refs #20211 **What happened?** Daytona per-turn restart-chaos K13 storm w-20261109T074128-s9503 (revisioncanary/v2026.1109.0-canary.9 |
||
|
|
6383eb8033 |
fix(authz): require board authority for agent budget changes on every route (#20213)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Budgets stop agent spending. The budget hard stop is a control-plane invariant > - The budget routes (`PATCH /api/agents/:id/budgets`, `PATCH /api/companies/:id/budgets`) are board-only > - `PATCH /api/agents/:id` also accepts `budgetMonthlyCents`, and the configuration service writes the agent budget policy from it. That route had no board check for the field > - So an agent, such as a CEO, a manager or the agent itself, can change a monthly budget without a board user. This is a second route around a board-only control, like the pause bypass that #20164 fixed > - This pull request applies the budget route authority to every route that writes an agent budget, and gives agents the final board-only denial with a link to the budget page (#20151) > - The benefit is that only the board changes budgets, and an agent that asks gets a final answer that it can pass to the person ## Linked Issues or Issue Description Refs #17857 (earlier fix, now in conflict with master; this PR rebases it and adds the budget page link). Refs #20164 and #20151 (same pattern for PATCH status and board-only denials). **What happened** An agent can send `PATCH /api/agents/{id}` with `budgetMonthlyCents`. The route accepts it, and the configuration service writes the agent budget policy. This works for the agent's own budget and, with update permission, for other agents. The same field also reaches budget policy through a configuration rollback, direct agent creation, an immediate hire and a built-in agent provision. A chat eval (`budgets-costs.resolve-budget`) found the route. **Expected behavior** Only a board user can change an active budget, as on the dedicated budget routes. An agent gets `403` with code `board_access_required`. The denial says it is final and links the agent's budget tab (`/<prefix>/agents/<id>/budgets`) or the company budgets page (`/<prefix>/activity/budgets`). An agent can still propose a budget in a hire that waits for board approval. The approving board user activates it. **Steps to reproduce** 1. On master, authenticate as an agent of a company (for example the CEO). 2. Send `PATCH /api/agents/{agentId}` with body `{"budgetMonthlyCents": 5000}`, for its own id or for another agent. 3. See `200`. The agent budget policy changes with no board action. 4. The new route tests reproduce this: on master source they fail with `expected 200 to be 403`. ## What Changed - `PATCH /api/agents/:id`: a changed `budgetMonthlyCents` requires a board actor. An unchanged amount with other edits still works. - Configuration rollback: a rollback that changes the budget requires a board actor. - `POST /api/companies/:id/agents` and an immediate `agent-hires`: a positive budget requires a board actor. A hire that needs board approval keeps the proposed budget and does not activate it before approval. - Built-in agent provision: a budget override requires a board actor. - `prepareAgentConfiguration` rejects an agent-originated budget change again (defense in depth). An agent rollback with an unchanged amount does not rewrite the policy. - Board writes keep the actual operator identity. The initial hire budget is saved in the hire transaction. - `assertBoard` and `boardAccessRequired` take an optional board page. Budget denials link the agent's budget tab or the company budgets page, not the general agent profile. - Agent access map updated. - This includes the four commits of #17857 ( |
||
|
|
b1049b174e |
fix(native): retire a finished remote owner whose suspend never finished once its sandbox leases ended (#20211)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Before a per-turn follow-up starts a new runner, it retires the previous turn's finished owner. Retirement requires the owner's controller journal to be settled > - A local runner that is proven stopped can no longer act on its own lifecycle commands, so an unfinished `runner.suspend` from it does not hold the follow-up > - On a remote per-turn sandbox the same thing happens when a restart or drain lands while the runner is suspending. But the remote path had no equivalent rule, so the unfinished `runner.suspend` held the follow-up even after the previous run's sandbox lease had ended > - This pull request treats a remote owner's lifecycle commands as settled once every sandbox lease of that run has been released or expired, and none of them is the sandbox the follow-up now uses > - The benefit is that a C1 follow-up after a restart on Daytona continues on its own ## Linked Issues or Issue Description Refs #20140 **What happened?** Daytona per-turn restart-chaos K13 storm w-20261109T074128-s9503 (revision |
||
|
|
fc5c79a327 |
fix(guidance): tell agents who acts and which page to link for person-only steps (#20206)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Agents in chat read the API catalog (`search_api`) and `connections_search` to learn what they can do and who must act > - Some requests need a step that only a person can take: connect an assistant, allow a quarantined app action, turn on an experiment, enter an API key, or finish their own connection request > - For these steps, no catalog entry or search result said who acts or which page to link, and none of them returns a board-only 403, so the denial guidance from #20151 and #20182 does not reach them > - The agent knew a person had to act, searched for the page until its turn ran out, and never replied > - This pull request puts the fact and the exact page link where the agent already looks, following #20152 and #20170 > - The benefit is that the agent replies at once with the right page instead of timing out ## Linked Issues or Issue Description Refs #20151, #20152, #20170, #20182 **What happened** In chat, five requests ended with no reply. The agent searched the API until the turn deadline: - "Connect my assistant so it can see our tasks." `GET /api/mcp/setup` is refused for agents. Its catalog entry had no description, so the refusal text "Its catalog description says who can use it" pointed at nothing. - "Use the app's new export feature." The new action was quarantined. No catalog entry said where a person reviews it. - "Let me chat with Morgan over iMessage." `connections_search` hides chat channels while the Chat connectors experiment is off, so the agent could not find iMessage or the setting that hides it. `POST /api/companies/{companyId}/chat-endpoints` also accepted a draft endpoint for the turned-off provider. - "Connect my account using an API key." The service was the person's own MCP server. `connections_search` returned no result and did not name the secure "Connect your own MCP server" page. - "I will finish Alex's connection request for them." No catalog entry said that only the addressee can resolve a connection request card. **Expected behavior** The agent says who must act, links the exact page, and replies in the same turn. It does not search further, does not ask for a key in chat, and does not create another request. **Steps to reproduce** 1. Ask a chat agent one of the five requests above, in the matching state (for example, Chat connectors off, or a quarantined catalog entry). 2. Watch the run events. The agent calls `search_api` and `call_api` until the turn deadline and posts no reply. ## What Changed - `server/src/routes/openapi.ts`: add catalog descriptions that say who acts, link the page, and say not to search further: - `GET /api/mcp/setup` and `POST /api/mcp/requests/{id}/consent` link [Assistant connections](/<prefix>/apps/assistant-connection) and say the consent screen offers only access within the person's own role. - `GET /api/tool-connections/{connectionId}/catalog`, `.../review-counts` and `GET /api/companies/{companyId}/tools/apps/attention` say a quarantined action waits for a person on `/<prefix>/apps/<connectionId>/permissions`. - The four `/api/connection-intents/{interactionId}/...` routes say only the addressee can resolve the card, and link the task that holds it. - `server/src/services/connection-intents.ts`: - While Chat connectors is off, a search that names a chat-only channel (for example iMessage) returns it as `unavailable`, with a reason that links `/<prefix>/company/settings/instance/experimental`. Channels that also offer tools, such as Slack, keep their current result. - A single `unavailable` result now always gets the "explain the reason, do not bypass" instruction. - When no listed service matches and the person gave the service's own server URL, the instruction links `/<prefix>/apps/byo`, the secure "Connect your own MCP server" page, and says never to ask for a key in chat. - `server/src/routes/chat-channels.ts`: `POST /api/companies/{companyId}/chat-endpoints` returns 403 with the Experimental settings page while the provider's experiment is off (Chat connectors, or GitHub review bots for GitHub). AgentMail is unchanged. The catalog entry says the same. The UI already hides the setup pages in this state. - `GET /api/issues/{id}/interactions` says only the addressee resolves a connection request card. - A hidden channel that an aggregator can serve (for example Discord) keeps its aggregator routes instead of the unavailable result. - Tests: `person-only-page-guidance.test.ts` checks the catalog text and that every named page is a route that `ui/src/App.tsx` mounts. `connection-intents-service.test.ts` covers the hidden channel (off and on) and the own-server page. ## Verification - `cd server && npx vitest run src/__tests__/person-only-page-guidance.test.ts src/__tests__/connection-intents-service.test.ts` passes. `chat-channels.integration.test.ts` passes with `-t` for every test that posts to the create route. - These related suites pass: `openapi-routes`, `connection-aggregator-fallback`, `generic-mcp-connection`, `assistant-connections-guidance`, `company-model-service-settings-guidance`, `board-access-denial`, `email-channels.integration`, and the eight suites that import `runner-api-catalog`. - `npx tsc --noEmit -p server` shows no errors in the changed files. - End to end: the full-stack chat evals for the five requests ran on a branch with this change. The run results are in the comments below. ## Risks - Low risk. The change is text in the API catalog and in `connections_search` results, plus one result for a channel that was hidden before. - A hidden channel now shows in search as `unavailable` with no methods. It cannot be set up from the result, so the gate stays in place. - Chat endpoint creation is now refused while the experiment is off. Callers that created endpoints with the setting off now get 403. Three integration tests that relied on that now turn the setting on first, and a new test covers the refusal. - The single-`unavailable` instruction now also applies in suggestion mode. Before, that mode told the agent to "respect unavailable states" with no reason. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - Anthropic Claude Opus 5.5 (`claude-opus-5-5`), used through Claude Code with tool use (shell, file edits, test runs). ## 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 - [x] 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> |
||
|
|
9c18cf61da |
fix(runner): keep discarded OpenCode token deltas from delaying tool calls (#20209)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Native OpenCode runs use a TypeScript app-server proxy between OpenCode and runnerd > - runnerd reads the proxy's frames in order, only after the controller acknowledges earlier events > - For each frame, runnerd rewrites its whole durable provider state with fsync, including every tool result that the turn retains > - The proxy sent one `item/delta` frame for each reasoning or tool-output token, and runnerd discards `item/delta` > - A Paperclip tool call request therefore waited behind hundreds of discarded frames, and the wait grew with each tool result in the turn > - This pull request stops the proxy from sending the token deltas that runnerd does not consume > - The benefit is that tool calls reach runnerd without that queue, so agent turns finish faster ## Linked Issues or Issue Description **What happened** In the chat eval `plugins.configure-plugin` (runner-opencode, deepseek-v4.1-flash, GitHub Actions run 42574640038), `paperclip_search_api` calls in one turn slowed from about 1.4 s to 9-15 s each. The server handled every call in 50-250 ms (`tool.execution` spans). For one `call_api` call, 6.3 s passed between the server's 403 and the next agent activity. Time to first token was about 4.4 s. The run events show the cause. Events that OpenCode streams reach runnerd in bursts. In one burst, the usage events for three different model requests arrived within 0.5 s, which no model can do. Each burst ends with the next tool call request. 143 of 203 recorded runner-opencode runs that we checked show such bursts, and 108 show a gap of 8 s or more. Many chat eval cases that time out use this runner. The mechanism: 1. OpenCode streams reasoning one token at a time. The proxy forwarded each token as its own `item/delta` frame. 2. runnerd reads proxy frames in order and only after the controller acknowledges earlier events. For each frame it calls `save_state`. That serializes the whole provider state, with the tool results retained in the turn, and fsyncs it. 3. runnerd's Codex facade has no mapping for `item/delta`, so it discards the frame after the save. 4. A tool call request is one more frame in the same ordered stream. It waits until runnerd works through every earlier delta frame. The wait is about (reasoning tokens) x (save cost), and the save cost grows with each retained tool result. **Expected behavior** A Paperclip tool call reaches runnerd soon after OpenCode makes it. Its delay does not grow with the reasoning length or with the number of earlier tool calls in the turn. **Steps to reproduce** 1. Run a native OpenCode chat turn with a reasoning model that makes many sequential Paperclip tool calls, for example `search_api`. 2. Compare each tool's `item.completed` tool_result time with the `tool.execution.completed` time and with the next `item.started` tool_use time in the run events. 3. See that the gap grows during the turn, and that events for several model requests arrive within less than one second. 4. For a local model of the mechanism: run the pinned OpenCode binary with a controlled OpenAI-compatible provider that streams 300 reasoning tokens before each tool call. Pass the driver events through the proxy's mapping and batching into a reader that consumes one frame at a time with a save cost that grows by 4 ms per completed tool result. Before this change, 8 sequential calls waited 1.7, 3.1, 4.2, 5.4, 6.7, 7.9, 9.2 and 10.5 s behind 142-261 queued frames. After this change, they waited 64-93 ms behind 0 frames. ## What Changed - `shouldForwardOpenCodeProxyItem` now takes the event type and payload. For `item.delta`, it forwards only assistant text (`agentMessage` or `text`), which runnerd maps to `item/agentMessage/delta`. - The proxy keeps reasoning and tool-output token deltas. Completed reasoning and tool items still go to runnerd as `item/completed`. Tool progress still goes as `item/updated`. - New regression test: a step with 1,000 streamed reasoning and tool-output tokens puts the same number of frames before the tool call request as a step with 1 token, and no `item/delta` frame is written. - New test: assistant text deltas are still forwarded. Completed reasoning items are still forwarded. ## Verification - `pnpm exec vitest run src/cli/opencode-proxy-events.test.ts src/cli/opencode-proxy-output.test.ts` in `packages/paperclip-runner`: 15 passed. - Red/green: with the predicate change reverted, the two new tests fail. With the change, they pass. - `tsc --noEmit -p tsconfig.test.json` in `packages/paperclip-runner` has no errors in the changed files. Local errors exist only in `src/eval/workflow-harness.ts`, which needs a built `@paperclipai/paperclip-eval-kernel`. - Local before/after measurement with the real pinned OpenCode binary (`opencode-ai@1.18.34`), described in Steps to reproduce, step 4. - Not run locally: the suites that need the `paperclip-runnerd` debug binary or the bundled provider entrypoints (`runnerd-opencode-streaming-tools`, `runnerd-codex-transport`, `opencode-proxy-thread-read`). They fail the same way on the unchanged base in this worktree because those build outputs are missing. CI covers them. ## Risks - Low risk. The changed frames were discarded by runnerd. No persisted state shape changes, so no previous-release story is needed. - If runnerd later maps a reasoning delta method, the proxy must forward it with batching, as it does for assistant text. The test names this boundary. - This change does not change the per-frame durable save in runnerd. Mapped frames (assistant text, tool items) still pay it. A runnerd change that skips the save when a notification changes no state would protect other proxies too. That is a separate change. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - Anthropic Claude Opus 5.5 (`claude-opus-5-5`), used through Claude Code with tool use (shell, file edits, local benchmarks). ## 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 - [x] 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> |
||
|
|
09e24dd12c |
fix(authz): say every plugin management step is board-only in plugin denials (#20207)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - People ask a chat agent to do work, and some of that work is board-only, such as plugin management > - When an agent gets a board-only denial, the denial tells it to stop and reply with a link to the board page > - The denial only spoke about the one route that was denied. An agent that read the plugin list first was not told that plugin configuration and secret binding are also board-only > - The agent then searched the API catalog for an agent route to configure the plugin. It searched for 110 seconds, and its reply came after the chat window closed > - This pull request adds one fact to every board-only denial on a plugin route: all plugin management steps have no agent route > - The benefit is that the agent replies at once with the plugin settings link, and does not search for a route that does not exist ## Linked Issues or Issue Description Refs #20182 (the plugin denial link and "reply now" text that this builds on). **What happened** In the chat eval `plugins.configure-plugin`, the person asked the agent to finish configuring the preview plugin. The agent called `GET /api/plugins` and got the board-only denial. Its reasoning said: "Denial final. But ... perhaps the preview plugin is configured through a different route accessible to agents". It then made 14 more catalog searches for plugin config, credential and secret-binding routes. The run did not reply before the 150 second chat window closed. **Expected behavior** After a board-only denial on any plugin route, the agent replies now. It says that a board user must configure the plugin, and it links the plugin settings page. **Steps to reproduce** 1. Install a plugin that has a required secret setting, and do not configure it. 2. In chat, ask an agent: "Finish configuring the preview plugin." 3. The agent calls `GET /api/plugins` and gets 403 "Board access required". 4. Before this change, the denial does not say that configuration and secret binding are also board-only, and the agent searches for another route. ## What Changed - `server/src/routes/authz.ts`: a board-only denial for an agent on any `/api/plugins` route adds one sentence. The sentence says that listing, installing, configuring (including choosing a secret for a plugin setting), enabling, disabling, upgrading and removing plugins have no agent route. - Other board-only denials do not change. Plugin tools that are assigned to an agent are not mentioned, so tool use is not discouraged. - `server/src/__tests__/board-access-denial.test.ts`: a regression test for the plugin list and plugin config denials, and a negative check for a non-plugin denial. ## Verification - `cd server && npx vitest run src/__tests__/board-access-denial.test.ts` passes (26 tests). - Without the `authz.ts` change, the new test fails. - The chat eval `plugins.configure-plugin` is rerun on a case branch that includes this commit. The result is in the PR comments. ## Risks - Low risk. The change only adds text to the 403 body that agents get. People and board actors get the same body as before. - The sentence is a fact about the current routes. Every plugin management route uses `assertBoard`, `assertBoardOrgAccess` or `assertInstanceAdmin`. If a plugin management route becomes agent-accessible later, this sentence must change too. ## Model Used - Anthropic Claude Opus 5.5 (`claude-opus-5-5`), with tool use and code execution in Claude Code. ## 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 - [x] 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>nightly/v2026.1109.0-nightly.0 canary/v2026.1109.0-canary.8 |
||
|
|
6fd46c0025 |
perf(budgets): judge Paperclip's share of restart recovery; report the provider's session load (#20183)
## Thinking Path - Speed budgets protect the time to first visible agent activity, and restart recovery is one of the judged metrics (`native-speed-budgets.ts`, wave grader). - In the B1 concurrency-6 chaos wave (`w-20261109T045320-s9807`), Codex `restart_recovery` was p50 8.6 s and p95 11.2 s, against a budget of 6.5 s and 8.0 s. At concurrency 4 (K13) it was 3.8 s and 5.0 s. - The preparation spans on a copy of the kept database break it down: - Paperclip's share (ready → claim → prepare → runtime stage → runner launch) is a steady 1.2–2.0 s. - `provider.session.load`, the Codex app-server boot plus `thread/resume` of long saved sessions, takes 3.0–6.3 s, 65–75% of each recovery. - Resumes run concurrently, and the worktree lock and the startup reconciler are not on the path. - Judging the full latency therefore mixes our code with provider and session-size effects. Budget the part we control, so a regression in our code shows up regardless of session size or concurrency, and report the rest. ## Linked Issues or Issue Description **What happened?** `restart_recovery` breached its budget whenever many long provider sessions resumed at once, although Paperclip's own recovery work stayed within about 2 s. The breach did not point at anything Paperclip could fix, and a real regression in our code could hide inside the provider-load variance. **Steps to reproduce** 1. Run a local chaos wave at concurrency 6 with long-running Codex tasks and K1/K2/K4 faults (for example B1, seed 9807). 2. Compare `restart_recovery` in `speed.json` with `provider.session.load` in the recovered runs' run logs. **Expected behavior** The budget judges Paperclip's share of a recovery. The provider's session load and the full latency are reported but not judged. ## What Changed - `server/src/services/native-runtime/native-speed-budgets.ts`: - New budgeted metric `restart_recovery_control_plane`: from `ready` to the start of `provider.session.load`, or to the next provider event when nothing reloads (for example a reattached runner). - Its budget is 2.5 s / 4.0 s for claude, codex and opencode. Measured: 1.2–2.0 s per run at concurrency 6. - The full `restart_recovery` stays in the table for reporting. - Breach payloads carry `restartTotalMs` and `providerLoadMs`. - `server/src/services/native-runtime/native-run-trace.ts`: - The per-run check records when `provider.session.load` starts, keeps the recovery measurement, and judges it when the run finishes. - It judges after the deferred diagnostics drain, so a provider span committed after the provider event still counts. - It emits `restart_recovery_control_plane` breaches only. - `tests/restart-chaos/ttft-baseline-stats.ts`: each recovery sample gains `controlPlaneMs` and `providerLoadMs`. The provider-load start is the span event's write time minus its duration. - `tests/restart-chaos/wave-speed.ts`: - Three restart rows per adapter: `restart_recovery` (report-only), `restart_recovery_control_plane` (budgeted) and `restart_provider_load` (report-only). - All three have outage time removed. - `tests/restart-chaos/ttft-baseline.ts`: the recovery table adds a "Paperclip share" column. - Docs: `doc/run-log-events.md` (metric and payload) and the speed-budget table in `doc/plans/2026-11-07-durable-ownership-and-reconciliation.md`. ## Verification - `server`: `npx vitest run src/services/native-runtime/native-speed-budgets.test.ts`, 18 passed. Two new tests: - a recovery dominated by provider load does not breach; - a slow Paperclip share breaches with `restartTotalMs` and `providerLoadMs`, even when the provider span is committed after the provider event. The existing recovery tests moved to the new metric. All six were red before the trace change. - `tests/restart-chaos`: `npx vitest run --config tests/restart-chaos/vitest.config.ts wave-speed.test.ts ttft-baseline-stats.test.ts`, 29 passed. New tests cover the sample split and the grader rows (a judged share, a report-only provider load, zero breaches). - Server typecheck: no errors in the touched files. ## Risks - **Lower sensitivity to provider-side slowdowns.** A provider regression in session load no longer breaches. It stays visible in the report-only rows and in breach payloads. - **Runs with no `provider.session.load` span** (reattach, or an adapter that doesn't emit it) are judged on the full latency against the control-plane budget. That's conservative. - **Run-log consumers that filter on `metric: "restart_recovery"`** see `restart_recovery_control_plane` from now on. The doc notes the change date. ## Model Used Claude Opus 5.5 (`claude-opus-5-5`, Anthropic), 1M context window, tool use and code execution, via Claude Code. ## 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 - [x] 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> |
||
|
|
42bc238aa5 |
fix(runner): keep a restart continuation when the lost turn's terminal is delivered again (#20196)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - When a restart loses a native run's active turn, the run records it as `provider_turn_lost_on_restore` and continues in a same-conversation continuation turn > - #20118 taught the event consumer to skip a stale restart-lost terminal of another turn once a continuation is running. It applied only when the checkpoint already recorded the interruption > - When the checkpoint instead names the lost turn as active (the drain hit right after the first turn was submitted), the consumer starts the continuation on the first lost terminal. If the same lost terminal is then delivered again, it was taken as the run's terminal and failed the run > - This pull request applies the skip to any recovered run once its continuation has started. For a checkpointed interruption it still waits for the startup decision first > - The benefit is that a turn lost on restore always continues, however often its lost terminal is delivered ## Linked Issues or Issue Description Refs #20118 **What happened?** Local restart-chaos K13 regression storm w-20261109T055902-s9402 (mastercanary/v2026.1109.0-canary.7 |
||
|
|
ad2304dc88 |
fix(chat): recover a held conversation turn's answer after a restart (#20204)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - People talk to agents in Agent Chat. The agent's final message becomes the chat reply. > - A chat turn can end while its task stays held: a pending approval, an execution stage or a dependency blocker keeps the status. > - #20179 made live run completion post that held turn's answer. It added `conversationTurnAnswered()`. > - Background presentation recovery still required a `conversation_turn_finished` decision and a `done` result. > - If the server restarts between the status commit and the live presentation, recovery skips the held turn. The person never gets an answer. > - Restarts are product failures. A committed answer must not be lost. > - This pull request makes recovery use the same answered rule as the live path, through one shared helper. > - The benefit is that a held turn's answer reaches the chat exactly once, with or without a restart. ## Linked Issues or Issue Description Refs #20179 (live fix; this PR closes the "Known gap" in its Risks section). **What happened** In an Agent Chat, the agent resubmitted a hire approval and ended its turn with a yielded result. The finalizer committed `governed_response_waiting` and moved the task to `in_review`. The server restarted before run completion posted the reply. Background presentation recovery found the run as a candidate, but declined it, because it accepted only `conversation_turn_finished` with a `done` result. No reply was posted. The run stayed a pending presentation candidate. **Expected behavior** After a restart, recovery posts the held turn's final answer exactly once. It never posts a second copy when the live path already presented the answer. When the turn's own question or confirmation card presents it, recovery posts nothing and records that decision, as the live path does. **Steps to reproduce** 1. Open an Agent Chat. Link a pending hire approval to the chat task. 2. Let the agent end the turn with a yielded result and a `response_wake` continuation. The status decision commits as `governed_response_waiting`. 3. Stop the server after the status commit and before run completion presents the reply. 4. Start the server. Presentation recovery runs. 5. See that the chat shows no reply. ## What Changed - `server/src/services/native-runtime/native-governance.ts`: add `conversationTurnPresentsAnswer()`. It is the one rule for whether a successful conversation turn's prose is its answer: `conversation_turn_finished`, or a committed decision with no pending card of the turn (`conversationTurnAnswered()`). - `server/src/services/heartbeat/run-completion.ts`: the live path calls the shared helper. No change in behavior. - `server/src/services/native-runtime/native-run-finalizer.ts` (`recoverCommittedNativeChatResponse`): - A current conversation turn no longer needs `conversation_turn_finished`. The turn must still be the unsettled current turn: same agent, user, session generation, `conversationState: active`, and no other execution run. A `conversation_turn_finished` turn still needs `in_progress`. - A conversation turn with a `yielded` result is accepted next to `done`. - The resolver gets `conversationTurnFinished` from the shared helper. - When the turn's own card presents it, recovery records `presentationDecision: none` (reason `committed_conversation_interaction_presented`) and settles the conversation turn, like the live path. The run then leaves the pending presentation set. Before, such a run stayed a candidate. - `server/src/services/native-runtime/native-response-presentation-query.ts`: the candidate prefilter matches the same turns: any committed reason for an active conversation turn, and `yielded` conversation results. - `server/src/services/native-runtime/agent-chat-response-recovery.test.ts`: restart regressions with the real finalizer and a real database. - `doc/execution-semantics.md`: state that recovery applies the same rule. ## Verification - Failing first. With only the new tests applied, 5 of 7 new cases failed: pending approval, revision-requested approval, execution stage (all yielded), pending approval with `done`, and the own-card case. The run was a candidate, but recovery posted nothing and did not settle it. - After the fix: `cd server && npx vitest run src/services/native-runtime/agent-chat-response-recovery.test.ts src/services/heartbeat/run-completion.test.ts src/__tests__/idle-native-finalization.test.ts src/services/presentation-ownership-read.test.ts src/services/chat-monitor-continuation.test.ts src/services/native-runtime/native-preserved-monitor-wait.test.ts src/services/native-runtime/native-response-publication-retry.test.ts src/services/heartbeat/run-lifecycle.test.ts src/__tests__/scoped-deadline-work.test.ts src/services/native-runtime/paperclip-control-plane-port.test.ts src/__tests__/heartbeat-run-summary.test.ts src/__tests__/agent-conversations.test.ts`: 12 files, 627 tests passed. - The new cases check: - Held turns: the run is a candidate, the check posts the exact final answer once, the presentation decision is recorded, the run leaves the candidate set, and a second recovery returns false with one comment. - Live already presented: no candidate, no second comment. - Own question card: no comment, `presentationDecision: none`, the card stays pending, the conversation settles to `waiting`, and the run leaves the candidate set. - Previous-release live decision `none` (`yielded_control_plane_wait`): kept, not re-presented. - `tsc --noEmit -p server`: no errors in changed files. Four unrelated errors remain in this local worktree (`esbuild` not linked in `native-harness-prestage.test.ts`, and a linked-package type mismatch in `run-dispatch/adapters/postgres.ts`). - Turn start: no change. The helper runs only in run completion (after the provider turn ends) and in background presentation recovery. Nothing new runs before `turn.start`, and no files move. The live path keeps the same queries: it short-circuits on `conversation_turn_finished`, and otherwise runs the same card read as before. ## Risks - Low to medium. Recovery can now post the answer of a conversation turn whose task is held. This matches the live path since #20179. - A card-presented turn found by recovery is now settled with `presentationDecision: none`. Before, it stayed a pending candidate and kept the presentation worker (and idle-sleep checks) busy. - A `conversation_turn_finished` turn keeps the `in_progress` check. Held turns accept any status their gate committed. The unsettled-turn checks (active conversation state, session generation, execution run) still block stale or superseded turns. - Previous-release state: no persisted shape changes. A run committed by the previous release with no saved presentation decision (the restart window) is recovered by the new code. The test fixture uses the unchanged finalizer, so it saves exactly the previous release's shape. A run whose live path already recorded a decision keeps it. A held turn that the previous release recorded as `yielded_control_plane_wait` is not re-presented, so no late duplicate or stale reply appears. - Not changed: generic stranded-work recovery in `recovery/service.ts` still skips only `conversation_turn_finished` runs during the publication window. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - Anthropic Claude Opus 5.5 (`claude-opus-5-5`), with tool use (shell, file edits, test runs) in Claude Code. ## 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 - [x] 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> |
||
|
|
bf67e2fbd9 |
test(chaos): honor 429 retry delays on harness reads (#20194)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - The restart chaos harness collects each wave's speed from the board API after the tasks rest, including the M1 subtasks' runs > - The server rate-limits concurrent issue-list requests per client and answers 429 with `retryAfterSeconds` > - After a 95-task storm, the harness requested every M1 parent's subtask list at once. The server returned 429, speed collection failed, and the wave exited 1 with no speed grade > - This pull request makes harness GET requests honor a 429's retry delay with a bounded number of attempts, and collects subtask speed one parent at a time > - The benefit is that a busy wave keeps its speed grade, and the harness behaves like a polite API client ## Linked Issues or Issue Description Refs #20071, #20147 **What happened?** Wave `w-20261109T055902-s9402` (K13 storm, A1/C1/M1, 95 tasks) finished its tasks. Speed collection then failed with: `GET /api/companies/…/issues?view=index&parentIds=…&limit=20 returned 429: {"error":"Too many concurrent issue-list requests for this actor/client","retryAfterSeconds":1}`. `main.ts` ran `childSpeedTasks()` for every observer in parallel. **Expected behavior** - The harness waits and retries a rate-limited read. - It does not flood the issue-list endpoint while collecting speed. - A busy wave still gets graded. ## What Changed - `api.ts`: `ChaosApi` retries a GET that returns 429. - It waits for the delay from `Retry-After` or the body's `retryAfterSeconds` (default 1 s, capped at 10 s), scaled by the attempt number. - It makes at most 5 attempts by default. - Writes (POST/PUT/PATCH/DELETE) are never retried, so a user action is never repeated. - `rateLimitDelayMs` is exported for tests. - `main.ts`: subtask speed is collected one observer at a time instead of with `Promise.all`. - New `api.test.ts` against a real local HTTP server: - a GET that gets 429 twice, then 200, succeeds; - a POST that gets 429 is sent once; - retries stop at the bound; - delay parsing (header, body, cap, default). ## Verification - `npx vitest run` in `tests/restart-chaos`: 216 passed. - `node scripts/typecheck-test-projects.mjs tests/restart-chaos`: clean. ## Risks - A persistently rate-limited read now takes up to about 15 s before it fails, instead of failing at once. - Collecting subtask speed one parent at a time adds a few seconds to the end of large M1 waves. ## Model Used - Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code execution in Claude Code. ## 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 - [x] 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> |
||
|
|
990de1e0bf |
perf(environments): release a stopped plugin task once (#20203)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Plugin environment drivers with `supportsTasks` run agents as provider tasks, and warm runs keep a parked task for the next run > - A follow-up run can find that its parked task already stopped. Core then retires that task before it acquires a new one > - Core sent `stop`, `status`, and `complete` in the settle step. Then the plugin release hook sent `stop`, `status`, and `complete` again for the same task. Every call was on the critical path of the new run > - This pull request removes the repeated calls and keeps every call that does work no other call does > - The benefit is a faster start for a run whose parked task already stopped ## Linked Issues or Issue Description No issue exists. This follows the enhancement template. **What existing behavior does this improve?** The release of a plugin task lease that has already stopped. This occurs when a follow-up run finds that its parked warm task stopped. **Subsystem affected** Server: plugin environment drivers and the plugin Runner task lifecycle. **Current behavior** Core sends `stop`, `status`, and `complete` in the settle step. Then the plugin release hook sends `stop`, `status`, and `complete` again for the same task. Core also sends `stop` after resume already saw that the task stopped. On a hosted test deployment, 8 of 40 follow-up runs took this path. From the resume to the new acquire, these runs spent P50 1014 ms and P90 1080 ms. These 8 runs are the full overall P90 (1275 ms). **Proposed behavior** Core does not call the release hook when settle confirmed the stop and completed the same task. After a resume with no lease, Core sends `status` first, and sends `stop` only when `status` does not confirm the stop. **Reason and benefit** The repeated calls do no new work. Removing them saves about 355 ms, plus the `stop` share of about 187 ms, for each such run. These numbers are modeled from trace timings. **Breaking changes** None for the plugin API. A task driver must release task-scoped resources in its `status` or `complete` handling. The doc now says this. **Additional context** The settle `status` stays. Providers release task storage there, and it records the stop that gates the credential writeback and `complete`. ## What Changed - `settleDispatchedEnvironmentTask` now returns `"completed"` when it confirmed the stop and completed the task in this call, `"stopped"` when the lease was already settled, and `false` otherwise. - The plugin driver release does not call `environmentReleaseLease` when settle returned `"completed"`. In all other cases it calls the hook as before. - When `environmentResumeLease` returns no provider lease, the release sends `status` first. It sends `stop` (and a second `status`) only when that `status` does not confirm `executionStopped: true`. Startup and startup-cleanup leases keep the old `stop` first order. - The cleanup sweep (`retryPendingSandboxTeardown`) and `destroyRunLease` do not change. A settle failure still leaves the lease in `pending_cleanup`, and the sweep runs `stop`, `status`, `complete`, and the release hook. - `doc/plugins/PLUGIN_RUNNER_EXECUTION.md` tells drivers about the new order and that task-scoped resources must be released in `status` or `complete`. ### Dispatch count for a stopped parked task (resume to new acquire) | | Calls | | --- | --- | | Before | resume, stop, status, (credentials), complete, release hook (stop, status, complete), acquire: 9 provider calls (10 with Codex writeback) | | After | resume, status, (credentials), complete, acquire: 4 provider calls (5 with Codex writeback) | The modeled saving is about 540 ms for each such run: about 355 ms for the repeated release, and about 187 ms for the settle `stop` and `status`. This pull request keeps the settle `status`, so the real saving is about 355 ms plus the `stop` share of the 187 ms. These numbers are modeled from trace timings. They are not measured on this branch. ### What covers each removed call - Release hook `stop`, `status`, `complete`: settle already sent `stop` and `status` and checked `executionStopped: true` for the same task id (`lease.providerLeaseId`), then sent `complete`. The skip uses settle's success as its condition, not the code path. - Settle `stop` after a resume with no lease: the next `status` must report `executionStopped: true`. If it does not, Core sends `stop` and `status` as before. ### What each kept call protects - Settle `status`: providers release task storage and cancel preparations here. It also writes `runnerTaskExecutionStopped`, which the credential writeback and the `complete` bookkeeping need. - Credential writeback: Codex can rotate its token. The writeback must land before the new task installs credentials. It still runs inside `complete`, before the new acquire. - Settle `complete`: it retires every run lease that owned the task and clears the dispatch marker. - Release hook when settle did not complete a dispatched task in this call: the provider may still hold the lease. ## Verification - `pnpm --filter @paperclipai/shared --filter @paperclipai/plugin-sdk build` - `pnpm --filter @paperclipai/server typecheck` - `npx vitest run src/__tests__/plugin-warm-tasks.integration.test.ts src/__tests__/plugin-task-credential-writeback.test.ts src/__tests__/environment-task-runtime.test.ts` - Related suites: `environment-runtime`, `plugin-runner-task.integration`, `task-creation-recovery.integration`, `heartbeat-plugin-environment`, `native-startup-cleanup-recovery`, `plugin-environment-driver-ready-recovery`, `environment-task-authority.integration`, `native-workspace-warm-reuse`, `remote-workspaces.integration`, `legacy-workspace-restore-recovery`, `plugin-task-source-recovery.integration`, `managed-plugin-environments`. Locally, all of these pass except `heartbeat-plugin-environment` "inherits the instance default environment across companies…". That test also fails locally with the `origin/master` service files, so this change does not cause it. The existing `plugin-runner-task.integration` same-run replacement test now expects no release hook after a completed settle. New tests: - The stopped parked path sends exactly resume, `status`, `complete`, acquire. The `status` goes to the old task. - A resume with no lease for a task that still runs sends `status`, `stop`, `status`, `complete`. - A failure at `status`, `complete`, or `stop` leaves the lease in `pending_cleanup`, and the sweep then sends `stop`, `status`, `complete`, and the release hook. - With an observed stop, the Codex writeback runs before `complete`. A writeback failure leaves the lease for cleanup, and the retry writes back and completes. - A startup-cleanup lease still sends `stop` first. ## Risks - A third-party task driver that releases resources only in `onEnvironmentReleaseLease` would not get that call after a completed settle. The doc now states the requirement. The cleanup sweep still calls the hook after any failure. - A driver that returns no lease from resume while the task still runs costs one extra `status`. The behavior stays correct. - No migration. No API change for plugins. ## Model Used - Claude Opus 5.5 (`claude-opus-5-5`), via Claude Code with tool use and code execution. ## 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 - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] 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> |
||
|
|
9046884811 |
perf(native-runtime): claim a parked Runner while the follow-up run prepares (#20195)
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - Paperclip Runner executes native agent runs. A host can park an idle
Runner (release it and freeze it) between runs, and resume it for the
next run on the same task.
> - A follow-up run that reuses a parked Runner must claim it first. The
claim is a bootstrap ticket, a checkpoint flush, and the plugin resume
that thaws the Runner.
> - Core made the claim only inside `attachRun`, after all of its
per-run preparation. The claim does not depend on that preparation, so
the two ran in series.
> - This pull request starts the claim while Core still prepares the
run. `attachRun` then waits for the claim that is already in progress.
> - The benefit is a shorter start for every follow-up run on a parked
Runner. The model below gives about 285 ms per run at P50.
## Linked Issues or Issue Description
No public issue exists. The issue is described inline.
**What happened?**
A follow-up run that reuses a parked Runner claims the Runner only in
`attachRun`. Core calls `attachRun` after backend setup: runner
environment, assigned MCP tools, tool authority and the plugin task
owner. The claim waits for all of this, but it uses none of it.
**Expected behavior**
The claim runs at the same time as backend setup. Everything that needs
the new run's tool authority stays at attach.
**Steps to reproduce**
1. Run a Codex or Claude task on a host that parks Runners between runs.
2. Let the run finish and the Runner park.
3. Send a follow-up. Before this change, the plugin resume starts only
after backend setup ends.
**Paperclip version or commit**
`master` at
|
||
|
|
b4a77b8915 |
perf(plugins): read runtime identity in one statement without a row lock (#20192)
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - Lifecycle plugins read the agent runtime identity through the plugin
host
> - The identity read used the full-state path. That path locks the
agent row and runs 11 statements
> - Plugins call the identity read many times as a freshness fence, so
each round trip adds to the run latency
> - This pull request makes the identity read one statement on the
request pool, without a row lock, and loads instance secrets with their
versions in one statement
> - The benefit is fewer round trips per read (11 to 4) and no wait
behind writers that hold the agent row
## Linked Issues or Issue Description
**What existing behavior does this improve?**
The plugin host read of the agent runtime identity
(`pluginAgentRuntimeIdentity`).
**Subsystem affected**
Server: plugin agent runtime state and instance secrets
(`server/src/services/plugin-agent-runtime-state.ts`,
`server/src/services/secrets.ts`).
**Current behavior**
The identity read shares the full-state transaction. It opens a
transaction on the file-storage pool and locks the agent row with `FOR
UPDATE`. It reads the plugin, the company settings and the latest
lifecycle event in separate statements. Then it reloads each instance
secret and its version one at a time. One read costs 11 statements. It
also waits behind any writer that holds the agent row, for example
managed file publication.
**Proposed behavior**
The identity read is one statement on the request pool. The statement
joins the plugin and the company settings, and reads the latest
lifecycle event id in a subquery. The agent fields, the authorization
inputs and the revision come from one snapshot. Instance secrets load
together with their latest versions in one statement. One read costs 4
statements.
**Reason and benefit**
A resumed agent run makes about 7 identity reads on its critical path in
a hosted deployment. With a 4 ms round trip, the modeled saving is about
330 ms per run at P50.
## What Changed
- `pluginAgentRuntimeIdentity` reads the agent, plugin, company settings
and latest lifecycle event id in one statement on the request pool. It
does not take a row lock.
- The authorization check moves into `assertOwningLifecyclePlugin`. The
identity path and the full-state path both use it, so both fail with the
same forbidden error.
- A missing agent still returns `null` before authorization runs.
- The full-state path (`readState`) keeps its transaction and its `FOR
UPDATE` lock. It no longer has an identity-only branch.
- `instanceSecrets.read` loads each secret with its latest version
through a left join in one statement. `resolveSecretValueInternal`
accepts these preloaded rows and skips the reload when the secret and
the version match. It still reloads when they do not match.
- The two audit writes per resolved secret run concurrently, as before.
- Tests:
- `agent-runtime-hooks.test.ts` compares the identity read with the full
read across revisions, lifecycle changes and secret changes, and across
every forbidden case. It confirms that forbidden reads record no secret
access. It confirms that the identity read does not wait for a held
agent row lock.
- `agent-instance-secrets.test.ts` confirms that a read of rotated
secrets uses one select and returns the latest values. It confirms that
each read records an access event. It confirms that a revoked version
fails with `version_inactive` and a missing version fails with
`version_missing`.
### Why dropping the lock is safe
The lock exists for the full-state path. That path projects instruction
files under the agent lock. The identity path does no projection. It
also released the lock before it returned, so no caller could rely on
the lock.
### Unchanged behavior
- Authorization: the same plugin, settings, capability and
required-plugin checks, with the same forbidden error.
- Decryption: the same provider resolution from the version material.
- Scope checks and version status checks: the same checks on the same
rows.
- Audit: the same `lastResolvedAt` update and access event for each
resolved secret.
## Verification
- `pnpm --filter @paperclipai/shared --filter @paperclipai/plugin-sdk
build`
- `pnpm --dir server run typecheck`: pass.
- Targeted tests: pass (6 files, 174 tests).
```
pnpm --dir server exec vitest run \
src/__tests__/agent-runtime-hooks.test.ts \
src/__tests__/agent-instance-secrets.test.ts \
src/__tests__/project-instance-secrets.test.ts \
src/__tests__/secrets-service.test.ts \
src/__tests__/agent-runtime-assignments.test.ts \
src/__tests__/agent-runtime-skills.test.ts
```
- CI runs the full suite.
### Benchmark
A local benchmark ran against the embedded test Postgres. A delay proxy
simulated network round trips.
| Condition | Before | After |
|---|---|---|
| Statements per read | 11 | 4 |
| P50, no added delay | 2.38 ms | 1.47 ms |
| P50, 4 ms round trip | 104.0 ms | 32.2 ms |
| P50, 4 ms round trip, prepared statements | 51.9 ms | 16.3 ms |
The new read costs about 0.3× the old read in round trips. In a hosted
deployment, a resumed agent run makes about 7 of these reads on its
critical path. The modeled saving is about 330 ms per run at P50.
## Risks
- Low risk.
- The identity read no longer runs in the same snapshot as a concurrent
writer that holds the agent row. It can return the state from before
that write commits. The `revision` field still comes from the same
snapshot as the agent fields, so callers that use it as a freshness
fence see a consistent pair.
- The identity read now runs on the request pool, not the file-storage
pool. It adds one short query to the request pool and removes one
transaction from the file-storage pool.
- The preloaded secret path applies only when the secret and the version
match the rows from the join. Any other case falls back to the existing
reload.
- No migration. No API change.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, through Claude Code
with tool use and code execution.
## 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
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] 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>
|
||
|
|
3964ee1fca |
fix(native): a governance pause Stop with no turn result stays a cancellation (#20186)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - A budget hard stop pauses the company; the pause's lifecycle Stop cancels the agents' running native turns > - When a Codex turn is interrupted this way, it ends without a semantic result > - The native executor treated that missing result as a retryable interruption, so it rewrote the cancelled run to `failed` and scheduled an exact resume > - The pause held that recovery, and Raise budget and resume continues only runs that are still cancelled, so the task stayed blocked with no new run > - This pull request makes the executor treat an acknowledged governance pause Stop like a person's Stop: the run stays cancelled > - The benefit is that Raise budget and resume continues the task (#20144), with no manual Retry ## Linked Issues or Issue Description Refs #20144 (resume continues budget-stopped work), #20143 (governance pause stops are not failures), #20113 **What happened?** Restart-chaos wave w-20261109T051242-s9802 (H1, seed 9802), task t03 (Codex, fault K2). The company budget crossed its hard limit at 05:19:39.9. The pause's lifecycle Stop ("Agent lifecycle stop requested") cancelled run 59e976bc at 05:19:40.05, and Codex acknowledged the interrupt. The turn ended without a semantic result, and at 05:19:41.27 the executor rewrote the run to `failed` / `native_session_interrupted`, with an agent-owned `active_run_watchdog` recovery (exact checkpoint resume). The task went to `blocked`. After `raise_budget_and_resume` at 05:20:20, no run started for 14 minutes. **Expected behavior** A pause's Stop is intentional. The run stays cancelled with its pause record, and resuming continues the task once. **Steps to reproduce** Pause a company for budget while a Codex native turn runs, so the Stop interrupts the turn before it records a result. Before this change, the run becomes `failed` with a scheduled exact resume instead of staying `cancelled`. ## What Changed - `native-session-executor.ts`: the no-semantic-result branch now also accepts an acknowledged governance pause Stop (`isGovernancePauseStop` with an acknowledged native Stop receipt) as an intentional stop. It settles the coordinator as `native_retry_cancelled` and raises `NativeCancellationPendingRecoveryError`, as it already does for a person's Stop and for a reassignment. The run is not rewritten to `failed`, and no recovery action is created. - The Stop's cancellation record is written by its fence before dispatch, so the check does not depend on whether the terminal write has landed. ## Verification - `native-session-executor.test.ts`: new cases for "Agent lifecycle stop requested" with a `pauseStop`, and for "Cancelled due to budget pause". Each was red before the fix (it reached the retryable-failure write) and is green now. A control case shows that a system lifecycle Stop without `pauseStop` (termination) still takes the retry path. - Full `native-session-executor.test.ts`: 1309 passed. Server typecheck (`typecheck-layers`, low-memory): no errors. - Not run: the H1 chaos rerun. Phase 3 runs it after merge. ## Risks - A governance pause Stop that lands during a turn no longer schedules an exact resume of that turn. The task continues on resume through a new Retry (#20144) instead. - Termination and cleanup use the same lifecycle Stop reason. They are excluded because only the pausing path records `pauseStop`. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with tool use (shell, file edits, test execution). Extended reasoning enabled. ## 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 - [x] 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> |
||
|
|
7c65a2f479 |
fix(authz): link plugin settings and say reply now on board-only denials (#20182)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Chat agents run with agent credentials, and plugin management routes are board-only > - A board-only denial tells the agent the denial is final and links the board page that holds the control, when the page is known (#20151) > - Plugin routes had no known page, so the denial said "link a page only if you already know its route" > - In a chat eval, an agent asked to finish configuring a plugin got this denial and kept searching the API for about two minutes, so it did not answer in time > - This pull request names the plugin manager, or that plugin's settings page, in the denial > - The benefit is that the agent can hand the step to the person with a working link right away ## Linked Issues or Issue Description Refs #20151 **What happened** An agent that called a board-only plugin route (for example `GET /api/plugins` or `POST /api/plugins/:pluginId/config`) got the generic remediation with no page. In the `plugins.configure-plugin` chat eval, the agent kept calling `search_api` for another plugin route after the 403 and did not reply within the 150-second window. **Expected behavior** The denial names the page where a board user does the step: the plugin manager (`/<prefix>/company/settings/instance/plugins`) or the plugin's settings page (`/<prefix>/company/settings/instance/plugins/<id or key>`). No link is given when the operator hides plugin management (`PAPERCLIP_HIDDEN_SETTINGS=instance.plugins`). **Steps to reproduce** 1. Install a plugin. 2. As an agent, call `POST /api/plugins/<pluginId>/config`. 3. See a 403 `board_access_required` remediation without a plugin page. ## What Changed - `server/src/routes/authz.ts`: board-only denials on `/api/plugins` routes link the plugin manager, or the plugin's settings page when the path names a plugin UUID or key. The plugin settings page accepts either. No link when plugin management is hidden. - `server/src/routes/authz.ts`: every agent board-only denial now says to stop working on the denied step and reply now. With the plugin link alone, the eval agent still searched the API for two more minutes. - `server/src/routes/authz.ts`: any manifest-format plugin key (including dot-free keys such as `my-plugin`) gets a direct link; plugin manager endpoints (`examples`, `install`, `tools`, `ui-contributions`) link the plugin list. - `ui/src/pages/PluginSettings.tsx`: custom settings slots match by plugin key as well as plugin id, so a page opened by key shows the plugin's own settings. - `server/src/__tests__/board-access-denial.test.ts`: cases for list, install, config by id, disable by key, a dot-free key, manager endpoints, and the hidden-settings case. `ui/src/pages/PluginSettings.test.tsx`: opening the page by key mounts that plugin's settings slot. - Merged `master`, keeping its project access-members denial cases. ## Verification - `cd server && npx vitest run src/__tests__/board-access-denial.test.ts src/__tests__/agent-permissions-routes.test.ts src/__tests__/environment-routes.test.ts src/__tests__/execution-workspace-runtime-lease-route.test.ts`: 241 passed after merging master. - `cd ui && npx vitest run src/pages/PluginSettings.test.tsx`: 9 passed; the new key-route case fails without the `PluginSettings` change. - `cd server && pnpm typecheck`: passes. ## Risks - Low risk. Only the remediation text of agent board-only denials on `/api/plugins` routes changes. Board users and authorization do not change. ## Model Used - Anthropic Claude Opus 5.5 (`claude-opus-5-5`), tool use and code execution in Claude Code. ## 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 - [x] 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> |
||
|
|
492316e576 |
fix(plugins): accept secret_ref bindings in string secret-ref config fields (#20181)
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - Plugins declare their settings in an `instanceConfigSchema`, and the
server validates each save against that schema
> - Plugin credential fields use `type: "string", format: "secret-ref"`
> - The settings form, the plugin secrets handler and the plugin SDK all
use the `{ type: "secret_ref", secretId, version }` binding object for
these fields, and the secrets handler rejects a bare secret id
> - Schema validation ran on the raw config, so the binding object
failed with "must be string" and no value could be saved
> - This pull request validates the binding's secret id in place of the
binding object
> - The benefit is that an operator can bind a company secret to a
plugin credential field from the settings page or the API
## Linked Issues or Issue Description
**What happened**
`POST /api/plugins/:pluginId/config` returned 400 "Configuration does
not match the plugin's instanceConfigSchema" with
`{"field":"/apiKey","message":"must be string"}` when the config held `{
"apiKey": { "type": "secret_ref", "secretId": "<uuid>", "version":
"latest" } }` for a field declared `{ "type": "string", "format":
"secret-ref" }`. A bare UUID string passes the schema but the secrets
handler then rejects it ("must use { type: \"secret_ref\", secretId,
version? }"). So no value could be saved for a required secret-ref
field.
**Expected behavior**
The binding object that the settings form submits and that
`ctx.secrets.resolve()` accepts is saved. Other schema rules still
apply.
**Steps to reproduce**
1. Install a plugin whose manifest has
`instanceConfigSchema.properties.apiKey = { "type": "string", "format":
"secret-ref" }`.
2. Create a company secret.
3. On the plugin settings page, pick that secret for the field and click
Save Configuration (or POST the binding object to
`/api/plugins/:pluginId/config`).
4. See the 400 "must be string" response.
Found while getting the chat eval cases `plugins.configure-plugin` and
`plugins.disable-plugin` to run. Their setup and the human's
settings-page save both hit this error.
## What Changed
- `validateInstanceConfig` now replaces each valid `secret_ref` binding
object at a schema `format: "secret-ref"` path with its secret id before
Ajv validates. The stored config and the config sent to the worker do
not change.
- The walk follows array `items` (single and tuple) and objects inside
arrays as well as object properties and `allOf`/`anyOf`/`oneOf`, so
secret pickers inside array fields can save too.
- A binding is accepted only when `envBindingSecretRefSchema` (the
secrets handler's own parser) accepts it, so validation and the secrets
handler agree on what is a valid binding.
- Malformed bindings and objects in ordinary string fields still fail
validation.
- Added `server/src/__tests__/plugin-config-validator.test.ts`.
## Verification
- `cd server && npx vitest run
src/__tests__/plugin-config-validator.test.ts
src/__tests__/plugin-routes-authz.test.ts
src/__tests__/plugin-sandbox-provider-config-validation.test.ts`: 48
passed.
- The new test fails on `master` without the fix (3 of 3 fail) and
passes with it.
- `cd server && npx vitest run
src/__tests__/plugin-config-validator.test.ts`: 6 passed after the
array/parser follow-up; the 3 follow-up tests fail on the previous
commit.
- `cd server && pnpm typecheck`: passes.
## Risks
- Low risk. Only configs that held a valid binding object at a declared
secret-ref path change result, from a 400 to accepted. The secrets
handler still checks that the secret belongs to the company.
## Model Used
- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), tool use and code
execution in Claude Code.
## 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
- [x] 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>
|
||
|
|
92a18bb0f3 |
perf(runner): restart the ACPX provider when a turn settles (#20190)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Paperclip Runner drives Claude through the ACPX sidecar. The sidecar's MCP endpoint lasts for the whole session, so it cannot tell which turn sent a late tool callback. > - Because of this, the Runner reaps the sidecar and provider of a generation that ran a turn before it releases call-ID tombstones and rotates receipt epochs. Today this restart runs at the start of the next turn. > - Every follow-up turn in a run pays that restart on its critical path: about 1.5 s with real Claude. #20145 moves the restart to park, but a second turn in the same run, after the first turn that follows an attach, still restarts cold. > - The restart does not have to wait for the next turn. When a turn settles and the host has its terminal result, the Runner is idle. > - This pull request restarts the provider at that point. The next turn, or the next park, then finds a fresh generation. > - The benefit: same-run follow-up turns start without a restart, park does no restart after a settled turn, and the late-callback guarantee is unchanged. ## Linked Issues or Issue Description Refs #20145, which moved the restart to park. This pull request builds on it. No public issue exists, so the problem is described here. **Subsystem affected** Paperclip Runner, ACPX provider (Claude). **Current behavior** `AcpxProviderSession::start_turn_timed` restarts the provider before any turn whose generation already ran a turn. A follow-up turn in the same run waits for `session.suspend`, sidecar shutdown, a new sidecar, `initialize`, `session.open` and `run.attach` before `turn.start`. Since #20145, park does the restart instead, but only for the first turn after an attach. **Proposed behavior** When a turn settles and the controller has ACKed every committed event, the Runner restarts the provider. The next `turn.start` uses the fresh generation and only rotates receipts. Park reuses the fresh generation and does no second restart. **Reason and benefit** The restart leaves the turn's critical path, both for same-run follow-ups and for the turns after a park. The order of reap and rotation stays the same, so a late callback from the reaped generation still cannot reach a later turn. ## What Changed - **Runner loop** (`durable/runner.rs`): new `CommandExecutor::prepare_next_turn` hook, a no-op by default. The runner calls it after the idle provider poll, only when no control frame is waiting and the controller has ACKed every committed event (`acked_source_seq >= highest_source_seq`). So the restart always runs after the terminal result reaches the host. - **ACPX turn-end restart** (`acpx_park_preparation.rs`, `acpx_provider_generation.rs`): - `restart_session_after_turn` restarts a Claude session that has a completed turn, nothing pending, no buffered events, a dormant goal and its whole event tail ACKed. It first settles an abandoned park observation. If that settlement reads more events, it skips this round, and the next idle round restarts after those events are ACKed. - `AcpxProviderSession::restart_after_turn` does the same suspend → reap → start → bootstrap → identity check as the turn-start restart. It records the reaped process id in `restarted_after_turn_from`. This field is the explicit "fresh because of a turn-end restart" marker. - The session does not poll a fresh generation until its next turn or rebind, the same as a prepared one. Its startup output is published with the next turn, as today. Polling it early would add events after the host computed its park cut. - **Next turn** (`acpx_provider_session.rs`): `start_turn_timed` restarts only when the generation ran a turn. When `restarted_after_turn_from` is set, it does the post-restart receipt and ledger rotation without a second restart. `session.reconciled` reports the reaped process as `previousProcessId`. - **Park**: `prepare_next_generation` restarts (with the 20 s park deadline) only if no turn-end restart ran. Otherwise it marks the generation for `run.attach` rebind and returns `false`, so the runner does not ask for a second park proof. The park-time receipt rotation moves to the next turn start, where the turn-end path does it too. - **Restart failure**: the executor sets the same state that a failed turn-start restart leaves (`closed`, no session, provider exit confirmed). The next `turn.start` fails with the same `ACPX turn start failed closed: …` error that the turn-start restart gives today. - **Fake sidecar**: new `pending-input` fault, which opens an input request in a turn that then completes. ### Races | Race | Handling | | --- | --- | | A turn or steer arrives during the turn-end restart | The runner loop is single-threaded. The command waits in the socket until the restart returns, then starts on the fresh generation without a restart. Only one generation is live. | | A turn arrives before the hook runs (for example, in the same read as the ACK) | Control frames go first, so the turn restarts at its start, as today. The hook then restarts after that turn. | | Park arrives during the turn-end restart | Host control is read at the top of the loop, so park waits. Park then reuses the fresh generation. No event is published in between, so the host's park cut still matches. | | Park arrives before the hook runs | Park restarts with the 20 s deadline, as in #20145. The hook then finds a fresh generation and does nothing. | | Eviction, `runner.suspend` or retire after the restart | They reap the fresh generation through the ordinary paths. | | The restart fails | The session is `closed`. The next turn fails with the same error and the same durable state as a failed turn-start restart. Park returns conflict, as it does for a closed session today. | | Cancellation | `run.cancel`, `turn.interrupt` and `turn.stop` after the restart return `already_settled`, as after any settled turn. A cancellation that retires the runtime closes the session, so no restart runs. | | Interrupted, cancelled or failed turn | `completed_turn_observed` is false, so no turn-end restart runs. The next turn restarts at its start, as today. | | Pending permission or input requests at the terminal | The reducer ends them at the terminal, so the restart runs. A later `request.resolve` fails with `no active turn`, with or without the restart. | | Active session goal at the terminal | No restart, because the goal can keep working after its prompt. | ### Removed as redundant, and what stays - **Removed:** the park-time restart and dormant verify when a turn-end restart already made the generation fresh. The runner's `host_park_ready` proves dormancy just before `prepare_for_park`, and nothing runs in between. - **Removed:** the runner's second park proof in that case, because the provider did not change. - **Removed:** the park-time receipt and ledger rotation. The next turn start does it once, for both paths. - **Kept: the turn-start restart.** Without it, a turn queued before the hook, a turn after an interrupted or cancelled turn, a turn after an active goal, and any non-Claude ACPX turn would run in the generation that ran the previous turn. Tests cover the queued-turn and interrupted cases. - **Kept: the park-time restart.** Without it, a park that arrives before the hook would leave the next run to restart at attach. A test covers this. - **Kept: the settlement of an abandoned park observation.** Without it, the restart drops events that the old generation sent before the abandoned response. A test covers this, and it fails with the settlement removed. - **Kept: the goal-dormancy check.** Without it, the restart kills an active goal's autonomous work. A test covers this, and it fails with the check removed. - **Kept: the ACK gate.** Without it, the restart can run before the host has the terminal result. A runner test covers this. - **Kept: deferred polling of the fresh generation.** Without it, its startup event lands after the host computed its park cut, and the park conflicts. - **Kept: the kept failure message.** Without it, the next turn fails with `ACPX session is not open`, and the restart error is lost. ## Verification - `cargo test -p paperclip-runner-core --lib` and every `tests/acpx_*.rs`, `native_provider_backend` and `durable_recovery` suite: all pass (debug). - New tests (`tests/support/acpx_turn_end_restart.rs`, run from `acpx_park_preparation`) prove these points: - A settled turn reaps the old pid before it spawns the new one, and no predecessor is still live. A same-run follow-up then sends only `turn.start` and reports the reaped pid in `session.reconciled`. - Park after a turn-end restart sends no sidecar command and returns `false`. Attach rebinds the same pid. The run's second turn restarts at its end, and the third turn starts without a restart. - The reaped generation's late callback never reaches the next turn. - A turn that arrives before the hook restarts at its start. A park before the hook restarts once. - A failed turn-end restart gives the same next-turn error, retry error, durable state and process count as a failed turn-start restart. - Cancellation after the restart is `already_settled`. `runner.suspend` reaps the fresh generation. - An active goal skips the restart. An input request open at the terminal behaves the same with or without the restart. - `acpx_provider_turns`: a session-level turn-end restart drops the old generation's late `issues.delete` callback, which reuses the previous turn's call ID. The next turn then admits that call ID from the fresh provider. Interrupted and retired turns do not restart at turn end. - `acpx_park_readiness`: a turn-end-restarted generation is park-ready. An abandoned park observation tail is delivered before the restart. - `durable::runner`: the hook runs only after the terminal is ACKed and when control is idle. It never runs during ACK debt, a waiting control frame or a disconnect. - Server integration tests (`plugin-runner-release-resume`, `plugin-runner-eviction` and `plugin-runner-task-warm`) with this branch's Runner: 49 passed. The provider process counts do not change: each released run still adds one process start, now at turn end instead of at park. - `cargo fmt --check`: clean. - Fake-sidecar timing (release build, 7 runs, medians): | | before (`master` with #20145) | this PR | | --- | --- | --- | | same-run follow-up `turn.start` → `turn.started` | 51.5 ms | 22.0 ms | | second turn after an attach, `turn.start` → `turn.started` | 270.3 ms | 23.5 ms | | turn-end restart, after the terminal is ACKed | — | 29.0 ms | | park preparation after a settled turn | 34.1 ms | 0.03 ms | - Real Claude: not run for this pull request. With real Claude, the restart took about 1.5 s on the turn-start path, and this change takes it off that path. That number is not measured on this branch. ## Risks - A run that ends after one turn without a park now starts one extra sidecar that no turn uses. The Claude CLI itself starts only at the first prompt, so the cost is the sidecar's start and bootstrap. - The restart blocks the Runner loop for its length (about 1.5 s with real Claude), as the turn-start restart does today. It has no deadline, as before. A park that arrives during it waits, inside the host's 30 s park wait. - The fresh generation is not polled until its next turn. If it exits while idle, the next turn finds out at `turn.start` and fails closed. Park finds out at its observation. - The turn-end restart is for Claude only, the same as park preparation. Other ACPX agents keep restarting at turn start. - If the Runner crashes during the restart, the durable state still says `session_open`. Recovery is the same as for a crash while idle. ## Model Used - Claude Opus 5.5 (`claude-opus-5-5`), in Claude Code with tool use (shell, file edits, local test runs). ## 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 - [ ] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] 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> |
||
|
|
7d92b6a7cd |
fix(api): use a neutral catalog pointer in the call_api protocol refusal (#20191)
## Thinking Path
- Paperclip agents reach REST routes through the `call_api` tool;
protocol endpoints (MCP, OAuth, WebSocket) are refused there with
guidance.
- #20170 changed that refusal to tell the agent the endpoint's catalog
description says who can use it.
- Review on #20170 found that some protocol endpoints' descriptions (for
example the WebSocket entries) do not say who may call them, so the
refusal could send the agent to look for information that is not there.
- The fix (
|
||
|
|
7086019d4b |
fix(api-catalog): say Agent Chat is always on instead of offering a retired toggle (#20169)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Agents learn what the API can do from the API catalog at `/api/openapi.json` > - PR #19360 made Agent Chat standard. The `enableAgentChat` key is now retired: it always reads true, and the app ignores written values > - The catalog still lists `enableAgentChat` as an ordinary boolean on the experimental settings operations, with no note that it is retired > - An agent that was asked to "Enable Agent Chat on this instance" read the catalog and told the person to turn on a toggle in Instance Settings → Experimental. That toggle no longer exists > - This pull request adds a note to both experimental settings operations: Agent Chat is always on, the key is retired, and agents must tell people it is already on > - The benefit is that agents give a correct answer and do not send people to a control that is not there ## Linked Issues or Issue Description Bug report (no public issue exists). **What happened** An agent was asked "Enable Agent Chat on this instance." It read `PATCH /api/instance/settings/experimental` in the API catalog and found `enableAgentChat`. It replied that an admin must open Instance Settings → Experimental and turn on Enable Agent Chat. That toggle was removed in #19360, and Agent Chat is already on for everyone. **Expected behavior** The agent says that Agent Chat is already on (the person is using it), and that nothing must be enabled. **Steps to reproduce** 1. Open a chat with an agent on any current instance. 2. Send "Enable Agent Chat on this instance." 3. The agent reads the experimental settings operation in the API catalog and sends you to a settings toggle that does not exist. Refs #19360 ## What Changed - `server/src/routes/openapi.ts`: the GET and PATCH `/api/instance/settings/experimental` operations now say that Agent Chat is always on, that `enableAgentChat` is a retired compatibility key that always reads true and ignores written values, and that agents must tell people it is already on instead of sending them to a toggle. - `server/src/__tests__/openapi-routes.test.ts`: a regression test checks this note on both operations. - The note says Agent Chat "needs no setup" instead of "there is nothing to enable", so it does not match unrelated keyword searches. The runner API no-results test (`nothing-zzzzzzzzzz`) had matched the word "nothing". ## Verification - `cd server && npx vitest run src/__tests__/openapi-routes.test.ts src/services/native-runtime/runner-api.test.ts` passes (2108 tests), and the server `tsc --noEmit` passes. - The chat eval `instance-operations.instance-settings` found this defect. Its run log shows that the agent reasoned from the catalog schema ("The setting is `enableAgentChat` on PATCH /api/instance/settings/experimental"). ## Risks - Low risk. This changes only catalog description text. It does not change behavior, validation or the schema. ## Model Used - Anthropic Claude Opus 5.5 (`claude-opus-5-5`), tool use and code execution, used through Claude Code. ## 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 - [x] 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> |
||
|
|
b07399c241 |
fix(authz): link the project's access settings when an agent cannot share a project (#20176)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Some actions are board-only. When an agent calls one, the 403 denial tells it that the denial is final and, when known, links the board page that holds the control > - Project sharing (`/api/projects/:id/access-members`) had no known page, so the denial told agents to link a page only if they already knew its route > - An agent asked to "Let Riley see the acquisition project" got this denial. It told the person to look for "access/sharing settings" and gave no link, so the person had to search for the control > - This pull request maps project access-member routes to the project's Configuration page, where the Manage access control is > - The benefit is that agents give people a direct link to the control that shares a private project ## Linked Issues or Issue Description Bug report (no public issue exists). **What happened** An agent tried to add a person to a private project. `POST /api/projects/:id/access-members` returned 403 "Board access required" without a page link. The agent told the person to find the project's access or sharing settings in the web app and gave no link. **Expected behavior** The denial links the project's Configuration page (`/<prefix>/projects/<id>/configuration`), where Manage access is. The agent gives that link to the person. **Steps to reproduce** 1. Create a private project and give an agent access to it. 2. In chat, ask the agent to let another company member see the project. 3. The agent's access-member request is denied, and its reply has no link to the sharing control. Refs #20151 ## What Changed - `server/src/routes/authz.ts`: board-only denials for `/projects/:id/access-members` (and its member subpaths) link "the project's access settings" at `/<prefix>/projects/<id>/configuration`. The link uses the resolved route project ID, so short references such as `/api/projects/acquisition/access-members` also get the link. Without a resolved UUID, the denial falls back to the generic instruction. - `server/src/__tests__/board-access-denial.test.ts`: cases for add and remove access-member denials, plus a short-reference case that goes through a route param resolver. ## Verification - `cd server && npx vitest run src/__tests__/board-access-denial.test.ts` passes (17 tests). The short-reference test fails on the previous matcher. The server `tsc --noEmit` passes. - The chat eval `projects.share-project` found this gap: in one run the agent linked the project, and in the next run it gave no link. ## Risks - Low risk. This changes only the remediation text in an agent's 403 response. It does not change authorization. ## Model Used - Anthropic Claude Opus 5.5 (`claude-opus-5-5`), tool use and code execution, used through Claude Code. ## 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 - [x] 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> |
||
|
|
2fc0293586 |
fix(api): tell agents only the person can revoke assistant connections, and where (#20170)
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - People connect external assistants to Paperclip, and each person
manages their own assistant connections in the browser
> - Chat agents find API routes through `search_api` and call them with
`call_api`
> - The catalog entries for `/api/mcp/connections` did not say who can
use them or which page holds the control
> - `call_api` refuses these routes as protocol endpoints, and its
message did not say the refusal is final
> - A chat agent asked to "disconnect my old assistant" searched for an
agent route for its whole turn and did not reply
> - This pull request names the person-only boundary and the page in the
catalog, and makes the refusal final
> - The benefit is that the agent replies at once with a link to the
page where the person revokes the connection
## Linked Issues or Issue Description
No public issue exists. This is a bug report.
**What happened**
A person asked the chat agent: "Disconnect my old assistant, but leave
the website task running." The agent found `DELETE
/api/mcp/connections/{id}`. `call_api` refused the list route with "This
endpoint requires its existing protocol client; call_api supports REST
only". The agent then searched the API for other routes (dot bindings,
agents, "mcp", "assistant") for two minutes. The turn timed out with no
reply. The agent wrote in its reasoning that there "must be a supported
path".
**Expected behavior**
The agent tells the person that only they can revoke the connection,
links the Assistant connections page, and says the delegated task keeps
running.
**Steps to reproduce**
1. Connect two assistants to a company as one person. Let the first
assistant create a task for an agent.
2. In chat, ask the CEO: "Disconnect my old assistant, but leave the
website task running."
3. See that the agent searches the API until the turn ends and does not
reply.
## What Changed
- `server/src/routes/openapi.ts`: the `GET /api/mcp/connections` and
`DELETE /api/mcp/connections/{id}` entries now have a description. It
says only the signed-in person can list or revoke their assistant
connections, links `[Assistant
connections](/<prefix>/apps/assistant-connection)`, notes that the
connected assistant itself can revoke its own token through OAuth token
revocation, says not to search for another route, and says that tasks
the assistant already created keep running.
- `server/src/services/native-runtime/runner-api-client.ts`: the
`call_api` protocol refusal now says no other API route substitutes for
the endpoint. It points to the catalog description only when that
operation has one, so undocumented protocol routes such as `POST
/api/mcp/project-tools` do not send the agent to an empty description.
- `server/src/__tests__/assistant-connections-guidance.test.ts`: new
regression test. It uses the exact search queries from the failed run
and covers the refusal with and without a description.
- `server/src/__tests__/public-mcp.test.ts`: agent and non-session board
actors get 401 on listing and revoking assistant connections.
## Verification
- `cd server && pnpm exec vitest run
src/__tests__/assistant-connections-guidance.test.ts
src/__tests__/primary-agent-guidance.test.ts
src/__tests__/openapi-routes.test.ts
src/services/native-runtime/runner-api.test.ts` passes (2111 tests).
After the review fixes,
`src/__tests__/assistant-connections-guidance.test.ts
src/__tests__/public-mcp.test.ts
src/services/native-runtime/runner-api.test.ts` passes (2157 tests), and
the server `tsc --noEmit` passes.
- The new test fails on `master` without the change (2 failures) and
passes with it.
- The end-to-end check is the
`assistant-connections.revoke-personal-grant` chat eval with this commit
cherry-picked.
## Risks
- Low risk. The change is catalog text and one error message. No route,
permission or data behavior changes.
## Model Used
- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), with tool use and code
execution in Claude Code.
## 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
- [x] 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>
|
||
|
|
9b71d03215 |
fix: recover interrupted native preparations and unblock upgrades (#20177)
## Thinking Path > - Paperclip manages AI-agent companies and their work. > - A native run prepares its workspace before a runner PID or checkpoint exists. > - A server crash can interrupt that stage and leave its controller on another host. > - Recovery needs a durable owner proof, a delivered recovery wake, and the saved execution contract. > - The old paths missed controller lease registration and boot proof, skipped checkpoint-free preparations, and counted retained work as live upgrade blockers. > - This PR connects preparation to the existing controller lease and recovery paths, and separates retained work from the upgrade census. > - The result is bounded recovery without a second executor or loss of saved work. ## Linked Issues or Issue Description **What happened?** A native preparation interrupted by a server crash could remain running indefinitely with no runner PID or checkpoint. Native controller stamps did not wake the shared instance lease. That lease was disabled on volume-backed Railway services, and native takeover omitted the boot binding needed for its cross-host proof. Scoped recovery selected checkpoint-backed runs but skipped these preparations. The upgrade probe then counted the retained row as current execution and could block every later upgrade. **Expected behavior** Track the preparation's controller before a runner starts. Recover it through the normal classifier after proving its owner stopped or fencing that boot. Keep live or unprovable owners held. Deliver recovery wakes to preparations without checkpoints. Preserve unresolved historical work and report it separately from current execution during upgrade observations. **Steps to reproduce** 1. Save an observed native preparation with no runner process, result, provider-start evidence or checkpoint. 2. Replace its controller with a process in another PID space. Let its owner lease expire and send a scoped recovery wake. 3. Before this fix, recovery skips the checkpoint-free preparation or cannot prove its foreign controller stopped. Upgrade readiness continues to count that old row as a live blocker. Related work: #18037, #19651, #19868, and #20168. The foreign-PID guard from #19868 remains. The queued first-turn input fix from #20168 is included through the current base. ## What Changed - Register the existing instance lease when native execution and restart claims are written. Keep passive identity reads free of ownership effects. Keep that lease enabled on volume-backed Railway services unless explicitly disabled. - Preserve the existing PID/start-time proof within the same readable PID space. Bind foreign or unknown-space takeover to its saved boot and last write. A live lease or grace period keeps the run held with a scheduled check. Repeated evidence waits preserve their timestamp. - Include explicitly fenced, checkpoint-free preparations in the existing finite recovery candidate inventory. Normal launch, checkpoint, provider evidence and admission checks still decide whether work may resume. - Expose the existing board stop-confirmation operation for historical foreign-host owners. Bind confirmation to the exact run, boot, PID, process start and host. Reject changed ownership. - Report narrowly defined old, unowned preparations as `retainedBlockedRuns` outside the live upgrade census. Local executions, pending admission, drain state, truncation and positive checkpoint proof retain their barriers. - Update the execution contract and add PostgreSQL, real-route, race and preservation regressions. ## Verification - Genuine red/green regressions cover the old upgrade census, missing native lease registration, the volume-backed default, foreign-host confirmation, and scoped recovery skipping checkpoint-free preparation. Test bodies ran against real PostgreSQL before their corresponding production changes. - Combined verification before the final idle correction (head `055be6c84d9`): **459 passed, 22 skipped, 11 files passed**. Covers controller lease/identity, native restart classification and integration, host confirmation, heartbeat recovery, instance route authorization, upgrade readiness, and queued first-turn input. The focused command used `pnpm exec vitest run` with those 11 suites and `--maxWorkers=1 --no-file-parallelism`. The 22 skipped cases require the Rust debug fake-Codex fixture, which was not built locally; the real-process CI coverage must pass. - `pnpm --filter @paperclipai/server typecheck`: passed on head `055be6c84d9b058064daad0af71bb5d893ab9529`. - Actual route proof uses the real heartbeat service and PostgreSQL. Unauthorized access returns 403. The retained run and owner records remain unchanged. Recovery integration admits one bootstrap claim across two replacement controllers, keeps a live owner held, and preserves local execution. - `pnpm verify`: exit 2, requests broad CI because the execution contract changed. Full local suite not run. Current-head CI must supply broad coverage. - Local source checkout on macOS ARM64, Node v25.6.1, pnpm 9.15.4. Tests use isolated PostgreSQL. No live provider restart is claimed by those fixtures. - CI found an idle-query regression in the first version of the lease wiring. Two focused PostgreSQL regressions reproduced the misplaced identity-read notification and the missing claim notification. The fix moves registration to actual ownership writes. `env -u npm_execpath pnpm exec vitest run server/src/__tests__/maintenance-server-routes.test.ts --maxWorkers=1 --no-file-parallelism`: **17 passed**, including 90 real seconds and two qualified hourly windows with no extra idle queries. An initial local setup attempt inherited npm metadata and did not run tests; clearing that metadata selected the correct pnpm helper. Three focused controller/native suites also passed **168 tests, 22 fixture-gated skips**. Server typecheck passed after the correction on head `4a527326dcd5b3a9e286f0e4c3e761d3056af4d9`. - The older CI server shard also exposed a same-space graceful-restart regression. The new boot fallback had overridden local process-exit proof. Seven suspension cases and a focused unit regression reproduced it; the fix limits boot fallback to foreign or unknown PID spaces. Fresh verification on `acf1065d98150210c1e227c7f3b05a1eb489730b`: **251 passed, 22 fixture-gated skips, 4 files passed**, including all **40** graceful-restart suspension tests. Server typecheck passed. The local regression failed before this correction, and the same-space and foreign-space paths pass together after it. - Current-head CI completed successfully on `acf1065d98150210c1e227c7f3b05a1eb489730b` (run 42568109784). Greptile reviewed that exact head at 5/5 with no unresolved review threads. - Secret and private-data scans passed. No workflow, lockfile or migration changes. ## Risks - The instance lease now applies to volume-backed Railway services. Its existing 30-second renewal, quiescence, grace and self-fence behavior applies while work is held. Idle servers retain the existing no-query behavior. An explicit `PAPERCLIP_CONTROLLER_INSTANCE_LEASE=off` remains available. - A controller lease proves only the server owner's state. It does not prove an agent process stopped or authorize replay, resource release or volume removal. The existing classifier and provider/runner checks remain responsible for those decisions. - Previous-release records keep their shape and saved state. A missing boot lease stays unprovable and requires the scoped stop-confirmation path. A changed confirmation target is rejected. - The upgrade observation does not recover a held task by itself. Source tests do not qualify a hosted replacement. Deployment and the affected task's useful result must be verified separately. - This repairs recovery after a crash; it does not claim to identify or fix every source of heap exhaustion. ## Model Used OpenAI `gpt-6.1-sol`, xhigh reasoning effort, through Codex in T3 Code, with repository, shell, and GitHub tools. Context window size is not exposed by the runtime. ## 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 - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>canary/v2026.1109.0-canary.6 |
||
|
|
2d93413085 |
perf(runner): prepare the next Claude provider generation at park (#20145)
## Thinking Path
> - Paperclip runs agents through Paperclip Runner, and a host can park
an idle Runner between runs and resume it for the next run on the same
task.
> - For Claude (ACPX), a follow-up run on a parked Runner pays about
1.5–2.3 s at `run.attach` before the provider can start the turn. The
attachment closes the Claude sidecar and spawns a new one: initialize,
`session.open` with `session/load`, `run.attach`, then a dormant verify.
> - The respawn is required. The tool-bridge MCP endpoint lasts for the
whole session and can't tell which turn a late callback came from, so a
new turn must not run in a process that ran an earlier one.
> - Nothing requires the respawn to happen at the *start* of the next
run. A turn that ends 2 s later is a better experience than one that
starts 2 s later.
> - This pull request does the respawn while parking. The next run's
`run.attach` then only rebinds the run id on a fresh, never-used
generation.
> - Follow-up Claude runs on a parked Runner reach the provider about
1.5–2 s sooner, and the late-callback guarantees are unchanged.
## Linked Issues or Issue Description
No public issue exists. The issue is described inline.
### What happened?
On a hosted deployment with parked Runners, Claude follow-up runs
reached the provider about 1.8 s later than Codex follow-ups.
`runner.session.resume` medians were 5.2 s for Claude and 3.4 s for
Codex. The extra time is the ACPX respawn inside `run.attach`. Codex
keeps its process in place.
### Expected behavior
The respawn happens while the Runner parks, so the next run starts on a
prepared generation.
### Steps to reproduce
1. Run a Claude (ACPX) task on a host that parks Runners between runs.
2. Let the run finish and the Runner park.
3. Send a follow-up. `run.attach` closes the provider and starts,
initializes, opens and attaches a new sidecar before `turn.start`. The
Claude CLI itself starts at the first prompt on either path.
### Paperclip version or commit
`master` at
|
||
|
|
0ddc9f1ef1 |
test(chaos): count an M1 subtask stall only when its recovery needs the board (#20178)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - The M1 chaos oracle checks that a parent gets exactly one `issue_blocker_stalled` wake per stalled subtask (#20147) > - The product wakes a parent only for a stall that needs the board. The harness recorded a stall for any blocked subtask, whatever its latest recovery action > - In the Daytona M1 wave, a subtask blocked twice on agent- and system-owned recovery that resolved by itself. The harness counted two stalls and failed the parent for getting no wake, a false failure > - This pull request records a stall only when the subtask's latest recovery action needs the board, matching the product's definition > - The benefit is that `parent_woken_once_per_stall` fails only for a wake the product owed ## Linked Issues or Issue Description Refs #20147, #20098 **What happened?** In wave `w-20261109T041248-s9501`, t10's subtask 195b5fd7 was blocked twice. Action 56d036e0 was agent-owned (`native_session_interrupted`) and was cancelled. Action 53eb91b0 was system-owned (`native_continuation_requires_reconciliation`) and resolved with outcome cancelled. Neither needed the board, so the product correctly sent no stall wake. The harness still failed `parent_woken_once_per_stall` with 2 stalls and 0 wakes. **Expected behavior** A stall counts when the subtask's latest recovery action is: - open (active or escalated) and board-owned, or escalated; or - board-owned and resolved with outcome blocked, with the subtask still blocked. That is the definition the product's blocker-stall wake uses. ## What Changed - `observeChildStalls` reads the parent's `stalledDependencies` from `GET /api/issues/:id`. That is the product's own stalled-dependency view (`listStalledDependencies`, built with the same `readStalls` the blocker-stall wake uses). - Each `relation: "child"` entry is recorded as a stall (`stalledIssueId`, `recoveryAction.id`). The harness no longer infers stalls from `/recovery-actions`. That endpoint lists only open actions and selected holds, and can omit a board action resolved with outcome blocked. - Clicking the board Retry on a blocked subtask is unchanged: it still happens whenever the product offers Retry, and it still counts as a needed Retry. - Tests: - a stall the product lists is recorded; - for t10's two non-board recovery states, the product lists nothing, so no stall is recorded, and the subtask's Retry is still clicked. ## Verification - `npx vitest run` in `tests/restart-chaos`: 212 passed. - `node scripts/typecheck-test-projects.mjs tests/restart-chaos`: clean. ## Risks - The harness checks `stalledDependencies` every 10 s. A stall that appears and clears inside one interval is not recorded, so its wake goes unchecked. It is never reported as a false failure. ## Model Used - Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code execution in Claude Code. ## 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 - [x] 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> |
||
|
|
95bf90bd2c |
fix(chat): post a conversation turn's answer while an approval holds the task (#20179)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - People talk to agents in chat. The agent's final message becomes the chat reply. > - A chat turn can end while the task stays held. For example, the agent resubmits a hire approval and the approval is pending. > - The finalizer keeps the task in review for the approval (`governed_response_waiting`). That decision is not `conversation_turn_finished`. > - Run completion then treats the yielded turn as a control-plane wait. It records `presentationDecision: none` and posts no comment. > - The person never sees the agent's answer. The chat shows nothing after the request. > - This pull request counts a committed conversation turn as answered unless its own pending question or confirmation card presents it. > - The benefit is that the person always gets the reply, whatever state the task keeps. ## Linked Issues or Issue Description Refs #19971 (post the full final reply). That fix covers the reply text. This fix covers replies that were never posted. **What happened** In a chat, the person asked the CEO agent to change a proposed hire to a cheaper model and send it back for approval. The agent resubmitted the approval correctly. It called `paperclip_finish`, and Paperclip accepted the report with "task is still waiting for approval". The run succeeded and the task moved to `in_review`. The run event `run.presentation.resolved` recorded `commentAction: none` (reason `yielded_control_plane_wait` on master). The agent's final message never reached the chat. **Expected behavior** A chat turn's accepted final answer is always posted to the conversation. An approval, an execution stage or a dependency blocker can hold the task. It must not hide the reply. Only a question or confirmation card that the turn itself left pending presents the turn instead of its prose. **Steps to reproduce** 1. Open an Agent Chat with an agent. Link a hire approval in `revision_requested` to the chat task. 2. Ask the agent to change the hire and resubmit it. 3. The agent resubmits the approval and ends the turn with a yielded result and a response wake. 4. See that the chat shows no reply. The run is `succeeded` and its presentation decision is `none`. ## What Changed - `server/src/services/native-runtime/native-governance.ts`: move the pending-card query of `pendingNativeGovernance()` into `pendingNativeInteraction()`, with no change in behavior. Add `conversationTurnAnswered()`. It returns false only when a pending thread interaction (the same predicate as status arbitration) gates the turn. It checks cards on their own, so a pending execution stage does not hide a card. - `server/src/services/heartbeat/run-completion.ts`: a successful conversation run whose status decision committed now passes `conversationTurnFinished: true` to the presentation resolver, unless `conversationTurnAnswered()` is false. The existing `conversation_turn_finished` path is unchanged. - `server/src/services/heartbeat/run-completion.test.ts`: add a regression table with a real database. A pending approval, a revision-requested approval, a pending execution stage and a dependency blocker now post the answer. A finished turn still posts. The turn's own question card still owns the response, also when an execution stage is pending. - `doc/execution-semantics.md`: state the rule. Other finish outcomes I checked: - `done` or `needs_review` with a gate (`governed_gate_pending`): the result is not yielded, so the reply was already posted. - `blocked` (`paperclip_block`, waiting on a person or another task): not yielded, so the reply was already posted. - Yielded with a dependency blocker (`durable_dependency_blocker_bound`), or with `record_finalization_error` (`prior_status_preserved_no_live_path`, `monitor_wait_authority_lost`): the reply was also dropped. This fix covers them, because the rule does not depend on the reason code. ## Verification - `cd server && npx vitest run src/services/heartbeat/run-completion.test.ts`: 118 passed. Before the fix, 4 new cases failed (both approval states, execution stage, dependency blocker). The finished-turn and question-card cases passed both before and after. - `cd server && npx vitest run src/__tests__/heartbeat-run-summary.test.ts src/services/heartbeat/run-comment-selection.test.ts src/__tests__/slack-conversation-lifecycle.test.ts src/services/native-runtime/agent-chat-response-recovery.test.ts src/__tests__/chat-run-milestone-governed-work.test.ts src/__tests__/agent-conversations.test.ts`: all passed. - `tsc --noEmit -p server`: no errors in the changed files. Unrelated errors in this worktree come from an unbuilt local Runner package. - An end-to-end chat eval run on the case branch is in progress. I will add its result here. ## Risks - Low risk. The change only affects successful Agent Chat runs with a committed status decision. Ordinary tasks and external chat (Slack, GitHub) keep their own paths. External chat returns before this check. - A turn that yields while an approval is pending now posts its prose. The approval stays pending and the task status does not change. - One extra small read (the task row and pending gates) runs after a conversation turn that is not `conversation_turn_finished`. It runs after the provider turn ends, so it does not add time before the first token. - Previous-release state: no persisted shape changes. The fix reads `finalizationPhase` and `finalizationReasonCode`, which the previous release already writes. A run in flight at the deploy completes on the new code and posts its reply. A run that completed before the deploy keeps its recorded decision; nothing re-presents it. - Known gap, unchanged: background presentation recovery for conversations still requires `conversation_turn_finished` with a `done` result. If the server restarts between the status commit and the live presentation, a held yielded turn is not re-posted. That is a separate follow-up. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - Anthropic Claude Opus 5.5 (`claude-opus-5-5`), with tool use (shell, file edits, test runs) in Claude Code. ## 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 - [x] 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> |
||
|
|
c5c587f162 |
fix(issues): start a deferred children-completed wake the release drain missed (#20180)
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - A parent that delegates waits for its children, and is woken when
they finish (`issue_children_completed`)
> - Restart chaos t06 ended with both children done and the parent
blocked forever
> - The children finished while the parent's run was still going, so the
wake was deferred as a follow-up. That run then ended with the agent
setting the parent blocked, and the deferred wake was never started
> - The durable re-drivers for deferred wakes cover only
`issue_blockers_resolved`, so nothing else picks up a missed
`issue_children_completed`
> - This pull request makes both re-drivers cover children-completed
wakes too
> - The benefit is that a parent always hears that its children
finished, even when the release drain misses the deferred wake
## Linked Issues or Issue Description
Refs #19831 (durable ownership plan). Restart chaos wave
`w-20261109T041248-s9501`, task t06 (claude, M1, K2), Daytona per-turn,
master
|
||
|
|
5a51a7685d |
fix(budgets): raise_budget_and_resume continues the work its budget stopped, once (#20144)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - A budget hard stop cancels the scope's running work; the board resolves it with Raise budget and resume > - In the restart-chaos H1 scenario, the cancelled tasks did not continue after resume and each needed a manual Retry > - Resume is the board's explicit decision to continue, so the work it stopped should continue once, as if the board clicked Retry > - This pull request files that Retry automatically, keyed by the cancelled run, once the run's stop has settled > - The benefit is that a budget raise restores the work without manual Retries, without repeating completed work, and without charging the run's attempt budget ## Linked Issues or Issue Description Refs #20143 (bug B; this PR builds on it), #20113 (bug A), #20101 (superseded: split into #20143 and this PR) **What happened?** Restart-chaos waves w-20261108T195944-s42 and w-20261108T210118-s9802 (H1). After `raise_budget_and_resume`, CHA-3's budget-cancelled run (enforcement stop) and CHA-1's run (agent lifecycle pause from the budget hold) stayed cancelled until someone clicked Retry. **Expected behavior** Resuming continues each task its budget stopped, exactly once, with no attempt charged, an activity entry, and no repeat of work that already continued. A run a person stopped stays stopped. **Steps to reproduce** Set a budget below spend while a task runs; let the stop cancel the run; resolve with Raise budget and resume. Before this change, no new run is queued. ## What Changed - New table `budget_resume_claims` (migration 0451). One row per cancelled run, outside the run's result JSON, so a Stop that settles during the resume cannot erase it. - `markBudgetCancelledWorkForResume` (called by `resolveIncident` before the incident closes): claims every task run its budget stopped since the incident opened. That covers runs budget enforcement fenced, including stops still in flight, and runs the agent lifecycle pause stopped for this budget (`pauseStop`, carried from the lifecycle operation's `pauseCause` so a resume before the Stop keeps the link). Runs a person stopped are excluded. Two indexed passes led by `(company_id, status)`. - `deliverBudgetResumeContinuations`: once a claimed run's stop settles, files the resolving board member's Retry (`requestTaskRetry`, idempotency key `budget-resume:<run>`, with the member's session source and the chat Retry callback). The Retry path checks, under the task lock, that the run is still the task's latest and the task is open, so continued or completed work is never repeated. Claim settlement and its activity entry commit together; retryable refusals and errors retry. - Delivery runs in the always-on budget-enforcement worker after each pass's Stop delivery. Its schedule includes deliverable claims and the earliest stale-claim expiry. The execution-continuation worker is a backstop. - `agent.paused` / `agent.resumed` logging is in #20113. `budget.cancelled_work_resume_requested` (incident) and `budget.cancelled_work_continued` / `_not_continued` (task) are recorded here. ## Verification - New `budget-resume-continuation.test.ts` (Postgres, real `resolveIncident` → claims → `requestTaskRetry` → real `resumeTaskRetry` into a queued run): - each task continues once, including a blocked task; repeat delivery or resolve saves nothing new; - a stop still in flight at resume, whose terminal write replaces the result JSON, is still continued; - a 30-task scope is fully continued; - a lifecycle-pause stop caused by the budget is continued, and one caused by a manual pause is not; - continued work, done tasks, a run that finished before its stop, and person-stopped runs (including ones enforcement also fenced) are not continued. - `lifecycle.test.ts`: the pause records its cause on the operation and on stopped runs, including when the budget is resumed before the pause's Stop. `budget-enforcement-worker.test.ts`: delivery after Stop delivery, and a wake at claim expiry. - 287 tests across the affected suites passed. Server typecheck (`typecheck-layers`, low-memory): no errors. - Not run: the H1 chaos rerun (Phase 3 runs it after merge). ## Risks - Resume now continues budget-stopped tasks automatically. A board that wants a task stopped can Stop it again; person-stopped runs are never continued. - A new table and one index (migration 0451). > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with tool use (shell, file edits, test execution). Extended reasoning enabled. ## 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 - [x] 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> |
||
|
|
61fa13c6d8 |
test(chaos): add S1, a board Stop across a restart then a continue comment (#20161)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - The restart chaos harness drives real user actions across control-plane restarts: comments, Retry, approvals and budget resumes > - The board's Stop button on a running task was not exercised. The cancel route stamps the stop as operator-initiated so that recovery stands down instead of re-waking the agent > - A restart right after a Stop is where recovery could wrongly restart work the operator stopped, or lose the operator's later "continue" direction > - This pull request adds S1: stop the running run, fire a fault right after, then comment to continue > - The benefit is a graded check that a Stop holds across a restart and that a later continue comment finishes the task ## Linked Issues or Issue Description Refs #20071 **What is missing?** The campaign's scenario list includes mid-run stops. C1 covers a mid-run comment (a steer), but no workload clicks Stop. Without one, a regression where recovery restarts operator-stopped work, or drops the continue comment, would go unseen. **Expected behavior** - After the board stops a run and a restart follows, the stopped run stays cancelled. - Nothing runs until the operator comments. - The continue comment then finishes the task. ## What Changed - S1 workload ("Stop while running, then continue"): a brief task. At its follow-up time, while a run is running, the observer posts `POST /api/heartbeat-runs/:id/cancel` (the board's Stop) and records `stop`. - New `stop` trigger, for S1 only: the planned fault fires 0–2 s after the stop. - The continue comment is posted only after the stop has settled: at least 20 s after the stop, with no run running or queued and no fault in progress. It asks the agent to continue and add a `CONTINUE-<nonce>` line. - The oracle requires: - status done; - the brief with the chaos and continue markers; - `stop_took_effect`: the stopped run ended `cancelled`; - `nothing_ran_after_stop`: no run was created between the stop and the continue comment. - S1 is not a default workload. There is a README section. ## Verification - `npx vitest run` in `tests/restart-chaos`: 205 passed. New tests cover: - the S1 grade (pass, a run restarted after the stop, the stop not taking effect, no stop); - the trigger-to-workload pairing; - an observer test that clicks Stop once and posts the continue comment only after the 20 s settle. - `node scripts/typecheck-test-projects.mjs tests/restart-chaos`: clean. - A live S1 wave runs after the current chaos wave. Its result will be posted here before merge. ## Risks - If the product moves the stopped run to a status other than `cancelled`, `stop_took_effect` fails. That check is intentional. - The continue comment waits for the stop to settle, so a run stuck `queued` delays it. R5 and the wave deadline still bound the wait. ## Model Used - Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code execution in Claude Code. ## 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 - [x] 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> |
||
|
|
b54317d3fb |
fix(native): give a preparing run's first turn the user's queued messages instead of interrupting it (#20168)
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - A Paperclip Runner (native) run prepares its workspace and
environment before it starts its first provider turn. This can take
about a minute.
> - A user's message that arrives during that time waits as a deferred
wake behind the run. The Interrupt control then cancelled the run before
any provider turn.
> - The cancel threw away the whole preparation. A new run had to
prepare again, so the user waited longer for the first token.
> - This pull request gives the run's first turn the queued messages.
The run seals its first-turn input immediately before `turn.start`, so
the messages are part of the prompt that the turn starts with.
> - The benefit is that the user sees the message handled in the same
run, with no cancel and no second preparation.
## Linked Issues or Issue Description
**What happened?**
A Paperclip Runner run took about 54 s to prepare (environment startup,
workspace, runtime selection). The user sent a second message 32 s into
that time and clicked Interrupt 21 s later. Interrupt cancelled the run
as `operator_interrupted` ("Interrupted to send queued messages") before
any provider turn. A new run then prepared again from the start. The
runtime-selection transaction that froze the run's input completed about
5 s before the click, and the coordinator claim came 4 s after it, so
the message could not be given to the first turn at any point in the old
code.
**Expected behavior**
When a user message arrives for a task whose current run has not started
its first provider turn, the run does not stop. The message goes into
that run's first turn. A run that is in a turn is interrupted as before.
**Steps to reproduce**
1. Assign a task to a Paperclip Runner agent with a sandbox environment,
so preparation takes a long time.
2. While the run prepares, post a second comment on the task.
3. Click Interrupt on the queued message before the run starts its
provider turn.
4. See that the run is cancelled and a new run prepares again.
**Paperclip version or commit**
Staging revision
canary/v2026.1109.0-canary.5
|
||
|
|
32b3648758 |
fix(storage): back off and reduce parallel parts when S3 asks to slow down (#20165)
A workspace snapshot upload sends up to eight file-backed parts at once. The SDK never retries a part because its body is a stream, so a part that received 503 SlowDown had only the uploader's three fixed-delay attempts (0.5 s and 1 s). Under a request-rate limit the save failed and the run failed in setup. A rate-limited part now retries with exponential full-jitter backoff (up to eight attempts, at most 20 s per wait), and the upload halves the parts it admits, once per round of throttled requests. Other part failures keep the existing three attempts. Tests cover a real-client SlowDown recovery, the single halving, the attempt bound, cancellation during a wait, and the backoff schedule through its ceiling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
098f7d370b |
fix(workspaces): release an unchanged remote writer without a snapshot and wake its waiters (#20171)
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - A remote (sandbox) task workspace has one writer run at a time. The
writer holds the workspace until its files are saved as a snapshot
> - A run that a user comment interrupts right after its claim did no
work. No provider turn started and no command ran in the sandbox
> - Paperclip still kept its sandbox, then took a full snapshot of the
unchanged workspace. On a 2 GB repository this took 3 minutes
> - The next runs failed setup with `workspace_busy` ("ended, but its
workspace save has not settled"). The retry then waited for the
one-minute settling re-check after the save. The user's message waited
more than 4 minutes
> - This pull request releases such a writer at once, at the saved
version it started from, when durable evidence proves that it changed
nothing. It also wakes parked `workspace_busy` waiters when a hold
settles
> - The benefit is that an interrupted run no longer delays the next
message by a full workspace save
## Linked Issues or Issue Description
**What happened?**
A remote-workspace run was claimed (`native.coordinator.claim`). One
second later a board comment interrupted it (`operator_interrupted`). It
made no provider turn and no tool calls. Cancelled-workspace
preservation still retained its sandbox, and workspace recovery took a
full `remote_workspace_snapshot` of the unchanged workspace (3 minutes
for a 2 GB repository). Two follow-up runs were cancelled with
`workspace_busy`. Their retry was due during the save, and it parked for
the one-minute settling re-check after the save finished.
**Expected behavior**
A writer that provably changed nothing releases the workspace at once
and keeps its existing `snapshotId`. The next run is admitted without
`workspace_busy`. When a save does run, waiters start as soon as it
commits.
**Steps to reproduce**
1. Run a task on a remote sandbox workspace that already has a saved
version.
2. Interrupt the run with a board comment right after the claim, before
the provider turn.
3. See the full snapshot operation on the interrupted run and
`workspace_busy` on the follow-up runs.
**Paperclip version or commit**
Staging served revision
|
||
|
|
8e5959c759 |
fix(agents): keep pause board-only and resume grant-gated on the PATCH status route (#20164)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - The board governs the agent lifecycle: pause, resume and terminate > - The V1 permission matrix says agents may never pause an agent, and may resume one only with a direct `agents:configure` change grant > - `POST /api/agents/:id/pause` and `POST /api/agents/:id/resume` enforce this, but `PATCH /api/agents/:id` with a `status` ran the same lifecycle change after only the agent-configuration check > - The `agents.pause-agent` chat eval caught it: the CEO agent paused Morgan itself through `PATCH`, although the case (and the spec) expect a board user to do it > - This pull request applies the dedicated routes' checks to `PATCH` status changes, as #19955 did for termination > - The benefit is one lifecycle authority, with no second route around it ## Linked Issues or Issue Description Refs #19955 **What happened** An agent that may configure another agent could pause it with `PATCH /api/agents/{id}` `{ "status": "paused" }`, although `POST /api/agents/{id}/pause` is board-only. The same `PATCH` with `{ "status": "idle" }` resumed an agent without the change-grant decision and without the invalid-org-chain guard that `POST /resume` applies. In a chat eval run, the CEO agent paused Morgan this way and reported "Morgan is paused". **Expected behavior** Pause is board-only on every route (`doc/SPEC-implementation.md` section 9.3: "Pause/resume agent ... pause: no; resume: direct `agents:configure` grant only"; "Pause, clear-error, terminate, approval, and key-management routes remain board-only"). An agent gets the standard final "Board access required" denial with a link to the agent's profile. Resume through `PATCH` needs the same grant and org-chain checks as `POST /resume`. **Steps to reproduce** 1. As an agent with agent-configuration permission over another agent, call `POST /api/agents/{id}/pause`. The result is 403. 2. Call `PATCH /api/agents/{id}` with `{ "status": "paused" }`. Before this change the result is 200 and the agent is paused. ## What Changed - `server/src/routes/agents.ts` (`PATCH /agents/:id`): `status: "paused"` now calls `assertBoard`, the same check as `POST /pause`. Agents get the structured `board_access_required` denial that says the denial is final and links `/<prefix>/agents/<id>`. - `status: "idle"` now calls `assertCanResumeAgent` (the `agent_config:update` decision with `requiresChangeGrant`) and returns 409 with the repair guidance when the org chain is invalid, the same as `POST /resume`. - `server/src/__tests__/agent-permissions-routes.test.ts`: pause is denied through both routes for an agent allowed to configure the target, with no lifecycle change, run cancel or activity. A board user can still pause through `PATCH`. `PATCH` idle applies the change-grant decision, allows a granted agent, and keeps the org-chain guard. ## Verification - `cd server && pnpm exec vitest run src/__tests__/agent-permissions-routes.test.ts src/__tests__/agent-cross-tenant-authz-routes.test.ts src/__tests__/board-access-denial.test.ts`: 147 passed. - Without the route change, three new tests fail (PATCH pause by an agent, PATCH idle without the change grant, PATCH idle with an invalid org chain). - Policy evidence: `doc/SPEC-implementation.md` section 9.3 and the `agents.pause-agent` scenario ("Pausing an agent requires board control"). ## Risks - Non-board automation that paused agents through `PATCH` status now gets 403. That is the intended governance boundary. Board users are not affected. - Agents that resumed agents through `PATCH` status now need the same change grant as `POST /resume`. Standard agents get `agents:configure` on creation, so ordinary behavior does not change. - `PATCH` idle now returns 409 for an agent with an invalid org chain, the same as `POST /resume`. ## Model Used Anthropic Claude Opus 5.5 (`claude-opus-5-5`), via Claude Code, with tool use (shell, file 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 - [x] 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> |
||
|
|
f89813d005 |
fix(api-catalog): describe the real backup and restore paths for agents (#20159)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Instance admins and operators own database backups; a chat agent cannot create one > - When a person asks an agent to back up the instance and check a restore, the agent must say who can do it and how > - The agent reads the API catalog to find the operator path, but the backup entry had only the summary "Trigger a database backup" > - With no real path in the catalog, the agent told the person to use an "Instance settings > Backups" page that does not exist > - This pull request describes the real paths in that catalog entry: the endpoint, the CLI, the retention setting, the result file, and how to check a restore safely > - The benefit is that the agent sends the person to paths that exist and keeps a restore check off the live database ## Linked Issues or Issue Description **What happened** A person asked the CEO chat agent: "Back up this test instance and check that we can restore it." The agent got 403 on `POST /api/instance/database-backups`, which is correct. It searched the catalog for backup and restore operations and found only that endpoint with no description. Its reply then said: "Create a backup in Instance settings → Backups". The web UI has no such page or control. **Expected behavior** The agent says that an instance admin or operator must do this. It names real paths: the endpoint for an instance admin, `paperclipai db:backup` on the server host, and the Backup retention setting for automatic backups. For the restore check, it says to restore into a separate empty database, not over the live one. **Steps to reproduce** 1. Start a local instance and open a chat with the CEO agent. 2. Ask: "Back up this test instance and check that we can restore it." 3. Read the reply: it names a backup page in Instance settings. Look for it in the UI: there is none. ## What Changed - `server/src/routes/openapi.ts`: add a description to `POST /api/instance/database-backups`. It says that only instance admins can call it (agents get 403, cloud-managed instances run backups for you), that automatic backups run about hourly with retention set on the company-prefixed /company/settings page, that the UI has no create or restore control, that an operator can run `paperclipai db:backup`, what the result file is, and that a restore check uses psql into a separate empty database, never over the live one. - `server/src/routes/openapi.ts`: register the success status as `201`, which is what the route returns, instead of `200`. The published document already showed `201` through the created-operations fixup, so this makes the source match it. - `server/src/__tests__/database-backup-catalog-guidance.test.ts`: new regression test. It runs the agent's catalog searches from the failed run and checks the description. It also checks that the published document shows `201` and not `200` for a successful backup. ## Verification - `pnpm exec vitest run src/__tests__/database-backup-catalog-guidance.test.ts src/__tests__/openapi-routes.test.ts` (from `server/`): 40 tests pass. Without the change, the 2 search tests fail. The status test guards the published document, which the fixup already kept correct. - `pnpm exec vitest run src/__tests__/instance-database-backups-routes.test.ts`: 7 tests pass. The server typecheck passes. - Chat eval `instance-operations.backup-and-restore` (runner-opencode), with this commit cherry-picked onto the eval branch: before the change, the reply named an "Instance settings → Backups" page that does not exist. After the change (GitHub Actions run 42552777052), the eval passed. The first reply said the agent got 403, named `POST /api/instance/database-backups` for an instance admin and `paperclipai db:backup --json` for an operator, linked Backup retention on the company settings page, and said to restore with psql into a separate, empty database, never over the live one. The second reply reported the operator's restore receipt and its limits correctly. ## Risks - Low risk. The change adds catalog text and makes the registered success status match the published 201. It does not change any API or permission behavior. - If a backup page or restore command is added later, this text must change. The regression test names these facts, so such a change fails the test and shows the text to update. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - Anthropic Claude Opus 5.5 (`claude-opus-5-5`), tool use and code execution in Claude Code. ## 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 - [x] 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> |
||
|
|
3f12046576 |
fix(tool-access): an unscoped tools:use grant does not turn on actions a connection left off (#20160)
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - People connect apps and choose which actions of each app agents may
call; the choice is saved as the connection's tool profile
> - Every new standard agent also gets an unscoped `tools:use` grant,
which means "this agent may use tools"
> - The access policy checked explicit grants before profiles, so this
default grant allowed every action of every installed connection,
including actions the person left off
> - The agent's effective profile showed the action off, but the gateway
and the test-as-agent summary allowed it
> - This pull request makes an unscoped grant stop at a connection
profile: when a profile governs the tool's connection, only a grant
scoped to the action, connection or application allows an action the
profile leaves off
> - The benefit is that the actions a person turns off for a connection
stay off for new agents
## Linked Issues or Issue Description
**What happened**
A person connected an app with one read action and one destructive write
action. When they finished the connection, they enabled only the read
action. They installed the connection for a new agent, Morgan. Morgan's
effective profile (`GET
/api/companies/{companyId}/tools/profiles/effective/agents/{agentId}`)
allowed only the read action. But `GET
/api/tool-connections/{connectionId}/test-agents/{agentId}/access`
reported both actions as `allowed` with reason `allow_explicit_grant`,
and a chat agent told the person that Morgan could also run the
destructive write. The same policy decision admits gateway calls, so
Morgan could call the write.
**Expected behavior**
An action that the connection's profile leaves off is off for the agent.
The access summary, the effective profile and the gateway agree. A grant
that a person scopes to that action, connection or application still
allows it.
**Steps to reproduce**
1. Connect an MCP app that has a read tool and a destructive tool. In
the finish step, enable only the read tool.
2. Create a new agent through the API (it gets the default unscoped
`tools:use` grant) and install the connection for it.
3. Call `GET
/api/tool-connections/{connectionId}/test-agents/{agentId}/access`: the
destructive tool shows `allowed`, reason `allow_explicit_grant`.
## What Changed
- `server/src/services/tool-access-policy.ts`: for an agent, a tool's
connection is governed by a connection profile when the agent's
effective profiles include the connection's own access profile (the app
wizard's `app:<connectionId>` profile or the agent's connection-intent
profile, even with no entries), or any entry that names this connection
or selects this action (including application, tool-name and risk-level
selectors). For a governed connection, `explicitGrant` counts only
grants whose scope names an action, connection or application (`allow`,
`toolName`, `catalogEntryId`, `connectionId`, `applicationId`,
`applicationKey`). A grant scoped only by context (for example an agent
or project) still covers every tool, so it does not override the
profile. Board users keep their operator grant.
- `server/src/services/tool-profile-binding-precedence.ts`: export the
existing connection-profile identity check as
`isConnectionAccessProfile`, so the policy and binding precedence use
one definition.
- `server/src/__tests__/generic-mcp-connection.test.ts`: regression
tests through the gateway's access summary and the direct policy check:
- only the read action enabled: the write stays off with the unscoped
grant, and a grant scoped to the write allows it
- every action turned off (both lists empty): both stay off; a
context-only scope stays off; a connection-scoped or application-scoped
grant allows them
- an agent the connection's profile does not cover keeps the unscoped
grant
- a general profile with an application-selector exclude (no connection
id) is not overridden by the unscoped grant
- with company-wide access, the direct policy check denies the off
action for an agent and still allows it for a board user with the
unscoped grant
## Verification
- New tests: all pass with the change. On `master`, 4 of 5 fail. On the
first revision of this PR, the all-off, application-exclude and
board-user tests fail.
- Regression suites, all pass: `generic-mcp-connection`,
`tool-access-policy-service`, `tool-profile-matching`,
`tool-profile-entry-order`, `connection-management.integration` (238
tests), and `tool-access-service`, `tool-gateway-service`,
`tool-gateway`, `tool-profile-review-service` (799 tests).
- Every enforcement path uses the one `decide` function in
`tool-access-policy.ts`, and `explicitGrant` is called only there. That
covers the gateway tool listing and calls, the access summary, delegated
and Test tab calls, the connection-intent access check, the GitHub
review bot tool, and the example smoke check.
- Chat eval `app-permissions.test-as-agent` (runner-opencode). Before
the change (GitHub Actions run 42550085273), the access summary for
Morgan reported the write as `allowed` by `allow_explicit_grant`. With
the first commit cherry-picked onto the eval branch, GitHub Actions run
42552819589 passed. The summary reported `launch_read` allowed
(`allow_profile`) and `launch_update` off (`deny_default`), which
matches Morgan's effective profile, and the agent's reply said the same.
## Risks
- Medium risk. This changes an access decision. An agent that used a
connection action only through its default unscoped grant, while the
connection's profile or an explicit exclude left that action off, loses
that action. That is the intended fix. An operator who relied on it must
enable the action in the connection or give the agent a grant that names
the action, connection or application.
- A grant scoped only by context (for example `{ projectId }`) no longer
overrides a connection profile. Risk-level and provider-type scopes are
not treated as naming an action.
- Tools that no connection profile governs (for example Paperclip's own
tools), agents without a connection profile, and board users keep the
current behavior.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), tool use and code
execution in Claude Code.
## 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
- [x] I will address all Greptile and reviewer comments before
requesting merge
Related: #17884 (scoped `tools:use` grants at hire time).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
canary/v2026.1109.0-canary.4
|
||
|
|
28e070f8dc |
feat(skills): show a line diff for a pending tracked-source update (#20155)
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - Skills can come from a tracked GitHub source, and an update moves
the installed copy to a newer upstream revision
> - Before an update, a person asks the agent "what changed in the new
version?"
> - The product has no diff for a pending update. The agent previews two
full SKILL.md snapshots and compares them by eye
> - In two eval runs the agent reversed the same changed line. In a
third run it fetched the files with curl and ran `diff` itself, and the
turn took too long
> - This pull request adds a read-only endpoint that returns the line
diff between the installed and the latest tracked revision
> - The benefit is that review answers come from an exact diff, and they
come faster
## Linked Issues or Issue Description
No public issue exists. Feature description:
**What happened**
An agent asked to show what changed in a pending skill update had only
`POST /skill-sources/preview`, which returns one file at one commit. It
previewed both commits and compared about 9 KB of text by eye. It
reported one changed line in the wrong direction ("every new build" ->
"every build or release" instead of the reverse), and later retracted a
real change.
**Expected behavior**
The agent can request the exact changes of a pending tracked-source
update and report them with the correct direction.
**Steps to reproduce**
1. Install a skill from a GitHub source at an old commit and set the
tracking reference to a newer commit (automatic updates off).
2. Ask the chat agent: "Show me what changed in the new accessibility
skill version before we update it."
3. See that the agent can only preview full files at each commit and
must compare them itself.
## What Changed
- Add `GET /api/companies/{companyId}/skills/{skillId}/update-diff`. For
a skill from a tracked GitHub source, it compares the installed source
revision (`currentRef`) with the latest tracked revision (`latestRef`).
For each changed package file it returns `change` (added, removed,
modified, or `not_compared`), `binary`, `executableChanged`, and a
unified line diff (`-` installed, `+` latest), or `diffUnavailable`
(`binary`, `too_large`, `omitted`, `budget`) that says why there is no
diff. The endpoint is read-only. The `skillId` parameter handler checks
company access and skill visibility, the same checks other skill reads
use.
- `hasUpdate` is false and `files` is empty when the installed revision
is already the latest one. That case reads only the latest commit and
scans nothing.
- Return `supported: false` with a reason for: a skill without a tracked
source, a disconnected source, an unknown installed revision, and a
repository package that a scan could include only in part (a subset, or
skill folders only). In the last case, files that a scan limit left out
would otherwise look removed.
- A path the scan left out of a revision (too large, a symlink, or
rejected by the audit) is listed as `not_compared` instead of added or
removed.
- One response shares a work budget (24M LCS cells), a diff-text budget
(1M characters) and a file-count budget (1,000 files), and sets
`truncated` when it reaches one. The GitHub read and both scans stop
when the caller disconnects.
- Add `server/src/services/skill-update-diff.ts` with a bounded line
diff (shared head and tail trimmed, LCS size capped). An absent side has
no lines, a final newline is not a line, and a missing final newline
shows the unified-diff marker. A replaced line reads `-old` then `+new`.
- Add the shared types `SkillSourceUpdateDiff` and
`SkillSourceUpdateDiffFile`.
- Document the endpoint in the API catalog (`openapi.ts`) so agents find
it with `search_api`.
## Verification
- `cd server && npx vitest run src/__tests__/skill-update-diff.test.ts`
(10 tests):
- direction of `-`/`+` lines, and separate hunks with bounded context
- exact diffs for added and removed files, and final-newline handling
- a change to only the executable bit, and paths left out of a scan
- the response budgets
- the snapshot comparison for the selected skill path, with the abort
signal passed to both scans
- a commit mismatch
- refusal of partial repository scans
- `cd server && npx vitest run
src/__tests__/skill-update-diff-routes.test.ts` (6 tests):
- the diff response
- no pending update (no scan)
- a caller from another company (board user and agent) gets 403 before
any lookup
- a skill the caller cannot read gets 404 before any source read
- every unsupported case
- a client disconnect aborts the scans
- `company-skills-routes.test.ts` and `openapi-routes.test.ts` pass (119
tests across the four files).
- `npx tsc --noEmit -p server`: no errors from this change.
- End to end: the `skills.review-update` chat eval exercises this path.
Its grader checks the quoted hunks against the real pinned diff.
## Risks
- Low risk. The endpoint is new and read-only. Existing routes do not
change.
- When an update is pending, each call scans the source twice (installed
and latest commit). It uses the same GitHub reader, scan limits and scan
slots as preview and refresh. The diff work and response size are capped
per response.
- `/update-status` and this route both read the latest commit of the
tracking ref. They are kept separate because `/update-status` compares
with the source's last scan, while this route compares with the
installed revision.
- No UI uses the endpoint yet.
## Model Used
- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), tool use and code
execution in Claude Code.
## 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
- [x] 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>
|
||
|
|
4afdf0493d |
fix(api-catalog): name the Connections page for company model service settings (#20152)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - A company can turn on model services such as Fast response, Decision model and Prompt Injection Prevention > - Only people who manage connections can change these services, so a chat agent must send the person to the right page > - The agent finds these operations through the API catalog search, but their catalog entries named no page > - The agent searched for the settings page, found nothing, and linked the general /company/settings page, which does not have the control > - This pull request adds a description to these catalog entries that names the section and the company-prefixed /company/settings/connections page > - The benefit is that the agent links the page that actually holds the control ## Linked Issues or Issue Description **What happened** A person asked the CEO chat agent: "Help me enable the company's Fast response feature so chat acknowledges my requests faster." The agent correctly got 403 on the fast-response API. It then searched the API catalog for "company settings page fast responses web UI" and found no page. It linked the general company settings page (`/<prefix>/company/settings`) and told the person to find Fast response there. That page has no Fast response section. The section is on /company/settings/connections. **Expected behavior** The agent links /<prefix>/company/settings/connections and tells the person to use the Fast response section there. If the section is hidden, the agent can say that an instance admin turns the experiment on. **Steps to reproduce** 1. Turn on the fast responses experiment. Create a shared OpenRouter API connection. 2. Open a chat with the CEO agent and ask it to enable Fast response. 3. Read the reply: it links /<prefix>/company/settings. Open that link: the page has no Fast response control. ## What Changed - `server/src/routes/openapi.ts`: add one shared description to the fast-response, decision-model and prompt-injection-prevention catalog entries. It names the section, the company-prefixed /company/settings/connections page, the Markdown link form, and that agent credentials cannot change or test the service. For Fast response, it also names the instance experiment that hides the section. - `server/src/__tests__/company-model-service-settings-guidance.test.ts`: new regression test. It runs the agent's catalog search (including the query from the failed run) and checks that the results name the Connections page and section. It also requires every page the descriptions name to be a route that `ui/src/App.tsx` mounts, so renaming a settings route fails CI. ## Verification - `pnpm exec vitest run src/__tests__/company-model-service-settings-guidance.test.ts` (from `server/`): 6 tests pass. Without the catalog change, the 5 search tests fail. If the Connections route in `ui/src/App.tsx` is renamed, the route test fails. - `pnpm exec vitest run src/__tests__/openapi-routes.test.ts`: 37 tests pass. - Chat eval `companies.fast-responses` (runner-opencode), with this commit cherry-picked onto the eval branch: before the change, the reply linked the general settings page, which has no Fast response control. After the change (GitHub Actions run 42549422681), the eval passed. The reply linked the Experimental settings page and the Connections page and said to use the Fast response section there. The scripted person clicked the reply's links and found the Fast response control on the Connections page, then saved and tested it. ## Risks - Low risk. The change adds catalog text only. It does not change any API or permission behavior. - If the settings page moves, these descriptions become wrong. The regression test compares every page the descriptions name with the routes in `ui/src/App.tsx`, so a route change that does not also update the description fails CI. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - Anthropic Claude Opus 5.5 (`claude-opus-5-5`), tool use and code execution in Claude Code. ## 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 - [x] 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> |
||
|
|
f316562045 |
test(chaos): wait for new agents to be ready before the first assignment (#20158)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - The restart chaos harness grades each run's time to first activity against the speed budgets > - The performance session traced an 8–12 s "first claim" delay on every wave's first cold runs. A brand-new agent spends about 8 s in `lifecycleState: verifying` while its harness runs a real hello probe, and queued runs start only when it becomes ready > - The chaos waves create agents and assign the first tasks in the same second, so those runs carry the one-time verification. Their cold first-activity and boot looked over budget even though dispatch after ready takes 40–60 ms > - This pull request makes the waves wait for each agent to be ready before the first assignment, records how long verification took, and fails the wave if an agent never verifies > - The benefit is that cold-start grades measure task startup, while agent verification stays visible as its own number ## Linked Issues or Issue Description Refs #20119 **What happened?** In waves `w-20261109T021322-s9801` (G1) and the K4/K6 wave, every new agent's first cold runs waited 8–12 s in `heartbeat.queue`. The performance session reproduced it locally: the agent is in `lifecycleState: verifying` while a hello probe runs, and its queued runs start on `onReady`. `ttft-baseline.ts` already waits for `ready` before launching tasks, so it never showed the delay. **Expected behavior** The chaos waves start their first task after the agent is ready, as the baseline does, and they report the verification time. ## What Changed - `seed.ts`: new `waitForAgentsReady` polls each new agent until `lifecycleState` is `ready`, with a 4-minute cap. - It records `seed_agent_ready` with `readyAfterMs` per agent on the timeline. - It throws, failing the wave, if an agent reports a `lifecycleError` or never becomes ready. - `seedChaosLab` runs it when `waitForReadyAgents` is set. `main.ts` sets it for every wave. `ttft-baseline.ts` keeps its own readiness logic, which skips unready adapters. - The seed test's fake API answers agent reads as ready. ## Verification - `npx vitest run` in `tests/restart-chaos`: 205 passed. New tests cover the wait, the verification-error failure and the timeout failure. - `node scripts/typecheck-test-projects.mjs tests/restart-chaos`: clean. ## Risks - Waves start about 8 s later, once per wave. - A product regression that keeps agents in `verifying` now fails the wave at seeding with a clear error, instead of showing as slow first tasks. - The one-time verification delay a user sees on a new agent's first task is no longer in the cold grade. It stays in the timeline (`seed_agent_ready`) for anyone tracking it. ## Model Used - Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code execution in Claude Code. ## 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 - [x] 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> |