The resume-agent grader required phrase lists in both replies. A valid
reply such as "Morgan hasn't started the work yet" failed the
no-invented-execution regex. Grade the first resume by its attribution to
the first request's own run, keep the no-worker-run check, and require
a bound reply for each phase.
Add no-agent-budget-override: in run 42404423330 the CEO lifted the
board's budget hard stop through PATCH /api/agents/{id} after the budget
route refused it. That outcome must fail even when the reply explains it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Chat guidance defined only the task-link template. Asked to resume an
agent, the chat agent resumed it and reported the result but never
linked the agent, leaving the person no direct route to check its
saved availability.
Tell chat to link an affected agent's profile at
/<prefix>/agents/<urlKey or id>, and keep that link distinct from an
agent:// mention, which notifies the agent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 5324cfc4df)
Configuration preparation on current master publishes passive work
hints through db.execute, so the empty object mock from #17857 throws
before the budget assertions run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Chat agents get a route pattern for task links but none for approvals.
Approval tools return no href, so an agent that lists a pending approval
cannot link it without guessing. Add the company-prefixed approval route
that the board UI serves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 6af054a22e)
The show-decisions grader read only the line that named each item and
required words such as "you" or "answered" there. A correct reply that
put "waiting on you" in its list heading failed. Read each item with its
heading, require the exact approval and task links, require Riley to be
named beyond the setup titles, and check that Riley's question and the
answered request are listed apart from the person's own decision.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The native Stop boundary required a bare UUID intent id, but production writes
native-cancellation:<uuid>. Label the health-detail and source-SDK attach steps and
code the remaining diagnostic and SDK-proof refusals.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reply path required one of "which/choose/select", so a valid
"Do you want the launch poster or the welcome note?" failed. Now a reply
must name both setup-chosen proposal titles and pose a question, or the
evaluated run must have created a pending question naming both. Bumps the
case oracle to paperclip.chat-catalog.identity.ambiguous-yes.v2.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
goals.create-goal-tree matched model-chosen goal titles with narrow phrase
regexes, so valid titles such as "Ship the website" or "Reach 1000
customer signups" failed, and a truthful "0% complete" note counted as
invented progress. Identify the goals by level and parent and require only
the requested subject (1,000 + signups; website) in saved title/description.
Bump the oracle to goal-hierarchy.v3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The later-result check required keyword combinations in the follow-up reply and
rejected any mention of 'failed', so a correct answer saying 'nothing failed'
failed, while an acceptance-only reply could pass through the
'summary ... is' alternative. The routine output check likewise required
specific words in the routine's own comment.
Grade instead that the routine's native run read the actual Launch project
and saved its summary, and that the follow-up answer came from a new chat run
which read the completed result task after the invocation finished, with the
task linked. The first-reply guard is now sentence-scoped and ignores
future/conditional framing. Bump the case oracle to
integrator.routine-invocation.v2.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Chat guidance defined only the task-link template. Asked to show a
task's runs, the chat agent had GET /api/issues/{id}/runs data with
agentId and run id but no run route or agent name, so it listed raw
UUIDs instead of linking each run and naming its agent.
Add the registered /<prefix>/agents/<agentId>/runs/<runId> route and
ask chat to read and name the agent rather than show a raw ID.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 5c326e2fb7)
The interrupted-and-continuation-states-explained check matched state
words on reply lines that contained a raw run UUID plus the literal
"Morgan". Correct replies that link runs on a separate line, or use other
state words, failed; it also pushed the agent toward pasting raw IDs.
Replace it with a check that the chat run read the website task's run
history (GET /api/issues/{id}/runs or the exact run detail) through a
product tool. Exact native run links per agent, and opening them to the
real cancelled/succeeded status, remain required. Bump the oracle to
paperclip.chat-catalog.run-history.v2.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Asked how to quickly find tasks without leaving their page, the chat agent
had no source describing quick search: it searched for docs, said it did not
know the UI, and told the person to ask in chat instead of naming Cmd/Ctrl+K.
Document the command palette, its search operators, the Search all handoff to
full search, and that the agent cannot drive the person's browser. Add a test
that ties the documented shortcuts to the palette's actual key bindings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 0595904175)
The independent observer pressed Cmd/Ctrl+K right after domcontentloaded,
before the app mounted the palette listener, so the palette never opened
(run 42404426034). Wait for the shell and retry the shortcut.
The reply check accepted any 'command ... k' or a bare 'command palette'
mention. Require the shortcut itself (a modifier followed by K, any common
spelling), which is the fact the person needs; bump the oracle version.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The write-plan oracle required nine concept regexes plus negation parsing
over the saved plan. A correct plan flipped to failing when reworded with a
single synonym (phone for mobile, tab-key for keyboard, landing page for
single page). Measure instead how much of the task's own brief the saved
plan covers, sharing the brief between setup and grader, so paraphrases pass
and generic or foreign plans still fail. Bump the case oracle version.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Run 42404415872 enabled the skill correctly: the CEO synced only Morgan's
skills, policy and installed revision were unchanged, and Morgan's next
task received and read the exact revision. It failed only the reply
regex, which read "Beta skill versioning isn't enabled" as a denial that
Morgan could use the skill.
Replace the phrase matcher with a state check: the chat run configured
no agent other than Morgan, and it replied. Saved assignment, unchanged
policy and future delivery remain graded. Bump oracle to v3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Post-seal attachment steps kept the seal-original stage and their messages had no
closed codes; give them an attach-original stage and codes. The post-stop fixture
could not read opencode serve launched from an inherited descriptor; fall back to
the kernel's /proc/<pid>/exe link.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The runner's deepest provider process is node running the extensionless opencode
launcher with serve. Qualify that shape and record absolute-argument basenames and
whether serve is present.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The seal still reported 'unclassified': its refusal came from a module it calls
or from a filesystem error. Add closed codes for framework-report, redaction and
retention messages, and report other filesystem errors by their error code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The runner's provider chain is three processes deep and nothing qualified. Allow
node runtime flags before the absolute proxy script (still last, still digest
checked), and record each descendant's command name, argument count and script
basenames so the next refusal shows why.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Packaging, sealing and report-asset refusals that were not in the closed table
surfaced as 'unclassified', so a refused attempt said nothing about why. Add a
code for each fixed message and a test that keeps the table complete.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The provider launch can be wrapped, so the provider need not be a direct runner
child. Accept exactly one qualifying descendant (depth <= 4) under its actual
parent, and record a closed summary of the descendants seen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The local session runs inside the verified runnerd, which launches the OpenCode
proxy (or Codex app-server) as its direct child without a checkpoint record.
Choose the one qualifying child; all existing ownership checks still apply.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Local Codex and OpenCode (through the app-server proxy, which keeps opencode serve
in its process group) run behind the Codex app-server driver. Its session.started
carried no process identity, so the control plane's stop proof never checked that
process. Emit its pid, process group and OS start time.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The OpenCode server driver spawns `opencode serve` in its own process group but
its session.started/resumed carried no process identity. The server's stop proof
checks processId from these events, so it never verified that this provider
process had exited. Emit its pid, process group and OS start time, and share the
runner's local process start reader.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Local OpenCode now runs opencode serve directly from the runner instead of the
app-server proxy behind the runnerd transport, so the checkpoint has no provider
process. Read the provider PID and start time from the run's session receipt, and
qualify the runner's own spawned opencode serve by parent, uid, start and command.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The status arbiter already blocks the task for a model choice. The release path
then opened a native reconciliation incident because the task was blocked, which
put the run in recovery_needed and hid its Retry. A rejected model or incomplete
configuration has no execution to reconcile; release without the incident.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A driver-classified model_unavailable failure that requires action now maps to
native_provider_model_rejected for any provider, so the task is blocked for a
model choice and chat shows Model unavailable instead of a generic adapter
failure. The shared blocker text no longer names a ChatGPT connection.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rerun's lead answered correctly (card generated before RUN-1 completed, so it
is stale), refreshed the card it authored, and said so. The harness waited for
exactly two runs, so the refresh run made it time out. Bound runs at 3..5, wait for
any refresh to settle before deleting the source, compare the access-change probe
with the card as answered, and require the original summary to stay in history,
changing only through a lead-requested refresh.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The explain-card grader required regex matches on the model's explanation and a
link back to the status card the person had attached. A correct, grounded reply
failed. Grade what the agent actually did instead: it read the live source task
and linked it before explaining, and after the source was deleted its follow-up
turn attempted a fresh read that returned 404 with no successful read.
Explanation wording is no longer graded; a reply that misstates a correctly read
live status is not detected by this oracle.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agents relayed the bare path as inline code, which the person cannot click.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 2e3a19c2aa)
Product rule: an agent cannot read other agents' commentary unless it holds
commentary:view, which is not a default agent grant. An agent may read its
own entries by asking for them explicitly. Without the grant, listing all
commentary or another agent's entries is a 403 that names the human page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 0fc6a7a635)
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - Remote native runs execute in a sandbox, and recovery can reattach a
run to its original sandbox runner
> - Reattachment checks the saved lease, workspace, and runner identity
in five places. Each failed check throws the same bare
`native_remote_recovery_lease_mismatch`
> - On staging, 34 OpenCode runs on 31 tasks failed with this bare error
in one evening. The run records could not show which check failed, so
the cause could not be confirmed from the run alone
> - This pull request makes every producer name the failed step and
field with closed values
> - The benefit is that an operator and the next investigation can see
which reattachment check failed, without new logging or identifiers in
the error
## Linked Issues or Issue Description
Refs #19399, Refs #19733, Refs #19755, Refs #19779
**What happened?**
Remote same-run retries failed with
`native_remote_recovery_lease_mismatch` and the task was blocked for
reconciliation. The error is thrown by environment acquire, environment
resume, workspace preparation, workspace sync, and remote runner
verification. All of them used the same bare string. The run's `error`
field and its run events did not show which check failed.
**Steps to reproduce**
1. Start reattachment for a remote native run whose saved lease,
workspace reference, or runner identity no longer matches.
2. Read the run's `error`.
3. It shows only `native_remote_recovery_lease_mismatch`.
**Expected behavior**
The error names the step and the field that differed, with no
identifiers, and existing consumers of the code keep working.
## What Changed
- New
`server/src/services/native-runtime/remote-recovery-lease-mismatch.ts`:
the code constant, a closed `stage` and `field` vocabulary, and
`remoteRecoveryLeaseMismatch(stage, field)`. It builds
`native_remote_recovery_lease_mismatch: stage=<stage> field=<field>`.
- `environment-run-orchestrator.ts` (stages `environment_acquire` and
`environment_resume`), `heartbeat/workspace-preparation.ts`
(`workspace_preparation`), `native-workspace-sync.ts`
(`workspace_sync`), and `verifyRemoteRunnerReattachment` in
`native-session-executor.ts` (`runner_verification`) now throw the
detailed form. Each check keeps the same condition and order. Only the
message changes.
- `restart-interrupted-turn.ts` uses the shared constant. It already
matches the code exactly or as the `code: ` prefix, so automatic
continuation after a reattachment loss is unchanged. No other consumer
matches the code.
- `doc/execution-semantics.md` documents the detail format.
## Previous-release state
This change adds no persisted state and changes no saved identity. Runs
that a previous release failed keep the bare
`native_remote_recovery_lease_mismatch` in `error`. Runs failed by this
release store the detailed form. The only consumer (`isReattachmentLoss`
in `restart-interrupted-turn.ts`) accepts both forms, and
`workspace-recovery-retry-copyback.test.ts` covers both.
## Verification
- Exact-detail assertions in the producer tests:
- `server/src/__tests__/environment-run-orchestrator.test.ts`: every
scope, status, expiry, provider, lease-ID, cwd, plugin-task, and resume
case asserts its stage and field. Against the previous producer, 21 of
these cases fail (the message was the bare code). With this change, all
57 tests pass.
- `native-session-executor.test.ts`: a replaced sandbox reports
`runner_verification`/`provider_lease_id`. A missing durable runner
identity reports `identity_run_id` before any sandbox read.
- `native-workspace-sync.test.ts`,
`heartbeat/workspace-preparation.test.ts`,
`native-controller-recovery.integration.test.ts` assert their exact
detail.
- End to end on embedded PostgreSQL:
`heartbeat-native-remote-failure-retention.test.ts` records
`native_remote_recovery_lease_mismatch: stage=environment_resume
field=lease` on the run when the provider no longer resumes the lease,
and it still requests the user's Retry once.
`workspace-recovery-retry-copyback.test.ts` adds a detailed-form row
that still continues once.
- The affected server test files pass locally. `pnpm --filter
@paperclipai/server typecheck` passes.
## Risks
- Low risk. Only the error text changes. Conditions, ordering, and
fail-closed behavior are unchanged.
- Logs and run errors show a longer message. The detail has closed
values only, so it adds no identifiers or secrets.
- A first version of this PR also changed remote retry selection. Review
showed that a plain resume in a new sandbox hits the remote workspace's
unsaved-work hold, and that a `destroy` disposition does not prove the
old sandbox is gone. That change is removed. It needs a separate design.
## Model Used
- Claude Opus 5.5 (Anthropic), model ID `claude-opus-5-5`, run in Claude
Code with tool use (shell, file editing, read-only browser access to
staging).
## 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>
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native agents use tools to read reusable company skills.
> - The existing native skill tools read only skills assigned to the
agent.
> - An agent can stop when a task needs another skill that is already in
the company library.
> - This pull request adds tools to search and read accessible company
skills on demand.
> - Agents can use the saved instructions without a persistent
assignment change.
## Linked Issues or Issue Description
**Problem or motivation**
An agent cannot use an unassigned company skill through the standard
native skill tools. A generic API workaround needs extra discovery and
can return the full library.
**Proposed solution**
Add `search_skills` and `load_skill` to the native tool catalog. Search
returns small pages of stored metadata. Load returns text from a saved
version and lists its files. Both tools use the current run identity and
existing company and project access checks.
**Alternatives considered**
Assign each skill before a read, or use generic API discovery. Both add
work to a read that the company already permits.
**Roadmap alignment**
This extends the existing skills discovery path. It does not add a
store, permission model or installation flow. Related: #15409 adds an
assigned-skill loader to a different adapter.
## What Changed
- Add bounded company skill search. Search does not synchronize sources
or fetch repositories.
- Add versioned text reads for SKILL.md and supporting files. Page text
and file lists. Reject unsafe paths and binary files.
- Recheck access after storage reads. Keep the existing closed Muse,
skill-test and completion-review tool scopes.
- Add integration tests with isolated PostgreSQL and real local package
storage. Update the native tool count assertion and skill documentation.
- Limit text files to 16 MiB before body reads. Accept saved file paths
up to 4,096 characters plus the repository prefix. Return the response
digest only on the first page.
- Add optional byte limits to the package readers. Select only the
requested legacy file in PostgreSQL. Cast the size comparison to
`bigint` so existing callers can omit the limit.
- Cache decoded text and its digest for one run. Limit the cache to 64
MiB and 64 files. Check access on each page and clear the cache when the
run closes.
- Read repository source files and wrapper inputs through verified warm
reads or isolated recovery. These reads do not hold the company startup
lock.
## Verification
- Local verification tested tree
`7ba5828387ca0a9c2ee99655818a28de1235a854`, published as commit
`c9d04cba601541cca506f5b72aa6f7272a25379f`. The published tree matches
that tested checkout. Current head
`33fa3d3d4c7988288685ce7c6499d064e0cb9079` (tree
`8daaa2f3264d1cd65a9b6d85dbb9d3729ac4a29d`) removes six comment lines
from three source files; its executable code is unchanged from the
locally tested revision.
- Four native skill and authority Vitest suites: 72 passed and 7
existing sandbox checks skipped. All 20 skill-library integration tests
passed.
- Skill-library and package-store suites: 65 passed. This includes the
45 package-store cases and the same 20 integration cases. Ran with umask
`022` for executable-file tests.
- Server source and test typecheck layers passed in low-memory mode. The
affected source cycle also passed a final separate check.
- `git diff --check` and `node scripts/check-module-boundaries.mjs`
passed.
- Red/green evidence: the new default inline-read case reproduced
PostgreSQL error `22003`. Two max-size paging cases reproduced extra
body reads. Warm and cold repository reads timed out while another
connection held the company startup lock. All five cases pass after the
fixes.
- Both 16 MiB paging cases check that the second page performs no
additional body read. They also check that source-access revocation
blocks a cached page. Warm and cold lock cases load both the generated
wrapper and a supporting file while the startup lock remains held.
- The first cold-read test attempt had an incorrect helper import.
Corrected the test import, reproduced the lock timeout, then verified
the fix.
- [CI on the previous
revision](https://github.com/paperclipai/paperclip/actions/runs/42319652795)
passed typecheck, build and policy. Serialized shard 2 failed in the
unchanged test `schedules bounded retries for failed accepted
interaction continuation wakes`. The same case passed locally: 1 passed;
416 cases were excluded by the filter. The cause of the earlier CI
failure remains unknown. [CI for
c9d04cba](https://github.com/paperclipai/paperclip/actions/runs/42323260426)
passed: 52 successful jobs and one skipped selector, including
typecheck, build, all serialized server shards, native server tests,
Runner/Rust, and e2e. This establishes a later pass; the earlier
heartbeat failure's cause remains unknown. [CI for current head
33fa3d3d](https://github.com/paperclipai/paperclip/actions/runs/42325148224)
passed: all 52 applicable workflow jobs succeeded, with one skipped
selector. The head has 58 successful check runs and three skipped
checks; Snyk also passed.
- Earlier failures remain recorded. Preflight cannot build Rust because
Cargo is absent. The first catalog assertion expected 51 tools instead
of 53. Test startup needed the plugin SDK build. An earlier typecheck
hit the 8 GiB memory limit; low-memory checks passed. The previous
package test failed under umask `077`; it passed under `022`, and this
run's complete package suite passes with `022`.
- Greptile reviewed current head `33fa3d3d` at 2026-11-07 01:40 UTC: 5/5
with no outstanding findings. The three older Ponytail threads are
resolved after checking the size/cache, long-path, and bigint fixes and
regression tests. Ponytail's supplemental refreshed assessment stopped
before completing and posted no new findings. Its earlier findings are
fixed and all seven review threads are resolved. The completed Greptile
assessment covers current head.
- No hosted deployment or live provider run was performed.
## Risks
- Package reads can recover stored packages through the existing storage
layer. Search reads only stored database metadata.
- Load is a read. It does not install files into the provider workspace
or change runtime bundles. It does not grant tool access or authorize
actions in the skill text.
- Callers must retain the returned version ID for later pages. A missing
version returns an error.
- Files above 16 MiB return a size-limit error. The run cache retains at
most 64 MiB of decoded text and 64 entries. A cache miss or eviction
requires a full verified file read. Each page still checks current
access. The SHA-256 response field is null after the first text or
file-list page.
- No database migration or UI change is required.
## Model Used
- OpenAI Codex, GPT-6 family, with reasoning, repository tools and code
execution. The exact model ID and context window are not exposed to this
agent.
## 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
Agents can submit commentary but not read it. A denied agent got only
"Board access required", so it could name the feed but not link it. The
403 now carries a remediation naming /<prefix>/activity/commentary for
the agent's own company. Cross-company requests stay a plain 403.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 4279d57aa9)
## Thinking Path
> - Paperclip manages AI agents and preserves their work across server
changes.
> - Native harness preservation stores retained web-server files in
authenticated archives.
> - A retained skill can contain hundreds of valid local files.
> - The WEB evidence owner rejected any directory or inventory above 128
entries.
> - This blocked preservation of two 462-file skills even though their
payload fit the existing byte limits.
> - This pull request shares finite inventory bounds between capture and
archive validation.
> - It preserves every file and keeps the execution, identity, writer,
and publication guards.
## Linked Issues or Issue Description
Refs: #19814
**What happened?**
WEB evidence preservation rejected a retained source with 927 local
skill files. The source had settled execution ownership. The files fit
the existing payload limits. The directory enumeration failed before
archive publication because its entry limit was 128.
**Expected behavior**
Preserve a valid finite inventory within explicit file, payload, path,
and metadata limits. Keep remote recovery obligations unchanged.
**Steps to reproduce**
1. Retain two 462-file skills and one three-file skill under a settled
native source.
2. Request canonical WEB evidence preservation.
3. The old owner rejects directory enumeration. The added integration
test reproduces this refusal with real files and PostgreSQL.
## What Changed
- Share the WEB limit through the existing controller archive owner:
10,000 total entries, 4 MiB of manifest metadata, and 4,096 bytes per
path.
- Apply the limits to filesystem capture, published binding validation,
and archive descriptor validation.
- Keep the existing 16 MiB per-file and 64 MiB payload bounds. Keep
authority history at 128 entries and 32 epochs.
- Add real archive and cold-restore coverage for 927 files, legacy
manifest compatibility, and bound refusals. Document the limits.
## Verification
- Genuine baseline RED: the new 927-file integration case reaches the
original `captureWebEvidence.directoryNames` limit and fails before
publication.
- The initial setup attempt failed before test collection because the
generated plugin SDK was absent. The required SDK build fixed setup; it
was not counted as a regression result.
- The first affected three-suite run passed 93 cases. The large
roundtrip exceeded the generic 15-second case deadline. That failure is
retained. The isolated large case passed with a 60-second case limit,
but full CI later exceeded that limit while the other 4,577 tests in its
shard passed. Both timeout failures are retained. The final fixture
keeps all 927 files and 18.5 MB, uses bounded 16-way independent
comparisons, and has a 120-second whole-case limit. The canonical
preservation signal remains 60 seconds. No production deadline or
assertion changed.
- The 927-file case then passed: encrypted archive, authenticated cold
restore, all bytes/hashes/modes, unchanged remote leases, and
database-only validation with source volume absent and storage reads
forbidden.
- The final metadata repair adds a genuine RED case: an entry list below
4 MiB previously emitted a larger complete manifest without rejection.
The owner now checks the complete serialized manifest. All ten focused
cases passed after that repair. The final bounded-I/O fixture passed the
same ten cases in 37.26 seconds, including the full 927-file roundtrip.
All 93 other affected cases passed in the preceding three-suite run.
Fresh required CI supplies broad final-head coverage.
- Server TypeScript, module boundaries, and whitespace checks pass.
Run the focused PostgreSQL checks from `server/` with the repository
wrapper:
```sh
node ../cli/node_modules/tsx/dist/cli.mjs ../packages/db/src/test-postgres-command.ts -- node ../node_modules/vitest/vitest.mjs run src/services/native-runtime/native-harness-filesystem-independence.integration.test.ts --testNamePattern 'preserves all 927|existing 128-entry WEB|WEB manifest beyond|oversized WEB filesystem' --hookTimeout 90000 --maxWorkers 1 --fsModuleCache
```
## Risks
The owner can hold more entries in memory. The fixed 64 MiB payload, 4
MiB metadata, path, and entry bounds constrain that cost. The archive
format and old receipt bytes do not change. Older builds still have the
128-entry limit and cannot qualify a larger new receipt. This change
does not settle remote recovery, grant execution authority, alter
finalizer locks, or change publication CAS rules. No hosted preservation
or deployment was performed for this PR.
## Model Used
OpenAI `gpt-6.1-sol`, with code execution and repository tools. The
context window size is not exposed.
## 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>
## Thinking Path
> - Paperclip manages AI agents and the work they save.
> - Online migration must save legacy local files while other remote
work continues.
> - A failed native run can retain a remote recovery hold after its
local writer has settled.
> - The current source proof recognizes only some of those recovery
producers.
> - This blocks stable local files and their shared Git aliases behind
an interrupted session or exhausted export recovery.
> - This change admits two exact producer shapes under the existing
physical and task locks.
> - It preserves the failed run, accepted result, remote hold, retained
allocations, and recovery history.
## Linked Issues or Issue Description
Refs #19642, #19688, #19810. A public search found these related
changes. This is a narrow correction to their local preservation proof.
**What happened?**
Online workspace import rejects a stable local source with
`online_workspace_source_scope_unproven` when its prepared native source
has an ambiguous session interruption hold or an exhausted file recovery
hold. Shared Git aliases inherit the refusal.
**Expected behavior**
Save the stable local copy under the existing source locks. Keep remote
recovery blocked until its own recovery owner supplies the required
evidence.
**Steps to reproduce**
1. Create a failed native run with an unowned terminal finalizer and an
exact prepared allocation.
2. Keep either its board-owned interrupted-session failure block or its
exhausted system recovery and blocked file recovery with an accepted
result.
3. Import the local Git source or a shared alias through
`migrateRemoteWorkspace`.
4. Before this change, both actual importer tests reject the source.
With this change, both save and restore its files while retaining the
hold.
**Paperclip version or commit**
Reproduced on master at `b2af45f0f1` with local PostgreSQL 18.1 on macOS
arm64 and Node 26.4.0.
## What Changed
- Reuse the exact prepared-source failure proof for ambiguous native
session interruptions.
- Admit an exhausted export hold only with its exact run, blocked task,
unowned finalizer, accepted result, original allocation, and current
recovery receipts.
- Bind the complete accepted result into the publication fingerprint.
Keep the existing source, alias, Git, finalizer, and task locks.
- Add real importer, authenticated restore, refusal, and
publication-race controls. Document the separate local and remote
obligations.
## Verification
- Baseline: the exhausted-export importer case failed at
`remote_workspace_migration_blocked:online_workspace_source_scope_unproven`.
- Review found that the first interruption fixture used the
artifact-failure resume mode. The corrected fixture reads
`nativeProviderRecoveryEvidence` and `nativeSessionFailureDisposition`.
Its canonical ambiguous interruption case failed at the source guard
before the cause-specific mode repair.
- Fresh recorded metadata then showed that an interrupted source can
have a boolean `providerSessionEstablished: false`. The final fixture
derives that exact false+ambiguous state from a checkpoint without a
provider identity or provider events. It failed against the intermediate
shared-true guard. The final cause-specific proof passes it. The
artifact-failure case still requires true+exact mode.
- Both importer cases remove the originals and restore saved files
through the real encrypted snapshot owner. They keep the failed run
status, native source reference, result bytes, tasks, recovery actions,
retained allocations, and unrelated active remote work unchanged. The
existing migration owner adds only its local source recipe.
- Final affected PostgreSQL suites: **107/107 passed**, 74.44 seconds,
canonical shared PostgreSQL wrapper with one worker and cleanup
complete. Controls reject the wrong interruption mode, a string session
flag, unreleased owners, active controllers, stale fingerprints, changed
allocations, changed recovery state, and changes during upload. Command
from `server`: `node ../cli/node_modules/tsx/dist/cli.mjs
../packages/db/src/test-postgres-command.ts -- node
../node_modules/vitest/vitest.mjs run
src/services/remote-workspace-online-source.test.ts
src/services/online-legacy-workspace-recovery.test.ts --fsModuleCache
--maxWorkers=1 --hookTimeout=90000`.
- Final server source and all test typecheck layers passed. Module
boundary check and `git diff --check` passed.
- A first setup attempt could not load the unbuilt plugin SDK and ran
zero tests. Building the SDK corrected the setup. An unchanged-baseline
run repeated source refusals after a local edit used the wrong path. The
first 105-case and 106-case passes predate the final producer correction
and are superseded by the 107-case pass. Earlier failures and passes are
retained separately.
- A read-only probe loaded the exact committed proposed inspection code
against three current stored holds in one repeatable-read snapshot. All
three passed the actual function. It verified the owned accepted result
and both interruption fields without changing source or provider state.
This is applicability proof; an import must still acquire its locks and
recheck after deployment.
- The first final-head CI run failed browser shard 5 at
`cloud-auth.spec.ts:34` with an APIResponse disposed during route
teardown. After the workflow completed, one scoped failed-job retry
passed that shard and its browser aggregate on the same head. The
initial failure is retained. No code, test, timeout or assertion changed
for that retry.
- `verify --explain` returned 2 because the documentation path requires
broad CI. Required CI passed on
`2050fa058cabe75e3d8f64f1f6bdb4b9b2fd0873`. Fresh Greptile review is 5/5
with zero open threads. No hosted workspace, provider, task, or recovery
state was changed for this PR.
## Risks
The new proof could accept an inconsistent recovery producer. Exact
company, run, task, allocation, action, result, and receipt checks keep
uncertain sources blocked. Changed evidence during capture prevents
publication. A saved local snapshot is not proof that remote files were
recovered, and this change never creates such a receipt.
## Model Used
OpenAI GPT-6 (Codex), with reasoning, tool use, and code execution. The
runtime does not expose a more specific model ID or context window.
## 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>
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - A user's follow-up comment or Retry must be delivered once,
eventually, even when a temporary company admission hold (accounting
still settling) is active
> - #19741 kept claimed runs queued through such a hold and woke held
work when it cleared
> - A restart-chaos wave still lost a comment, because its wake was
refused before admission and written as a terminal skip
> - The same wave left a Retry waiting forever for a stop record of a
process that never existed
> - This pull request saves the refused comment as a deferred receipt,
and routes input after a pre-launch quarantine failure to the
quarantine's own gate
> - The benefit is that the comment is delivered after the hold, and the
task asks for the action that actually unblocks it instead of offering a
Retry that can never start
## Linked Issues or Issue Description
Refs #19741 and #19702.
**What happened?**
In a restart-chaos wave, a failed run's accounting held the company for
600 s.
1. A user commented while the task's run was live. The enqueue budget
gate refused the comment's wake before it reached the deferral behind
the live run. Because it was a user request with no saved receipt, it
was written as `skipped` (`budget.blocked`). The live run finished
without it and the task ended `done`, so the comment was lost.
2. A Retry created a run that the native cleanup-quarantine fence
refused before it requested any runner launch
(`native_session_cleanup_quarantined`, no process events). A second
Retry then waited with `process_identity_missing` ("The previous run has
no verified stop record") for a process that never existed. The task
kept offering Retry, and that Retry could never start.
**Expected behavior**
A comment refused by a temporary hold stays saved and is delivered once
later. Input after a run that was refused before launch is handled by
the quarantine gate: Retry is not offered (send a new message), and a
new message gets the verified-stop check of the previous provider
session.
## What Changed
- `heartbeat/queue.ts`: when the enqueue budget gate refuses a user's
comment wake and the block is temporary (not a reached hard stop, per
`isBoardOwnedBudgetBlock`), the wake is saved as a
`deferred_issue_execution` receipt. The receipt carries the comment ids
and an `executionWait` of `budget.blocked`, and the gate notifies the
deferred-receipt worker and throws `PersistedExecutionWaitRefusal`. It
is not written as `skipped`. Normal promotion (when the live run ends)
and #19741's hold-release wakeups deliver it.
- `native-local-process-stop.ts`: new `nativeRunRefusedBeforeLaunch`. It
returns true for a failed native run with
`native_session_cleanup_quarantined`, no recorded process, and no
launch-request or identity event.
- `explicit-native-continuation.ts`: such a run passes the
process-identity check, so the existing quarantine branch decides. A
Retry gets `cleanup_quarantined` (shown with `canRetry: false` and "send
a new message"). A new message gets
`verifyStoppedNativeSessionForContinuation`. Any launch request or
identity record keeps the original stop proofs.
- `native-session-executor.ts`
(`verifyStoppedNativeSessionForContinuation`): when the source has no
process record and no launch event, the verifier relies on the previous
owner's stop evidence. Before this change it required the source to
share the owner's process identity, so a new message stayed held even
after the owner stopped. A source that carries a process identity must
still match the owner exactly.
## Verification
Each new test fails on `master` and passes with this change, on embedded
Postgres:
- `server/src/__tests__/heartbeat-held-comment-wake.test.ts`: a comment
whose wake the hold refuses at enqueue is saved with a `budget.blocked`
wait and is not skipped. It is delivered exactly once after accounting
settles.
- `server/src/services/explicit-native-continuation.test.ts`:
- A run refused by the quarantine before launch routes a new message to
the quarantine's stop verification and a Retry to `cleanup_quarantined`.
These failed on `master` with `process_identity_missing`.
- Guards (green on both): a quarantined run that requested a launch, and
another failure without a process record, still require
`process_identity_missing`.
- `server/src/services/quarantined-task-retry.test.ts`: a never-launched
quarantined source is certified once the previous owner has stopped (red
on `master`). It stays held if the source requested a launch or the
owner is still running. All 31 tests pass, and the `stopped native
conversation physical cleanup` suite passes (46).
- Related suites pass locally (6 files, 620 tests): explicit native
continuation, held comment wake, queued comments routes, comment wake
batching, refused assignment wake, deferred receipt work. Two
`deferred-execution-receipt-work` tests failed once under parallel load
and passed when run alone.
## Risks
- The saved-comment path applies only to user comment wakes refused by a
temporary budget hold. Hard stops, system wakes, case handoffs, durable
chat requests, and existing saved receipts are unchanged.
- The pre-launch rule is limited to the quarantine failure code with no
launch evidence. Every other missing process identity still requires the
existing stop proof.
## Model Used
- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, run as an agent in
Claude Code with tool use (shell, file editing, local test execution,
kept restart-chaos instance inspection).
## 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>
The prerequisite task that scripts Morgan's complaint lived in a visible
project. The chat agent hit "Board access required" on commentary, then
searched tasks, read the scripted text and reported it as feedback, so
the case tested the fixture instead of the permission boundary.
- Setup: the scripted-feedback task now lives in a private project shared
only with the submitting worker and the human reader. Setup asserts the
chat agent gets 403/404 on both scripted sources.
- Grade: the three regular expressions over the reply's wording are gone.
The check keeps the agent's 403 on commentary and the commentary-view
link, and adds that the reply does not relay the setup's own feedback
text (setup-chosen literals). Oracle agent-feedback.v2.
Calibrated on the retained run: it still fails (relayed text, no link);
the same state with an honest redirect passes in two wordings; a redirect
that relays the text fails.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - Agent runs must survive a control-plane restart: they must continue
on their own, keep their work, and offer a Retry that works
> - The 2026-11-06/07 restart chaos campaign found about 40 restart
failures and merged about 69 fixes (#19469 through #19807)
> - Most of these failures have one shape: a restart stops a process
between two durable writes, and each subsystem then decides on its own
whether the leftover record is safe to release
> - Each fix closed one crash window, and about 12 fixes rebuilt the
same "this owner is dead" proof
> - This pull request adds a plan for one shared ownership record,
write-ahead intents, one reconciler, and an on-demand crash-point suite
> - The plan also makes time to first token and agent boot hard
requirements, so restart safety never makes turns slow
> - The benefit is that we fix the whole class of failure once, and we
stop fixing one window at a time
## Linked Issues or Issue Description
Refs #19696, #19713, #19717, #19724, #19741, #19747, #19784, #19807
**What existing behavior does this improve?**
Recovery after a control-plane restart. Today each subsystem keeps its
own in-progress records and its own release rules.
**Subsystem affected**
Native runtime, runner, environments and sandboxes, workspaces,
accounting and budgets, and the wake queue.
**Current behavior**
A restart can leave a lease, a fence, a claim, a workspace operation, a
receipt, or a deferred wake that belongs to a dead process. If no
subsystem can prove that the owner is dead, the system fails closed. The
task then needs a Retry, the company holds new work, or a run stays
`running`. About 40 server files read a controller boot id, and about 25
files check process liveness in different ways.
**Proposed behavior**
One owner stamp and one liveness proof for every in-progress record.
Intents are written before outside effects. One reconciler brings every
record to a terminal state. Admission never waits on cleanup. Accepted
work is delivered exactly once. Time to first token and agent boot are
measured and stay within budgets.
**Reason and benefit**
Fewer restart failures, less duplicate code, and a fast first response
after a restart.
## What Changed
- Added `doc/plans/2026-11-07-durable-ownership-and-reconciliation.md`.
The plan contains:
- the five failure classes from the campaign, with example IDs and PR
counts
- eight invariants
- a performance section with time-to-first-token, boot, and
restart-latency metrics and rules
- the design (sections A–G)
- an on-demand crash-point suite
- six phases
- risks
- the decisions that Dotta made on 2026-11-07
## Verification
- This change adds documentation only. No code changes.
- Read the plan. Check that each failure class cites real merged PRs.
- Check the code claims:
- `git grep controller-process-identity -- server/src` shows three
users.
- `git grep task.time_to_first_token -- server/src` shows the existing
metric.
## Risks
- Low risk. The change adds one plan document and changes no code.
- The plan proposes later behavior changes. The largest is the removal
of the company-wide accounting gate. Each later phase will come as its
own pull request with tests.
## Model Used
- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used in Claude Code
with tool use (shell, git, GitHub CLI). The model coordinated the chaos
campaign and wrote the plan from its evidence.
## 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>