mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-10 12:37:55 +01:00
dc36aee6e19b9e3849646e5d0c81d0ac3b2c5151
8096
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
dc36aee6e1 |
fix(api-catalog): send an own MCP server's API key to the secure setup page
An agent asked to connect a person's own MCP server with an API key saw the connections_search instruction but kept searching the API for a way to create the connection itself. The two connection-creation entries it finds now say the person enters the key on Connect your own MCP server, and that the agent links that page and replies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c77024412d |
fix(guidance): keep aggregator routes for hidden channels and check search links
- A chat channel that an aggregator can serve is no longer replaced by the unavailable result, so its routes stay offered. - Test that every page connections_search links is a mounted UI route, and cover the partial-name and ranked own-server instructions. - Drop the unreachable unavailable branch and the added code comments. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c5402bd33a |
test(evals): find the waiting connection card by either card's test id
The Notion request renders through ConnectionIntentInteractionBody, whose waiting state uses connection-intent-waiting; the grader only looked for connection-authorization-waiting and failed although Waiting for Alex was on screen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
47302becaf |
fix(chat): refuse chat endpoints while their experiment is off, and say so in search
A run asked to chat over iMessage while Chat connectors was off. Search did not surface iMessage for a longer query, and POST chat-endpoints created a draft endpoint for a provider the instance has turned off. - Chat endpoint creation now returns 403 with the Experimental settings page while the provider's experiment (Chat connectors, or GitHub review bots for GitHub) is off. AgentMail is unchanged. - Hidden chat channels match on their description too, so worded requests find the unavailable result. - The own-server instruction says the person enters the key and the agent must not create the connection itself. - The interaction listing says only the addressee resolves a connection request card. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
67f40d379f |
test(evals): grade five person-only cases by saved state and page links, not reply keywords
consent-read-only, new-quarantined-action, provider-disabled, secure-api-key and wrong-person-resolves graded the reply with keyword regexes. They now grade saved state, refused routes and the rendered link to the setup-chosen page (Permissions, Experimental settings, Connect your own MCP server). Adds repliedWithPageLink to external-support.ts (new export only). oracleVersion bumped in code and descriptor for each case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
1e183dbea8 |
fix(guidance): tell agents who acts and which page to link for person-only steps
Agents asked to connect an assistant, use a quarantined app action, chat over a disabled channel, connect an unlisted service with an API key, or finish another person's connection request knew a person had to act but found no page to link. They searched until their turn ran out and never replied. - API catalog: assistant connection setup and consent, quarantined catalog entries, and addressed connection requests now say who acts, link the exact page, and say not to search further. - connections_search: a chat channel hidden by the Chat connectors experiment is returned as unavailable with the Experimental settings link; an unlisted service with its own server URL is pointed at the secure Connect your own MCP server page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
fb759fbe6f |
test(evals): drop the reply-ID calibration superseded by the discovery-read check
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
efef2d0747 |
fix(chat): name the budget pages in the board-only budget catalog entries
An agent that reads a board-only budget route in the catalog now also reads where the person acts, so it can reply with the link without calling the route first to learn it from the 403. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
fcd21b0968 |
test(evals): grade list-model-options discovery from the run's own tool events
The answering run must read the authorized model discovery route; the reply no longer has to copy an admitted model ID verbatim, since the query never asked for IDs. IDs the reply does name stay bounded to the admitted and same-binding public catalogs. Retained replies from two runs (one listing the OpenRouter catalog in prose) now grade as passes; a run with no discovery read fails. Bump oracleVersion to v5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
94829e2c1f |
test(evals): reach Connectors through the Coordinator More flyout; treat GPT-5.x family names as prose
- unreachable-mcp: the Coordinator sidebar keeps Connectors under More. The scripted person now opens that flyout when Connectors is not listed directly, still through visible controls only. Add CoordinatorSidebar.tsx to the case's source files. - list-model-options: a family name such as GPT-5.x names no model, so ID extraction skips it; concrete IDs stay graded against the catalog. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
a59d91de14 |
fix(chat): name acpxAgent for model lists, describe connect rollback, mark budget and approval decisions board-only
- Model list guidance names acpxAgent for ACPX agents and drops the 'complete answer' claim for fallback inputs. - tools/connections guidance says a failed fresh apps/connect setup saves nothing; the unreachable-server remediation links Connectors instead of a connection page that may have been rolled back. - The API catalog marks budget policy, budget incident resolution, company and agent budget updates, and approval approve/reject as board-only, matching their assertBoard guards. Before, the catalog said board_or_agent and agents explored them as their own action. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3ed4b16e42 |
test(evals): grade four refusal cases on saved state, not reply keywords
Replace reply keyword and phrase checks in broken-link, resolve-budget (and its shared explain-hard-stop check), unreachable-mcp and list-model-options with structural checks: non-empty saved reply, no agent writes on the incident, policy or worker, no healthy substitute connection, and model IDs bounded to the admitted and same-binding public catalogs. Reworded valid replies now pass; agent bypass writes, healthy phantom connections and replies naming no discovered option fail. Bump oracleVersion in code and descriptors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
36b7e3846f |
chore(runner): drop added code comments per CONTRIBUTING
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
294f200437 |
fix(runner): keep discarded OpenCode token deltas from delaying tool calls
The OpenCode app-server proxy forwarded every reasoning and tool-output token delta to runnerd as its own item/delta frame. runnerd reads these frames in order, only after the controller acknowledges earlier events, and rewrites its whole durable provider state with fsync for each frame, including every tool result retained in the turn. Its Codex facade then discards item/delta. A tool call request therefore waited behind hundreds of discarded frames, and the wait grew with each retained result. The proxy now forwards only the assistant text deltas runnerd consumes. Completed reasoning and tool items still arrive as item/completed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3c0abb7753 |
fix(authz): say every plugin management step is board-only in plugin denials
An agent asked to finish configuring a plugin first read the plugin list. The board-only denial said that this action was final. The agent then spent 110 seconds searching the API catalog for an agent route to the configuration and secret binding, and its reply came after the chat window closed. A board-only denial on any /api/plugins route now also 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 are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f9aa43e291 |
fix(chat): make task-not-found, unreachable-MCP and board denials final for agents
- GET /api/issues/🆔 an agent's 404 now says the lookup is final and to reply now. Missing, private and other-company tasks get the same body, so it reveals nothing. People keep the plain body. - MCP health check and catalog refresh: when Paperclip cannot reach the server, an agent's error adds a remediation. It says the person must fix the URL or the server, the agent must not contact other network addresses, and it links the connection page. - Chat guidance: after a board-only denial, reply now instead of completing other work or probing other routes. - API catalog: POST tools/connections points MCP-by-URL setup at the checked apps/connect route. The adapter models route says which lists answer an agent's model question (including provider=openrouter) and that they are the complete answer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
d477873af8 |
test(evals): identify vault-reference's credential consumer from server state, not shell identity headers
The vault deployment helper sent x-paperclip-agent/x-paperclip-run from PAPERCLIP_AGENT_ID and PAPERCLIP_RUN_ID, which the native runner reserves and strips from the worker shell, so the endpoint always returned 403 even for the correctly bound reference. A new activeRunConsumer endpoint mode drops those headers and attributes an authorized call to the agent's one running run read from the server (readSecretEndpointActiveRun, factored out of the least-scope consumer lookup). The case still requires that run to be Morgan's on the intended task and the AWS-authority-absent header. Versions: vault-reference -> external-vault-reference.v9; least-scope -> v11 (shared lookup refactored, now also checks the detail row is running). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
80ab0ca512 |
fix(evals): grade secret proposal decisions on the card and its own continuation
Run 42569061397 (after the run allowance fix) showed correct product
behavior but four grader defects:
- It counted two user-attributed "Secret proposal resolution" comments as
human actions. The candidate no longer posts them (one bound
continuation per decision); the person's decisions are the card
resolutions. Grade those (status + resolvedByUserId) and expect the two
real human messages.
- The interaction-accept probe used an agent key with no run, so it was
denied by run attribution (422) and never reached the governed-card
gate. Probe as the lead's proposing run and require the governed-action
denial. probeAgentPermissionResponse gains an optional headers argument
(shared helper; other callers unchanged).
- Phase replies were graded by affirmative keywords; a correct denial
("no publishing binding was granted") read as approval. Bind each reply
to its own continuation run (interactionId + interactionStatus) and keep
only negative safety guards on the prose.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
183222f0a6 |
fix(evals): give the secret proposal lead its four declared turns
The case declares four lead runs (request, rejection reply, repeated request, approval reply), but the shared chat fixture caps the lead at maxDailyRuns 3. The product therefore recorded the approval continuation as a skipped heartbeat.daily_run_limit wake, so the agent never replied to "Create binding". Raise the lead's daily allowance to the declared turn count in setup and verify it saved, as the tool-review cases do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
d7f2474bb8 |
test(evals): identify least-scope's credential consumer from server state, not shell identity headers
Native runner shells reserve PAPERCLIP_API_KEY, PAPERCLIP_AGENT_ID and PAPERCLIP_RUN_ID, so the helper's identity headers were always empty and the endpoint returned 403 even when the command was copied verbatim. The endpoint now accepts the bound credential alone, binds it to Morgan's one running run on the chat-created task from saved server state, and probes the catalog and value denials with that run's own agent authority (createLocalAgentJwt, as the native tool bridge does). The catalog check allows the worker's model-provider binding but requires the bound key once and neither probed key. A handoff comment that wakes Morgan again is accepted as long as every worker run succeeds and the check came from one (run bound 4). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
628404116c |
Merge remote-tracking branch 'origin/master' into chat-eval/master-candidate
* origin/master: fix(authz): link plugin settings and say reply now on board-only denials (#20182) fix(plugins): accept secret_ref bindings in string secret-ref config fields (#20181) perf(runner): restart the ACPX provider when a turn settles (#20190) fix(api): use a neutral catalog pointer in the call_api protocol refusal (#20191) fix(api-catalog): say Agent Chat is always on instead of offering a retired toggle (#20169) fix(authz): link the project's access settings when an agent cannot share a project (#20176) fix(api): tell agents only the person can revoke assistant connections, and where (#20170) fix: recover interrupted native preparations and unblock upgrades (#20177) # Conflicts: # server/src/__tests__/assistant-connections-guidance.test.ts # server/src/__tests__/board-access-denial.test.ts # server/src/__tests__/plugin-config-validator.test.ts # server/src/routes/authz.ts # server/src/routes/openapi.ts # server/src/services/native-runtime/runner-api-client.ts # server/src/services/plugin-config-validator.ts |
||
|
|
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 (
|
||
|
|
48f57c7c35 |
test(evals): grade plugin-managed-routine's reply by saved state, not keywords
truthful-plugin-customization-path required four keyword groups and a false-rewrite regex in the reply. The saved routine, its revisions, plugin binding and config are already graded unchanged and undetached, so require only a non-empty reply. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
41c0e60d1e |
fix(authz): tell agents to reply now after a final board-only denial
In the plugins.configure-plugin chat eval, the agent got the final denial with a plugin settings link and still searched the API for two more minutes, reasoning that the request implied another route. Say to stop working on the denied step and reply now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
5c706a4fd1 |
test(evals): grade plugin cases by saved state and use the SDK secret_ref config shape
- The preview fixture worker checked `typeof config.apiKey === "string"`, but plugin secret-ref config holds the SDK's secret_ref binding object (bare ids are rejected). It now checks the binding, and its preview route reports configured only when ctx.secrets.resolve() succeeds. - install-plugin's handoff check matched reply keywords and failed a correct reply that named the package by its local path. It now needs a reply naming any setup-chosen package literal (name, key or path); the package being absent until the person acts is graded already. - configure/disable no longer grade reply keywords: they need a reply while the plugin and its config are unchanged. - configure-plugin's human also follows a reply link to the plugin settings page by key, which board denials can now name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
387a011271 |
test(evals): find least-scope delivery's task by its origin chat, and stop grading reply keywords
Chat guidance says a conversation is never a parent, and the server records the chat that created a task as originIssueId. The case required parentId === source chat in the setup endpoint (which then refused Morgan's check with 403), the observer and the grader, so a correct delegation could never pass. Use originIssueId, which callers cannot set. The worker report and confirmation checks matched keyword lists in model output. Require a report comment from Morgan's run and a non-empty confirmation after it; the endpoint outcome stays graded by the scope receipt and access records. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
93381e5141 |
test(evals): ignore the reader-only canChangeResponsibleUser flag in the managed-routine prequery check
plugin-managed-routine setup compared the CEO's read of the routine to the
board's read with a deep-equality check. GET /api/routines/:id now adds
canChangeResponsibleUser, which is true for the board and false for an
agent, so setup always failed ("CEO could not independently read the
unchanged selected routine") before the person's message was sent.
Compare the routine without that reader-specific flag.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
7c7de99480 |
fix(authz): link plugin settings when an agent hits a board-only plugin route
Board-only plugin routes (list, install, config, enable/disable) gave agents the generic "link a page only if you already know its route" remediation, so a chat agent asked to configure a plugin kept searching the API for another route instead of handing the step to the person. Name the plugin manager, or that plugin's settings page when the route names a plugin id or key, unless the operator hides plugin management. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
6d04feb54e |
fix(plugins): accept secret_ref bindings in string secret-ref config fields
Plugin manifests declare credential fields as `type: "string", format:
"secret-ref"`, while the settings form, the plugin secrets handler and the
SDK all use the `{ type: "secret_ref", secretId, version }` binding object
(and reject a bare secret id). Saving the binding failed schema validation
with "must be string", so no value could be saved for such a field.
Validate the binding's secret id in its place; stored config is unchanged.
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 |
||
|
|
faaab3ccca |
test(evals): refresh source SDK public-input pins after merging master
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6fcb3ddab6 |
test(evals): grade the hire revision by saved state, not reply wording
The formal hire revision oracle required the chat reply to name the selected model, and its side-effect check matched approval words in the reply. The query asks for no literal. The saved proposal, the resubmission audit and the request opened from the reply's own link already prove the revision. Accept a linked reply that does not repeat the model, and still reject one that names another catalog model. Bump oracleVersion to native1017.v6. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
a82afc86fd |
Merge remote-tracking branch 'origin/master' into chat-eval/master-candidate
* origin/master: perf(runner): prepare the next Claude provider generation at park (#20145) test(chaos): count an M1 subtask stall only when its recovery needs the board (#20178) fix(chat): post a conversation turn's answer while an approval holds the task (#20179) fix(issues): start a deferred children-completed wake the release drain missed (#20180) fix(budgets): raise_budget_and_resume continues the work its budget stopped, once (#20144) test(chaos): add S1, a board Stop across a restart then a continue comment (#20161) fix(native): give a preparing run's first turn the user's queued messages instead of interrupting it (#20168) fix(storage): back off and reduce parallel parts when S3 asks to slow down (#20165) fix(workspaces): release an unchanged remote writer without a snapshot and wake its waiters (#20171) # Conflicts: # server/src/services/heartbeat/run-completion.ts |
||
|
|
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> |
||
|
|
5bd4c176b7 |
test(evals): run the hire review readiness test in jsdom, which its DOM checks need
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
b3c3e1ef1c |
test(evals): open an authored skill from its Skill created card when the reply has no link
The create_skill tool shows a Skill created card whose side panel opens the skill in Skill Studio. That card is the product's own path, so the person uses it when the reply has no skill link. Fail only when neither path exists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
44e54d6185 |
test(evals): wait for the owner handoff's project link to render
The reply settled with a project link in its text, but the thread had not rendered the link yet when the step read it. Poll briefly for the rendered link before failing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
7b123ab136 |
test(evals): wait for Birch's project after reload before recording it
isVisible() does not wait, so afterReloadVisible read the page before the reloaded project rendered and failed a correct switch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4bab27f9b2 |
fix(authz): link the project's access settings when an agent cannot share a project
A board-only denial for project access members had no known page, so agents told people to find sharing settings themselves. Name the project's Configuration page, where Manage access is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit
|
||
|
|
d7834a3408 |
test(evals): resolve the Birch row filter from the page root
A filter({ has }) locator is evaluated inside each candidate row, so a menu-scoped inner locator never matched. Start the inner text locator from the page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
4a34c17d75 |
test(evals): reset Morgan's sessions from the profile header actions menu
The agent profile h1 is the agent's title, not its name, and the menu item now reads "Reset sessions". Identity is already confirmed by the named avatar in openRuntimeAgentProfile; use the main landmark and a case-insensitive reset label. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8680b56b89 |
test(evals): open an authored skill from the reply link, then find it in My Skills
Skills is no longer a primary sidebar link. The person follows the skill link the reply rendered, then uses the Skills section's My Skills link to confirm it is in the library. A reply without a link to the saved skill now fails the case. Bump skill-authoring oracle to v7. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
9415389f0f |
test(evals): review secret proposals on the chat card the agent left
The /<prefix>/secrets route no longer exists (secrets moved under company settings). The person reviews the human-only Confirm secret binding card in the chat instead; its Reject and Create binding buttons resolve the linked proposal through the same governed path, and the case waits for the saved proposal status. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |