Commit Graph
6923 Commits
Author SHA1 Message Date
DottaandClaude Opus 5.5 0189fedffc fix(native): bound an idle warm local runner whose controller died (#19879)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A warm runner (`lifecycleMode: warm`) keeps runnerd alive between
runs so a follow-up turn starts fast. The follow-up default flip (next
PR) makes warm the default for local Codex and OpenCode agents.
> - The controller owns a warm runner's idle window. Runnerd ignores the
idle timeout itself.
> - A graceful controller shutdown suspends idle warm sessions. A
controller that dies (SIGKILL, crash) does not. Local runners launch
with no process reconnect grace, so the idle runner kept running until
its 1-hour connection lease ran out, owned by nobody.
> - This pull request gives warm local runners the 7-minute reconnect
grace that remote runners already use.
> - The benefit: idle runners are reaped by proof within a bounded time,
never left orphaned, before warm becomes the default.

## Linked Issues or Issue Description

Refs #19874 (warm completion contract), #19846 (TTFT baseline), #19852.

**What existing behavior does this improve?**
Ownership of an idle warm local runner after its controller dies.

**Current behavior**
`runnerProcessReconnectGraceMs` is `undefined` for local targets, so
runnerd launches without `--reconnect-grace-ms`. An idle warm runner
whose controller was killed stays alive until its connection lease (one
hour) expires. The restart-chaos harness cannot show this: its K1 kills
the controller's whole process tree, runner included.

**Proposed behavior**
`localRunnerProcessReconnectGraceMs(policy)` returns the remote grace (7
minutes) for a warm policy and `undefined` for per-turn. After the
grace, runnerd records `transport_reconnect_grace_exceeded`, stops its
provider, keeps durable state and exits. The next run resumes cold.

**Reason and benefit**
The restart plan requires idle runners to be reaped by proof, never
orphaned. Seven minutes is far longer than a controller restart (13–20 s
locally) and the long-outage fault holds (3–5 minutes), so a live turn
still reattaches.

## What Changed

- `native-runner-recovery-contract.ts`:
`localRunnerProcessReconnectGraceMs`.
- `native-session-executor.ts`: the local runnerd transport passes it as
`runnerProcessReconnectGraceMs`. Remote targets keep `null`
(lease-owned).
- `runnerd-warm-idle-orphan.test.ts` (new): real runnerd and fake Codex.
An idle warm runner, after its turn completed, whose controller
detaches.
- `doc/DEVELOPING.md`: the grace and its bound.

## Verification

- `pnpm --filter @paperclipai/paperclip-runner exec vitest run
src/live/runnerd-warm-idle-orphan.test.ts`: with a process grace, the
idle runner exits with `transport_reconnect_grace_exceeded` and the next
transport resumes the same provider session cold. With no grace (`null`,
today's local launch), the runner is still alive after the controller
left.
- `cd server && npx vitest run
src/services/native-runtime/native-runner-recovery-contract.test.ts`:
warm gets the remote grace; per-turn keeps none; the grace outlasts a
controller restart and a long outage.
- `tsc --noEmit` passes for `server` and `packages/paperclip-runner`.
- Local restart measurement with warm runners (K1/K2, codex, claude,
opencode, n = 3 each): every interrupted run recovered, with recovery
latency equal or lower than per-turn. The chaos waves with this change
are in the default-flip PR.

## Risks

- A warm local runner whose controller stays down for more than 7
minutes exits. A turn in progress then resumes through
`resume_dead_runner`, the same path as a controller crash that kills the
runner. Remote runners already behave this way.
- Previous-release story (`rules/upgrade-safe-run-state.mdc`): no
persisted state changes. A warm runner launched by the previous release
keeps the launch arguments it started with (no grace) until it exits.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with
tool use (shell, file edit, test execution).

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 13:11:18 -06:00
Harjoth KharaandClaude Sonnet 5.5 d53fa727e1 docs: use a supported agent role in role examples (#16449)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Every agent has a role, and the API only accepts the values in
`AGENT_ROLES` (`z.enum(AGENT_ROLES)` in
`packages/shared/src/validators/agent.ts`)
> - The board-operator guide, the API docs and the agent-facing API
reference are where operators and agents learn which role values to send
> - They name `manager` as a role, but `manager` is not in
`AGENT_ROLES`, so creating an agent with it is rejected with
`invalid_value`
> - This pull request replaces the unsupported value with supported ones
in the guide and in the two places that show the same invalid value in
an example
> - The benefit is that the documented role values match what the API
accepts

## Linked Issues or Issue Description

- Fixes: #15687

## What Changed

- `docs/guides/board-operator/managing-agents.md`: the Role list now
reads `ceo`, `cto`, `pm`, `engineer`, `researcher`, etc. (`pm` replaces
the unsupported `manager`). The next bullet already says Reports to is
the agent's manager in the org tree, so the hierarchy is still described
and no extra wording was added.
- `docs/api/agents.md` and
`skills/paperclip/references/api-reference.md` (two examples): the same
invalid value appeared as `"role": "manager"` in response examples for
an "EngineeringLead" / "VP Engineering" agent. Changed to `cto`, which
is a supported role and fits those examples. These are single-word
changes made so the documented examples do not contradict the guide.
- Left alone on purpose: ordinary uses of the word "manager" in prose
(reports-to wording, AWS Secrets Manager), the "Worked Example: Manager
Heartbeat" heading, the planning docs under `doc/plans/`, and a code
test fixture (`ui/src/components/RoutineOverview.test.tsx`).
- No code, validation or enum change; no new role.

## Verification

- Before: the guide lists `manager`, and `AGENT_ROLES`
(`packages/shared/src/constants.ts`) is `ceo, cto, cmo, cfo, security,
engineer, designer, pm, qa, devops, researcher, general`, with no
`manager`; the create validator uses `z.enum(AGENT_ROLES)`.
- After: every role value named in the changed lines (`ceo`, `cto`,
`pm`, `engineer`, `researcher`) is in `AGENT_ROLES`. `git diff --check`
is clean. `pnpm-lock.yaml` is unchanged.
- This is a docs-only change (3 files, 4 lines), so I did not run the
test suite and did not add a test; there is no docs lint script that
runs without installing dependencies.
- To check by hand: send `POST /api/companies/{companyId}/agents` with
`"role": "manager"` (rejected with `invalid_value`) and with `"role":
"pm"` (passes role validation).

## Risks

- Low. Documentation only, no runtime behavior change.
- `cto` in the two example responses is a judgment call for the
"EngineeringLead" / "VP Engineering" sample; any supported role would
also be valid there. The two extra files are example-only and can be
dropped if a maintainer prefers this PR to touch only the guide.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- Implementation: Anthropic Claude Sonnet 5.5 (`claude-sonnet-5-5`),
medium reasoning effort, with tool use and code execution through Claude
Code. Context window not reported by the runtime.
- Coordination and this description: Anthropic Claude Sonnet 5.5
(`claude-sonnet-5-5`).
- Independent review of the exact head: OpenAI Codex `gpt-5.6-sol`,
medium reasoning effort, read-only, no findings.
- The issue was picked by an automated screening agent; a human
contributor directed the work and owns the change.

## 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
- [ ] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 13:06:25 -06:00
DottaandClaude Opus 5.5 ff53eb84db feat(recovery): add OwnerStamp and proveOwnerGone (gone | alive | unprovable) (#19853)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Restart recovery must decide when a dead process's in-progress
record may be released
> - About 31 sites decide "owner gone" today, with different rules for
EPERM, PID reuse and other hosts (Phase 0 inventory, finding 1)
> - The durable ownership plan (section A) asks for one owner stamp and
one prover that returns `gone | alive | unprovable`
> - This pull request adds that stamp and prover. Nothing calls it yet;
the next PR uses it in a report-only reconciler
> - The benefit is one tested definition of "owner gone" that later
phases move each record kind onto

## Linked Issues or Issue Description

Refs #19831 (plan, section A and Phase 1). Refs #19846 (Phase 0
inventory). Refs #19848 (Track 1: 1 s start-time gap and EPERM as
alive). Stacked on #19851.

**Problem**
Each subsystem proves "owner gone" with its own rules. Some accept a
dead PID on another host as proof. None returns "unprovable".

**Expected behavior**
One prover with one set of rules. A different host and an expired fence
alone are never proof. "Unprovable" never releases a record.

**Impact**
This is the primitive that Phase 1's report-only reconciler and Phase
2's migrations use.

## What Changed

- New `server/src/services/owner-proof.ts`:
- `OwnerStamp`: a local stamp (`host`, `processSpace`, `bootId`, `pid`,
`processStartedAt`, `processGroupId`, `generation`, `scope`) or a remote
stamp (`fenceToken`, `generation`, `scope`, `provider`).
- `proveOwnerGone(stamp, …)` returns `{ verdict: "gone" | "alive" |
"unprovable", reason }`.
- Local rules: this boot is alive; the stamp must name this host and PID
space (host, kernel boot, PID namespace); the PID must be dead or reused
(start-time gap of at least 1 s, via Track 1's
`processStartsDefinitelyDiffer`); EPERM is alive; the process group must
be empty or led by a newer process.
- Remote rules: a superseded fence token or generation is gone; a
provider stop proof is gone; a held fence is alive; a matching fence
that only expired is unprovable. Process liveness is never consulted for
a remote owner.
- `currentLocalOwnerStamp()` reads only cached identity, so writing a
stamp adds no OS call to the turn path.
- `controller-process-identity.ts` exports `observedProcessStartedAt`
and `UNKNOWN_PROCESS_SPACE` for the prover.
- New `owner-proof.test.ts`: the proof matrix (EPERM, ESRCH, PID reuse
inside and at 1 s, unreadable start time, a different host, a different
or unknown PID space, process groups, a held, superseded, expired or
provider-proved remote fence).

## Verification

- `npx vitest run src/services/owner-proof.test.ts` (in `server/`): 20
tests pass.
- `npx tsc --noEmit -p server`: no errors.

## Risks

- Low risk. No caller uses the prover yet, so no behavior changes.
- Previous-release story (`rules/upgrade-safe-run-state.mdc`): no
persisted shape changes. Stamps from older rows that lack fields return
`unprovable` (`missing_identity`), which never releases a record.
- Track 1 is still fixing proofs in place. The prover reuses Track 1's
helpers (`processExists`, `processStartsDefinitelyDiffer`), so a later
Track 1 rule change applies here too.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, in Claude Code with
tool use and code execution.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] 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>
2026-11-07 13:04:55 -06:00
DottaandPaperclip 965ca91dd0 Capture legacy workspace cohorts under one source fence (#19873)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Workspace migration preserves local files as authenticated portable
snapshots.
> - Workspaces that share Git storage require one complete source fence.
> - Independent imports contend on that fence and cannot safely run as a
batch.
> - This change adds an opt-in cohort owner that captures members
concurrently and publishes them atomically.
> - Existing source, recovery, snapshot and publication checks remain
authoritative.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

The administrative remote-workspace migration owner for workspaces in
one shared physical source component. Related changes: #19688, #19854
and #19855.

**Current behavior**

Each independent import acquires the complete source fence. Parallel
independent calls therefore contend instead of overlapping archive work.
A caller cannot use a surrounding transaction and assume that a
successful savepoint confirms the outer commit.

**Proposed behavior**

An explicit `migrateRemoteWorkspaceCohort` call selects one to eight
leaves from one proven component. It owns the full
source/lifecycle/copyback fence, uses the original pool for bounded
archive work, validates every member, publishes serially, and returns
success only after the outer commit.

**Reason and benefit**

This supplies a correctness-gated path for a controlled two-member
hosted qualification. It makes no wall-clock speedup claim. No running
app call site or scheduler selects cohorts automatically.

**Breaking changes**

The existing single-import API retains its guards and rejects a journal
owned by a cohort. New cohort journals require the same complete member
set on resume.

## What Changed

- Extract the existing reservation, capture, validation and publication
stages for reuse by both owners.
- Add an opt-in cohort API with default concurrency two and a maximum of
eight. Every member must share the same complete physical/Git/alias
component; selected roots must be leaves relative to each other.
- Persist exact cohort and operation membership in the existing journal.
Compare all selected fingerprints with the aggregate proof before the
first reservation under the complete fence, and recheck each member when
reserving its journal. Refuse foreign journals, incomplete or mixed
replay sets, lost fences and changed sources. Keep all authorities and
activity events unpublished if the outer commit fails.
- Share the merged nested-child verifier inside one complete fence.
Queue its prepare calls, retain its two-child bound and authenticated
immutable proof, and recheck local bytes and database bindings on every
pass.
- Document the API, exact-membership resume protocol, resource bounds
and required hosted qualification.

## Verification

- Integration head `fce1896010`: 32/32 PostgreSQL/filesystem cases
across the cohort owner, the complete merged nested-verifier suite, and
five existing parent-import cases.
- Final source-proof repair: 19/19 cohort-owner cases pass, including
real first- and last-member metadata changes before reservation,
changed-source partial-resume refusal, mixed local/imported refusal with
storage forbidden, cancelled/failed/finished admission, a late finished
timestamp, complete fences, queued child-proof reuse, cold restore,
cancellation, commit failure and exact replay. All local database checks
use the canonical owned wrapper, one Vitest worker and Node 24.22.2 on
macOS arm64/PostgreSQL 18.1.
- Final rebased head is based on
`fbe689938c3bf8c5849d2cd56b9da90029db9389`, including current
source-owner, database-schema and Runner contract changes. The 19-case
owner suite, full server layered source/tests/test-tree typechecks,
module boundaries and diff checks are rerun on that base; required CI
supplies the native Runner qualification.
- Selection-only `node scripts/local-verify.mjs --base
68db7ed9e3 --explain` exceeded its
60-second observation bound before producing a plan. It supplies no test
pass; required CI must supply broad coverage.
- Genuine replay RED: a committed two-member cohort accepted a
one-member replay. The repaired owner requires its full
succeeded-journal/snapshot membership.
- Genuine terminal-journal RED: read-only preflight accepted a
cancelled, finished saved member, and apply created two pending
snapshots and the missing peer journal before refusing later. The repair
rejects that state before preflight eligibility or snapshot work and
rechecks unfinished state before publication.
- Genuine source-proof race REDs: early workspace metadata drift could
strand a partial journal. A first repair checked each member only when
reserving it; an additional last-member regression showed that an
earlier journal could still be written before noticing the changed
member. The final owner compares the complete preimage in the existing
fenced preflight before the first reservation and retains
per-reservation checks. Both first- and last-member cases now leave zero
journals/snapshots and permit a fresh attempt. Changed sources after a
genuine partial reservation still refuse.
- Coverage includes parallel uploads, cold restore with originals
removed, complete source/copyback fences, exact partial-reservation
resume, actual read-only preflight with storage forbidden,
foreign/changed journals, peer failure and cancellation, fence
connection loss, and a real deferred outer-commit failure.
- Prior head `70bc2740d0` passed all CI after one explicitly bounded
same-head retry of server shard 4. Its first attempt observed an
unrelated heartbeat/plugin test still running at a five-second wait;
automatic test retry then hit the fixed-name environment uniqueness
constraint. The test and 1,100 relevant owner files matched the nearest
successful base gate. The one job retry passed the target test in
3,491ms and all 3,045 shard tests. Original failures remain recorded;
their initial cause is unknown. This does not supply CI acceptance for
the later repair head.
- Earlier prototype evidence remains historical: 286 PostgreSQL controls
and twelve cold restores passed on its prior base. Six macOS timing runs
were inconclusive under contention, including slower cohort runs. They
are not speedup or hosted acceptance evidence.
- Hosted acceptance remains a two-member same-component leaf canary with
no imported children, followed by same-host timing, peak-memory and
scratch-headroom observations before considering a larger batch.

Run the focused owner file from `server/` with the canonical temporary
PostgreSQL owner:

```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/remote-workspace-cohort-migration.test.ts --fsModuleCache --maxWorkers 1 --hookTimeout 90000
```

Final head `112ffeb20c272c6ef5e1e0956be889c85511f0d0` passed [required
CI](https://github.com/paperclipai/paperclip/actions/runs/42402942287)
with 56 successful checks and four skips; the aggregate is green.
[Greptile reviewed this exact head at
5/5](https://github.com/paperclipai/paperclip/pull/19873#issuecomment-6710780943),
and a fresh paginated review-thread read has zero unresolved findings.

## Risks

- Concurrent archive work increases CPU, memory, scratch space, database
and storage load. Admission must count logical member workers and up to
two nested-child workers, not Node processes. Eight-member use remains
unqualified.
- The complete cohort fence lasts until all started work joins. Slow
callbacks can delay cancellation and associated source writers.
- The exact journal exception is internal and binds company, cohort,
workspace and operation IDs. Wrong or changed membership fails closed;
operators must inspect unknown outcomes before resuming the same full
set.
- Existing remote recovery holds, provider obligations, source files and
history remain preserved. A successful import does not settle those
independent obligations.

## Model Used

OpenAI GPT-6 (Codex), with reasoning, code execution and repository
tools. This runtime does not expose a more specific backend model ID or
context-window size.

## 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>
2026-11-07 13:02:54 -06:00
Can H. Tartanoglu 410aa6d1b4 test(runner): resolve provider fixture signals through PATH (#14476)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Its native runner tests verify exact provider shutdown and recovery.
> - Four fixture call sites invoke `/bin/kill` directly.
> - NixOS supplies `kill` through PATH, so those tests fail before
checking process state.
> - This pull request resolves `kill` through PATH, as the neighboring
runner tests already do.
> - The same stop, probe, and resume assertions can run on non-FHS
hosts.

## Linked Issues or Issue Description

**What happened?**

Four Codex provider tests fail with `No such file or directory` on
NixOS. Each failure comes from a test-owned `/bin/kill` invocation. The
executable is installed on PATH.

**Expected behavior**

Tests should probe and signal their disposable provider processes with
the installed `kill` command.

**Steps to reproduce**

On a NixOS host with `kill` on PATH and no `/bin/kill`, run the
`codex_provider` Rust integration target. Three `durable_stop_*` cases
and `structured_question_round_trips_through_the_normalized_backend`
fail. All four failures were reproduced separately on unmodified
`e9debd3eac328e6abe87ab89e9ff40ee2bb56b73`.

Related: these failures also blocked the broader local qualification of
#14474. This fixture fix has no dependency on that runtime change.

## What Changed

- Replace four test-only `/bin/kill` executable paths with `kill`.
- Retain every signal, PID/process-group operand, assertion, and unwind
cleanup.
- Match existing PATH lookup in this file and
`tests/process_supervisor.rs`.

## Verification

Rebased onto master `820c458465` (single commit `ad1fb1043`).

- `cd packages/paperclip-runner/runner && cargo test -p
paperclip-runner-core --test codex_provider --locked -j4 --
--test-threads=2`: 94 passed, 0 failed, 2 ignored (subprocess helper
entries that their parent tests run). The four tests that used
`/bin/kill` pass on a host without `/bin/kill`.
- No production code or documentation changes.
- Hosted CI: `General tests (workspaces-b)` timed out in
`packages/db/src/backup-lib.test.ts` and `e2e shard (1/8)` hit
`ERR_STREAM_UNABLE_TO_PIPE`. Neither area is touched by this test-only
diff. Master pushes currently run the "CI profile skipped" workflow, so
there is no master run of those jobs to compare against.

## Risks

Low risk. Tests require a working `kill` on their test PATH. Fixture
PIDs and arguments stay unchanged.

## Model Used

- Anthropic Claude Sonnet 5.5 (`claude-sonnet-5-5`) through Claude Code
with tool use and code execution, for the rebase and verification.
Original change: OpenAI `gpt-6-astra` through OpenCode.

## 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
2026-11-07 13:02:29 -06:00
Devin Foley fbe689938c docs(release): canonicalize stable notes for v2026.1106.0 (#19770)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The release process promotes a soaked beta to stable. The stable run
publishes the curated notes from `releases/beta/v<beta-version>.md` on
`master` as the GitHub Release body
> - After the publish, the notes must move to
`releases/v<stable-version>.md` on `master`. That path is the canonical
location that the website changelog and the next release notes use
> - Stable `v2026.1106.0` was published on 2026-11-06 from beta
`2026.1023.0-beta.0`. The `canonicalize_stable_notes` job in run
42295041150 was skipped, so the workflow did not push this branch
> - This pull request does by hand what that job does: it renames the
beta notes file to the stable path. It changes no content
> - The benefit is that `releases/v2026.1106.0.md` exists on `master`
and is byte-identical to the published GitHub Release body

## Linked Issues or Issue Description

Refs #17400
Refs #19607

**Issue type**

Missing content

**Where is the issue?**

`releases/` on `master`. The stable notes for `v2026.1106.0` are only at
the beta path `releases/beta/v2026.1023.0-beta.0.md`. The canonical path
`releases/v2026.1106.0.md` does not exist.

**What's wrong?**

The GitHub Release
https://github.com/paperclipai/paperclip/releases/tag/v2026.1106.0 is
published, but the `canonicalize_stable_notes` job of the release run
was skipped. The website changelog sync reads `releases/v2026.1106.0.md`
on `master`, so the changelog page for this release cannot appear until
this rename is merged.

**Suggested fix**

Rename the beta notes file to the stable path with no content change.
This is the same step the workflow job performs.

## What Changed

- `releases/beta/v2026.1023.0-beta.0.md` is renamed to
`releases/v2026.1106.0.md`. Git reports a 100% rename
- No line of the file changes. The blob is
`16f05f22d4d71de3a568b493de2ddcb8eadb7859`, the same blob the release
run pinned at `master` commit `da9a4cb9` when it created the GitHub
Release
- The branch has two commits (add the stable path, then remove the beta
path) because it was written through the GitHub API. Squashed, it is one
pure rename
- This pull request does not change the release workflow

## Verification

- `git diff --stat -M100%
master...release-notes/v2026.1106.0-canonicalize` shows one renamed file
with 0 insertions and 0 deletions
- `git rev-parse
release-notes/v2026.1106.0-canonicalize:releases/v2026.1106.0.md` prints
`16f05f22d4d71de3a568b493de2ddcb8eadb7859`
- `git rev-parse
da9a4cb96c46945d693f0ddffdcb0e0743649a01:releases/beta/v2026.1023.0-beta.0.md`
prints the same blob
- `gh api repos/paperclipai/paperclip/releases/tags/v2026.1106.0 --jq
.body` piped through `cmp` against the file (without the trailing
newline that `--jq` adds) reports no difference. The release body and
the file have the same bytes
- After merge,
https://github.com/paperclipai/paperclip/blob/master/releases/v2026.1106.0.md
returns 200 and the old beta path returns 404

## Risks

- Low risk: one markdown file moves and no content changes
- The release workflow's `canonicalize_stable_notes` job was skipped
again, as on the `v2026.1023.0` release (#17400). `publish_stable`
succeeded and the notes mode was `master_beta`. The job's `if` has no
status function, so GitHub applies an implicit `success()`. That check
is false when an upstream job in the chain was skipped (jobs inside
`release_runner_binaries` were skipped in this run). A `!cancelled() &&`
guard, as `publish_stable` already uses, fixes it. That fix is not in
this pull request so this stays a pure rename

## Model Used

- Claude (Anthropic), model ID `claude-opus-5-5` (Claude Opus 5.5), tool
use via Claude Code

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)
canary/v2026.1107.0-canary.19
2026-11-07 10:42:47 -08:00
DottaandClaude Opus 5.5 dcaada5f59 fix(native): carry each warm run's own completion contract (#19874)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A warm runner (`lifecycleMode: warm`) keeps runnerd and the provider
process alive between runs, so a follow-up message can start fast.
> - Each run has its own completion contract. The revision changes with
each follow-up, and the agent must name that revision when it finishes.
> - On a warm attachment the transport rebuilt the `run.attach` payload
from its constructor options. Those options belong to the first run.
Every later run kept the first run's contract.
> - The OpenCode proxy checks the finish claim locally against that
stale contract. Every warm OpenCode follow-up was refused ("Semantic
result completion contract revision does not match"), and the agent
looped until the run timed out.
> - This pull request carries each run's own contract into the warm
attachment, and lets runnerd adopt it without a cold provider resume.
> - The benefit: warm follow-ups finish correctly, and they keep the
live provider process, which is the reason to be warm.

## Linked Issues or Issue Description

Refs #19852 (startup sub-spans), #19846 (TTFT baseline).

**What existing behavior does this improve?**
Warm cross-run attachment for native Codex and OpenCode agents.

**Current behavior**
In a local measurement wave with `lifecycleMode: warm`, 4 of 4 OpenCode
follow-ups looped for 8+ minutes. Each finish call was refused with
"Semantic result completion contract revision does not match". The saved
`runAttachTemplate` after the attachment still had revision `1`, while
the follow-up turn named revision `2`. Codex hid the defect because its
finish claim goes through the controller's tool path, which uses the
current input.

**Proposed behavior**
The attachment carries the attaching run's contract. Runnerd updates the
contract in place for Codex and OpenCode and keeps the provider process.
The OpenCode proxy receives the current contract with the run identity
on `turn/start`.

**Reason and benefit**
Warm follow-ups are the largest TTFT lever in the baseline. A
correctness-only fix (cold-resume the provider when the contract
changes) would make every warm follow-up relaunch Codex or OpenCode,
because the revision changes on every follow-up.

## What Changed

- `attachRun` carries `completionContract` through `NativeSession`, the
harness backend, the Codex harness session and the runnerd transport.
`executeNativeSession` passes the current input's contract. The
transport uses it instead of its first-run `resumeCompletionContract`.
- Runnerd (`codex_provider.rs`, `attach_run_in_place`): a changed
contract no longer forces a cold resume for `codex` and `opencode`.
Codex app-server never receives the contract, and the OpenCode proxy now
receives it per turn. Other providers keep the cold-resume boundary. A
changed tool set still forces it.
- Runnerd sends `paperclipCompletionContract` with each OpenCode
`turn/start`, next to the existing `paperclipRunId`.
- The OpenCode proxy passes that contract to `attachRun` when the run
changes. `OpenCodeHarnessSession.attachRun` replaces its envelope's
contract and validates it.

## Verification

- `src/live/runnerd-codex-transport-warm-attachment.test.ts` (new case):
real runnerd and fake Codex. After a warm attachment with a new
contract, the saved `runAttachTemplate` has the new revision and the
Codex pid is unchanged.
- `src/cli/opencode-proxy-input.test.ts`: the real proxy bundle serves a
warm second run with revision `proxy-v2`. The second
`paperclip/runResult` is produced and names `proxy-v2`. Without the fix
the finish call is refused.
- Each test fails without its part of the fix: the proxy test without
the proxy change; the transport test without the transport change (stale
saved revision); and, with `PAPERCLIP_ATTACH_TRANSITION_RUNNER` pointed
at a runnerd built without the `codex_provider.rs` change, the Codex pid
changes.
- `cargo test -p paperclip-runner-core --lib warm` (11 tests),
`native-session-runtime.test.ts` (232), the OpenCode driver, backend and
warm-attachment suites, and `tsc --noEmit` for the runner package pass.

## Risks

- Previous-release story (`rules/upgrade-safe-run-state.mdc`): no
persisted shape changes. Saved `runAttachTemplate` values carry forward,
and the next attachment writes the current contract. A new runnerd with
an old OpenCode proxy behaves as today: the proxy ignores the new
parameter and keeps the stale contract. An old runnerd sends no
parameter, so a new proxy keeps its launch contract.
- In-place adoption widens the warm-retention path for Codex and
OpenCode to changed contracts. The idle, accounting and pending-work
checks still apply first.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with
tool use (shell, file edit, test execution).

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 12:40:51 -06:00
DottaandClaude Opus 5.5 5a20fca9f7 fix(recovery): judge an orphaned run's pid only in the PID space that recorded it (#19871)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The orphan reaper (`terminalizeOrphanedRunningRun`) ends a `running`
run whose recorded process is gone
> - It judged "gone" with `kill(pid, 0)` on the local host, wherever the
process actually ran: another host, another PID namespace, or a sandbox
> - So a run whose process is alive elsewhere could be terminalized
while it still works
> - #19868 fixed the same gap for native coordinator takeover and the
runtime-control sweep. This pull request finishes finding 2 for the
reaper
> - The benefit is that a dead local pid never proves an owner on
another host is gone

## Linked Issues or Issue Description

**Bug description**
The Phase 0 ownership inventory (finding 2) names the orphan reaper
`terminalizeOrphanedRunningRun` (`recovery/service.ts`) as a proof that
accepts "the pid is dead here" for a process anywhere. `heartbeat_runs`
recorded `process_pid` with no PID space, and a sandbox pid could
collide with a local one.

**Steps to reproduce**
1. A run records `process_pid` on host A, or in a sandbox.
2. On host B, that pid number is not alive.
3. B's stale-lock sweep terminalizes the run as `orphaned_running_run`.

**Expected behavior**
The process-death authority applies only in the PID space that recorded
the pid. Issue-terminal authority is unchanged.

Refs #19846, follows #19868

## What Changed

- Migration `0442`: add nullable `heartbeat_runs.process_host_hash`.
- `heartbeat/run-lifecycle.ts` `persistHeartbeatRunProcessMetadata`, the
one writer of `process_pid`, stores `currentProcessSpaceHash()` for a
local pid and `"remote"` for a sandbox pid.
- `recovery/service.ts`: the process-death authority runs only when
`process_host_hash` is null (rows saved before this column, for one
release) or `sameProcessSpace` matches.
- Tests: the reaper leaves a dead pid alone for another host, a sandbox,
and an unreadable space, and terminalizes it in this space. The writer
stamps local and remote pids.

## Verification

- `npx vitest run
src/__tests__/recovery-stale-issue-lock-sweep.test.ts`: 46 passed.
- Red before the fix: with the old `recovery/service.ts`, the three
cross-space cases fail (the run is terminalized). Green after.
- `pnpm db:generate` produced `0442_low_salo.sql` (one `ADD COLUMN`),
and the snapshot set was pruned to five. `pnpm --filter @paperclipai/db
check:migrations` passed. Server `tsc --noEmit` shows no errors in the
changed files.

## Risks

- Previous-release story: runs saved with `process_host_hash = null`
keep the previous local proof for this release. That matches #19868,
because stranding in-flight runs at upgrade would be a restart failure.
New rows are strict.
- A run whose process really lived on another host now stays `running`
until issue-terminal authority, its controller lease, or another owner
settles it.
- No new I/O before `turn.start`. The hash is computed at module load,
and the stamp rides the existing process-metadata update.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used with tool use and
code execution in Claude Code.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 12:22:15 -06:00
DottaandClaude Opus 5.5 00e627a23e test(logs): give the 4 MiB archive segment test a 60 s budget (#19877)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native run logs migrate into a bounded object-storage archive
(#19719)
> - One integration test migrates a 4 MiB log and reads it back across a
segment boundary against embedded PostgreSQL
> - On loaded CI runners it exceeds Vitest's 15 s default, so `General
tests (server (9/12))` fails on unrelated PRs
> - This pull request gives that one test an explicit 60 s budget
> - The benefit is that unrelated PRs stop failing on a timing-only
timeout, and the test keeps its exact-bytes assertions

## Linked Issues or Issue Description

Refs #19719 (the test's origin).

**Problem**
`native-log-archive.integration.test.ts > preserves exact bytes across
the bounded archive segment boundary` times out at 15 s on CI. On
2026-11-07 it failed on at least eight unrelated branches, among them
`fix/skill-cache-lock-owner-proof`, `chat-eval/master-candidate`,
`fix/subtasks-own-workspace` and `feat/owner-gone-prover` (three
attempts).

**Expected behavior**
The test passes when the code is correct, regardless of runner load.

**Impact**
It blocks merges on unrelated PRs and wastes reruns.

## What Changed

- Give that single test an explicit 60 s timeout, with a comment that
names the reason. No assertion changes.

## Verification

- `npx vitest run src/services/native-log-archive.integration.test.ts`
(in `server/`): 15 tests pass locally.
- The CI shard `General tests (server (9/12))` should pass on this PR.

## Risks

- Low risk. This is a test-only timeout change. A real hang still fails,
after 60 s.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, in Claude Code with
tool use and code execution.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] 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>
2026-11-07 12:16:49 -06:00
DottaandClaude Opus 5.5 00ad9503fe feat(commentary): agents read only their own commentary unless granted commentary:view (#19842)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents can submit complaints and suggestions as commentary, and
people with commentary access read them on the Commentary page
> - When a person asks an agent in chat what agents have been
complaining about, the agent cannot read commentary and gets "Board
access required"
> - That response does not say where a person can see it, so the agent
can name the feed but cannot link it
> - Product rule: an agent cannot read other agents' commentary unless
it holds `commentary:view`, which is off by default; it may read its own
> - This pull request implements that rule and adds a remediation to the
denial naming the human page for the agent's own company
> - The benefit is a clear permission boundary and a working link for
the person instead of a dead end

## Linked Issues or Issue Description

**What happened?**
In the `activity.agent-feedback` chat eval, the agent correctly said it
could not read agent commentary and that only the person could open it,
but it gave no link, because the denial named no page.

**Expected behavior**
An agent denied a commentary read learns which page a person with
commentary access can open.

**Steps to reproduce**
1. As an agent, call `GET /api/companies/{companyId}/agent-commentary`.
2. Before this change, the 403 body is only `{ "error": "Board access
required" }`.

## What Changed

- `server/src/routes/agent-commentary.ts`:
- An agent with the `commentary:view` grant reads all commentary its
task access allows. The grant is not among the default agent grants.
- Without the grant, an agent may read its own entries: `?agentId=<its
own id>` on the list, or its own entry by ID.
- Without the grant, listing all commentary, filtering by another agent,
or reading another agent's entry returns 403 "Board access required".
The response's `remediation` names `/<issuePrefix>/activity/commentary`
for the agent's own company.
- A request for another company still fails on company access first,
with no page path.
- Board reads are unchanged: `commentary:view` for the user, plus task
read conditions.
- `agent-commentary.integration.test.ts`: one test covers the full-list
and other-agent denials with the page path, own-list and own-entry
reads, the cross-company denial, the grant allowing a full list, and
that `commentary:view` is not a default agent grant.

## Verification

- `npx vitest run src/__tests__/agent-commentary.integration.test.ts` in
`server`: 21 tests passed.
- The new test fails against the previous route.
- Negative control: with the previous route, the new test fails.
- Related commentary tests: `agent-commentary-reader`,
`agent-commentary-helper`, `commentary-permission-migration` passed.
`agent-commentary-skills` fails the same way with and without this
change (a missing skill file in a temporary directory locally).
- The chat eval rerun with this change is recorded in #19614.

## Risks

- Agents can now read their own commentary when they ask for it
explicitly; before, every agent read was denied.
- The denial's status code and message are unchanged; a `remediation`
string is added for same-company agents.
- There is no UI yet for granting `commentary:view` to an agent.
- An agent learns the company's issue prefix, which it already sees in
task identifiers.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code
execution in Claude Code.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
canary/v2026.1107.0-canary.18
2026-11-07 12:04:46 -06:00
DottaandClaude Opus 5.5 05e3309740 fix(accounting): settle a failed usage capture as unpriced at the closure bound (#19857)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Every finished run settles its usage into the cost ledger. Until it
settles, budget admission makes new company work wait
> - When usage capture fails (`accountingCaptureFailed=true`),
accounting skips the run forever. It is also not "recovering", so it
counts in `waitingRunCount`
> - So one failed capture holds new company work until the UTC budget
month rolls over
> - This pull request bounds that wait with the existing accounting
closure timeout. It then settles the run's known usage as unpriced, with
provenance that names the capture failure
> - The benefit is that admission recovers by itself, while unknown
spend stays visible and still counts under a blocking unpriced-usage
policy

## Linked Issues or Issue Description

**Bug description**
The Phase 0 ownership inventory (finding 3) found that runs with
`accountingCaptureFailed=true` are excluded from accounting
(`run-cost-accounting.ts` `actionableRunAccounting` and
`accountRunCost`). They are also excluded from "recovering" in
`budget-exposure.ts`, so they count as waiting until the month window
rolls over. Nothing else releases them.

**Steps to reproduce**
1. Finish a run whose usage capture failed (for example, a fence write
failure or usage after a source handoff).
2. Wait any length of time and run `reconcileRunCosts`.
3. The run stays `costAccountingPending`, and `getInvocationBlock` keeps
holding new company work.

**Expected behavior**
After a bounded wait, the run settles with its known usage marked
unpriced, and held work resumes. A blocking unpriced-usage policy still
treats the run as unknown spend.

Refs #19846

## What Changed

- `run-cost-accounting.ts`: new `captureFailureBoundPassed`. It applies
to a terminal capture-failed run whose native coordinator (if any)
settled, after `ACCOUNTING_CLOSURE_TIMEOUT_MS`. This is the same bound
used for missing closure evidence.
- `actionableRunAccounting` selects these runs, and
`readCostAccountingWork` schedules a wake at their bound. A past bound
never schedules a wake, as before.
- `accountRunCost`: a capture-failed run is never treated as
pre-provider work. Before the bound it stays pending. After the bound it
settles through the existing lost-receipt path as `receiptUnavailable` +
`closureTimedOut`: known cost becomes a lower-bound estimate, otherwise
the charge is unpriced, and the run gets `accountingClosureTimedOut:
true`.
- Provenance evidence and the `cost.accounting_closure_timed_out`
activity say that the usage capture failed (`details.captureFailed:
true`).
- Tests: three new cases in `closed-native-receipt-accounting.test.ts`.
`cost-data-quality.test.ts` now pins both sides of the bound for a board
retry.

## Verification

- `npx vitest run
src/__tests__/closed-native-receipt-accounting.test.ts`: 54 passed.
- `npx vitest run src/services/run-cost-accounting.test.ts
src/__tests__/idle-accounting-work.test.ts
src/__tests__/cost-data-quality.test.ts`: 40 passed.
- Red before the fix: with the old `run-cost-accounting.ts`, all 3 new
capture-failure cases fail. The run never settles, nothing schedules a
wake, and work stays held. Green after.
- Hard stop: "keeps a settled capture failure under the blocking
unpriced-usage policy" shows that a blocking policy still blocks after
settlement. Real observed spend (`budgetExposure`) counts toward the
hard stop before and after.
- Server `tsc --noEmit` shows no errors in the changed files.

## Risks

- Behavior change: capture-failed runs no longer stay pending forever.
The 2026-10-28 Cursor runner plan recorded "capture failures remain
pending" as an interim rule. This PR keeps them pending through the
closure bound and then settles them as unknown spend, never as free. A
complete receipt that arrives later still reconciles through
late-receipt handling.
- A board retry of a capture-failed run past the bound now settles it.
Before the bound it is still refused.
- Previous-release story: no persisted shape changes. Existing
capture-failed rows past the bound settle on the next reconcile pass.
That can release work that was held, and under a blocking policy it
records unknown spend that blocks.
- No new waits or I/O before `turn.start`. This is background accounting
only.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used with tool use and
code execution in Claude Code.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 12:01:56 -06:00
DottaandPaperclip 4d5265fe7e Preserve fully covered historical workspace sources (#19872)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Workspace migration must preserve local files while retaining
unfinished recovery obligations.
> - A shared Git source needs proof for its companion workspaces before
import can publish.
> - Recovery backfill can add a historical-source marker to an exhausted
prepared-export hold.
> - The hold owner rejected that marker even when every recorded
allocation already belonged to its proven prepared source.
> - This pull request accepts only that fully validated form through the
existing source owners and publication fences.
> - Unrelated work can continue while import saves the local files and
keeps the remote recovery hold intact.

## Linked Issues or Issue Description

Refs #19830. No separate public issue was found. A search for the marker
found no open duplicate PR.

**What happened?**

A companion workspace with an exhausted prepared-export hold blocked the
whole shared Git cohort after canonical recovery backfill added its
historical-source marker. The prepared and historical source readers
both accepted the same allocation.

**Expected behavior**

Local file preservation can proceed when all historical references are
already covered by the proven prepared source. The recovery hold and
remote obligations remain unchanged. Unknown or uncovered sources stay
blocked.

**Steps to reproduce**

1. Create a shared Git repository with a selected leaf and a companion
workspace.
2. Give the companion a settled failed export, accepted result,
exhausted recovery and blocked task.
3. Run canonical historical recovery backfill for its retained
allocation.
4. Import the selected leaf through the normal online migration owner.

**Paperclip version or commit**

Reproduced on `4185e308659abf0887930e20049ca6e7cbfc0e81`.

**Deployment mode**

A source installation with legacy local Git workspaces and object
storage.

## What Changed

- Validate the exact historical marker schema, run and unique lease
references against the already proven prepared source and declared
allocations.
- Reuse the canonical historical-source inspector and compare the same
provider allocation and remote working directory.
- Reject unknown fields, malformed markers and foreign, duplicate or
uncovered references.
- Keep the full marker in the existing hold fingerprint and retain the
existing source, task, lifecycle, finalizer and publication checks.
- Add real PostgreSQL/Git tests through shared-cohort inspection and the
encrypted import/restore owners.

## Verification

- The unchanged owner failed the actual selected-leaf importer
regression with `online_workspace_source_scope_unproven`. The canonical
prepared/historical reader controls passed before that refusal.
- Production head `3e678e10fa` passed 41 focused PostgreSQL/Git cases
across three files in 18.93 seconds after rebasing onto the new
finalizer schema migration. This includes 14 new cases; 80 unrelated
cases were filtered out. The final test-only review update passed both
additional multi-allocation controls plus the original selected-leaf
cold restore (3/3, 11.29 seconds); TypeScript passed again.
- The positive uses the actual historical backfill producer. It
cold-restores an encrypted selected-leaf snapshot with the original Git
tree unavailable and keeps the peer's task, finalizer, result,
allocation and recovery action unchanged.
- The tests retain an unrelated active object-backed run, prove the peer
finalizer lock is held, refuse bad bindings and reject late marker
changes without discarding the uploaded snapshot.
- Server TypeScript, module-boundary checks and `git diff --check`
passed. The canonical PostgreSQL wrapper exited and cleaned up.
- Final-head CI passed: 56 successful checks and four intentional skips,
with no failures or pending jobs
([run](https://github.com/paperclipai/paperclip/actions/runs/42397090541)).
Greptile reviewed `0cbf0a0255` at 5/5 with no open findings; GitHub
reports CLEAN/MERGEABLE.

## Risks

- The allowance applies only to the existing exhausted prepared-export
hold. Other recovery forms and unknown or uncovered references remain
blockers.
- A marked hold adds database reads proportional to its already declared
allocations during maintenance inspection. It adds no provider calls,
payload download or recurring background work.
- Import saves the current local files. It does not claim that unsaved
remote files were recovered, that a provider stopped or that execution
may resume. Existing holds and warnings remain authoritative.
- Local tests use the real encryption and restore owners with a local
object backend. Hosted backfill and normal-delivery qualification remain
separate.

## Model Used

OpenAI Codex, based on GPT-6, with reasoning, repository tools and code
execution. The exact serving snapshot and context-window limit are not
exposed in this session.

## 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
#` 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>
2026-11-07 11:53:00 -06:00
DottaandClaude Opus 5.5 51c352ec2e fix(skills): release a runtime skill-cache lock whose holder provably exited (#19849)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Before a run starts, the server builds a read-only cache of each
company skill under a cross-process file lock
> - The lock records only a pid and a hostname, so a crash leaves a lock
that nothing can release
> - If the holder pid is dead on the same host, the next run fails and
asks for manual cleanup. If the host name differs, the next run waits 60
s and then fails
> - This is on the turn path, so one crash during a skill build breaks
later runs that use that skill
> - This pull request records the shared controller process identity in
the lock and releases the lock at once when `controllerProcessExited`
proves the holder is gone
> - The benefit is that a crash during a skill build no longer blocks
later runs, and the release never relies on lock age or another host

## Linked Issues or Issue Description

**Bug description**
The Phase 0 ownership inventory (finding 4) lists the runtime
skill-cache lock (`runtime-skill-cache.ts`) as a record that nothing
releases automatically. When a publisher crashes while it holds
`<skillId>.lock`, every later preparation of that skill fails.

**Steps to reproduce**
1. Start a skill cache build, then kill the server process while it
holds the lock.
2. Restart and start a run that uses the same skill.
3. Preparation fails with "Runtime skill cache publisher exited; remove
its stale publication lock before retrying". For a holder whose host
name differs, it fails after 60 s with "publisher is busy".

**Expected behavior**
A lock whose holder has provably exited is released, and the build
continues without waiting. A holder that cannot be proven gone is still
waited for.

Refs #19846

## What Changed

- `controller-process-identity.ts`: add
`currentControllerProcessStamp()`. It returns this process's identity
with no I/O, so the turn path gets no new wait.
- `runtime-skill-cache.ts`: the lock owner file now also stores
`controller` (boot id, pid, PID-space hash, start time if already
known). `pid` and `host` stay so older readers still work.
- New `runtimeSkillCacheLockHolderExited`: true only when
`controllerProcessExited` proves the holder gone. Another host or PID
namespace, an unknown space, a legacy lock without `controller`, a live
pid, and this process all return false.
- On proof, waiters race for one exclusive claim named by the stale
holder's unique token (`<lock>.released-<token>`). Only the winner
unlinks the lock, and only while it still has the stale inode. The claim
is kept, so a late waiter does nothing. The waiter then retries the link
right away instead of after the 20 ms poll.
- A live holder is re-proved at most once a second.
`controllerProcessExited` skips the start-time probe (`ps` on macOS)
when no start time was recorded.
- The busy error now names the lock path. The same-host "publisher
exited" hard failure is removed.

## Verification

- `npx vitest run src/__tests__/runtime-skill-cache.test.ts
src/services/controller-process-identity.test.ts`: 28 passed, rebased on
master after #19848.
- New case "takes over a release claim whose claimer exited before
removing the stale lock" covers the second Greptile finding.
- New case "never removes a newer lock for a waiter that read a stale
one late" covers the Greptile P1.
- New case "releases a publication lock at once when its holder provably
exited". On the old code it fails with "Runtime skill cache publisher
exited; remove its stale publication lock before retrying". On the new
code it builds in well under 5 s and leaves no lock.
- New case "never treats age, another host, or a legacy holder as
proof": a different host hash, a legacy `{pid, host}` lock, a live pid,
this process, and malformed content all stay held.
- Server `tsc --noEmit` shows no errors in the changed files.

## Risks

- Previous-release story: a lock written by the previous release has no
`controller` field. It is unprovable. The waiter waits 60 s and fails as
before, and the error now names the path to remove. A new lock still
carries `pid` and `host`, so a previous-release reader behaves as
before.
- Lock release race: while a stale lock exists, only the single claim
winner can remove it, and the inode check cannot match a newer lock
because the stale holder's owner file stays and pins that inode. A claim
whose claimer crashed is itself released by the same proof (depth
bounded at 2).
- Each released lock leaves one small claim file and the dead holder's
owner file next to it.
- No new I/O before `turn.start`. The stamp is synchronous. The proof
runs only when the lock is contended.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used with tool use and
code execution in Claude Code.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 11:51:25 -06:00
DottaandClaude Opus 5.5 738058553f fix(recovery): resume a continuation that never launched its runner in its own sandbox (#19850)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A chat follow-up on a remote native task runs as a continuation: it
starts from its predecessor's saved provider session and, for a warm
session, copies the predecessor's runner process identity onto the new
run
> - When the continuation fails before it asks for a runner of its own,
its due same-run retry sees a recorded process identity and reattaches
to "this run's" runner. That runner never existed, so reattachment
always fails with `native_remote_recovery_lease_mismatch` and the task
is blocked
> - On staging, 34 OpenCode runs on 31 tasks failed this way in one
evening. Each first attempt failed while staging its provider pack,
recorded no runner start, and kept its own sandbox lease active
> - This pull request proves that shape from durable state, retires the
inherited identity, and resumes the run the way its failed attempt ran,
in its own retained sandbox
> - The benefit is that these retries recover automatically, without a
second sandbox and without the unsaved-work hold, while every run that
may hold its own provider work keeps verified reattachment

## Linked Issues or Issue Description

Refs #19839, Refs #19399, Refs #19755, Refs #19779, Refs #19827

**What happened?**
A remote OpenCode chat continuation failed during provider-pack staging.
The run had no runner start event, no process identity event, no
controller checkpoint, and no provider event of its own. Its saved
session (no active turn, no pending request) and its process identity
were inherited from the previous run. Its failure detail recorded
`providerEventsExist: false`, and its own sandbox lease stayed active.
The due same-run retry chose `reattach_remote_runner` because a process
identity was recorded. Reattachment failed with
`native_remote_recovery_lease_mismatch`, and the run ended in
`native_continuation_requires_reconciliation`.

**Steps to reproduce**
1. Continue a warm remote native session so the new run inherits the
predecessor's runner process identity and saved session.
2. Fail the new run with a retryable error before it requests a runner
(for example during provider-pack staging).
3. Let the same-run retry become due. Recovery requests
`reattach_remote_runner`, and reattachment fails.

**Expected behavior**
The retry resumes the run the way its failed attempt ran, in the same
sandbox, and continues the inherited provider session. A run with any
provider work, runner start, or controller checkpoint of its own keeps
verified reattachment.

## What Changed

- New
`server/src/services/native-runtime/native-inherited-runner-retry.ts`:
- `retireInheritedRemoteRunnerIdentity` locks the issue, the
coordinator, then the run (the same order as same-run resumption). It
retires the recorded process identity only when all of these hold: the
coordinator is a result-less, unowned `exact_checkpoint_resume` retry
with `providerEventsExist: false`; the run has no runner start, process
identity, adapter invocation, tool start, runner event, controller
checkpoint, provider event or provider session of its own; the saved
session has no active turn and no pending runtime request; and a settled
predecessor of the same native session recorded the exact same process
identity (PID, group, start time).
- It records `nativeInheritedRunnerRetirement` on the run profile, with
the run's own sandbox location when that lease is still active, and an
`inherited_runner_never_launched` lifecycle event.
- `readInheritedRunnerRetainedSandbox` returns that sandbox only while
it still matches the run's saved remote location.
- `heartbeat/recovery.ts`: a due remote retry calls the retirement
first. It returns `retired`, `not_applicable` or `transient`. A retired
run (including one a concurrent sweep already retired) goes to the
ordinary same-run resume (`claimNativeSessionResumptions`), with every
existing admission gate. A lock timeout or database error is
`transient`: the sweep defers the run and checks again, and never falls
back to reattachment. Any other run keeps verified reattachment.
- `heartbeat/workspace-preparation.ts`: an ordinary same-run resume with
a retired inherited runner acquires its retained sandbox through the
existing exact-lease path (`reattachRemoteLease`), which validates and
resumes that lease and never acquires a replacement. It acquires as an
ordinary resume does only when the exact-lease failure proves the
sandbox is unusable (`stage=environment_acquire|environment_resume`,
`field=lease|lease_status|lease_expired|provider_lease_id`). Any other
failure, such as a scope mismatch on a live lease, is kept and acquires
nothing.
- New `server/src/services/retained-sandbox-absent-error.ts`: a sandbox
provider's resume reply with no provider lease and `expired: true` (the
reply Daytona and other providers send when the sandbox is gone) now
throws `RetainedSandboxAbsentError`. Every other mismatched reply keeps
the existing error. The exact-lease reattachment in
`environment-run-orchestrator.ts` maps it to
`native_remote_recovery_lease_mismatch: stage=environment_resume
field=lease_expired`, so the same-run resume above acquires as an
ordinary resume when the provider has removed the retained sandbox. A
transient resume failure is not treated as absence and never acquires
another sandbox.
- `native-restart-recovery.ts` exports the existing
`hasNativeExecutionStartEvidence` so both paths use one definition of
"execution reached a runner".
- `doc/execution-semantics.md` documents the rule.

## Previous-release state

- No schema change. The new profile field is additive.
- A run saved by an older release without `providerEventsExist`, without
a native session ID, or without a matching predecessor keeps verified
reattachment. That path is unchanged. When it loses its runner, it still
requests the user's Retry once (#19399).
- A run that an older release left with its own runner start, controller
checkpoint or provider event keeps verified reattachment, because its
outcome may be ambiguous.
- A profile without the new marker never changes acquisition.

## Verification

- `npx vitest run server/src/services/heartbeat/recovery.test.ts -t
"never launched its own runner|keeps verified reattachment"` (embedded
PostgreSQL):
- Staging shape (inherited session and process identity, no runner start
of its own, saved remote workspace, active own lease, recorded PID).
Before the fix: fails, because recovery requested reattachment. After
the fix: passes. The PID fields are cleared, the marker names the
predecessor and the retained lease, `executeRun` is called once with a
plain `nativeLeaseOwner` claim, the run still has exactly one (active)
lease, and no process is signalled.
- Negatives, passing before and after: a provider turn of its own, a
runner start of its own, a controller checkpoint of its own, no matching
predecessor, a pending runtime request in the saved session, and a
failure detail without `providerEventsExist`. Each keeps verified
reattachment and gets no marker.
- `npx vitest run
server/src/services/heartbeat/workspace-preparation.test.ts -t "retained
sandbox|retired inherited"`:
- A retired inherited runner acquires its retained sandbox with
`reattachRemoteLease`. Before: fails. After: passes.
- When the retained sandbox is gone, it acquires as an ordinary resume.
Before: fails. After: passes.
- Without the marker, acquisition is unchanged (passes before and
after).
- A `nativeWorkspaceSync` reference also resumes its retained sandbox. A
live lease's `lease_scope` failure is kept and acquires nothing.
- `recovery.test.ts` also covers a second retirement call (returns
`retired`, one event) and a held issue lock (defers; no reattachment, no
dispatch, PID kept).
- `environment-runtime.test.ts`: on exact resume, only a provider reply
with no lease and `expired: true` (with or without a workspace sentinel
mismatch) is `RetainedSandboxAbsentError`; a different lease or a reply
without `expired` keeps the existing error.
`environment-run-orchestrator.test.ts`: confirmed absence becomes
`stage=environment_resume field=lease_expired` and acquires nothing.
- Affected suites pass locally: `recovery.test.ts`,
`workspace-preparation.test.ts`,
`heartbeat-native-remote-failure-retention.test.ts`,
`workspace-recovery-retry-copyback.test.ts`,
`native-runner-restart-recovery.integration.test.ts`. `pnpm --filter
@paperclipai/server typecheck` passes.

## Risks

- **No second executor.** The rule applies only when this run never
requested a runner, so no runner of this run exists. The predecessor
must be settled, and its identity is matched exactly. The resume then
takes the normal same-run claim, which holds the issue lock and
admission gates.
- **No repeated side effects.** The run has no provider event, turn or
tool start of its own. A saved session with a pending runtime request or
an active turn keeps verified reattachment. The resumed provider session
goes through the existing provider recovery, which refuses a pending
side-effecting call with a permanent, named code.
- **Bounded.** The retirement takes effect once per run, because
afterwards the run has no recorded identity. A contended retirement
waits for the next sweep. Further failures use the existing
three-attempt provider budget.
- I did not run a live Daytona sandbox, or a full preparation through a
real sandbox plugin, through this path. The orchestrator's exact-lease
checks are covered by its own unit tests. The evidence is recovery and
preparation tests on a real database with the staging run shape.

## 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>
2026-11-07 11:35:23 -06:00
Devin FoleyandClaude Opus 5.5 de82f93443 fix(storage): retry S3 reads that the provider times out with HTTP 408 (#19039)
Mark HTTP 408 on GET/HEAD as retryable inside the SDK retry middleware so the standard backoff, retry quota and three-attempt limit apply. Writes are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 09:34:46 -08:00
DottaandClaude Opus 5.5 68db7ed9e3 fix(recovery): never accept a dead local pid as proof for an owner on another host (#19868)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - After a restart, recovery decides that an in-progress record's owner
is gone and takes it over
> - Two of those proofs use `kill(pid, 0)` on the local host. A pid that
is dead here says nothing about a process on another host or in another
PID namespace
> - So a controller or runtime-control owner that is still alive
elsewhere can be taken over while it still runs
> - This pull request records the owner's PID space (the existing
`controller-process-identity` host hash) and judges a pid only in that
same space
> - The benefit is that a different host is never proof. Those records
follow their existing lease or TTL instead

## Linked Issues or Issue Description

**Bug description**
The Phase 0 ownership inventory (finding 2) found three proofs that
accept "the pid is dead here" as proof for an owner on another host:
`evaluateNativeControllerTakeover` (`native_run_finalizations` has no
host column), the runtime-control sweep (`workspace-operations.ts`), and
the orphan reaper `terminalizeOrphanedRunningRun`. Only
`controllerProcessExited` checked the host.

**Steps to reproduce**
1. Run two control-plane processes on different hosts, or in different
PID namespaces, against one database.
2. Let host A own a native finalization lease or a runtime-control
operation.
3. On host B, run restart recovery. If A's pid number is not alive on B,
B takes over the live owner.

**Expected behavior**
An owner whose PID space differs from this process's, or cannot be read,
is unprovable. Its record waits for its lease, fence, or TTL.

Refs #19846

## What Changed

- Migration `0441`: add nullable
`native_run_finalizations.controller_host_hash`.
- `controller-process-identity.ts`: export `currentProcessSpaceHash()`
and `sameProcessSpace()`. An unreadable space is stored as `"unknown"`,
which never matches.
- `native-restart-recovery.ts`: `currentNativeControllerIdentity`
carries `hostHash`, which both coordinator claim writes
(`native-restart-recovery.ts`, `native-session-executor.ts`) store.
`evaluateNativeControllerTakeover` returns `controller_host_unprovable`
before any pid probe when the recorded space is not this one.
`native-dead-controller-result-lease.ts` selects the column.
- `workspace-operations.ts`: runtime-control owner stamps record
`hostHash`. `ownerProcessProvenGone` requires the same PID space (EPERM
= alive), otherwise the existing staleness TTL applies.
- Not in this PR: the orphan reaper `terminalizeOrphanedRunningRun`.
`heartbeat_runs` has no host for `process_pid`, and its writers are
spread across legacy and native paths. It follows as a separate PR with
its own column.

## Verification

- `npx vitest run
src/services/native-runtime/native-restart-recovery.test.ts
src/__tests__/workspace-runtime-control-recovery.test.ts`: 55 passed.
New cases cover another host and an unreadable space (dead and recycled
pid) for takeover, plus another host, unreadable space, and legacy stamp
for the sweep. The sweep cases also confirm that the TTL still reclaims
after the heartbeat lapses.
- Red before the fix: the 5 new cases fail on the old code. Green after.
- `native-controller-recovery.integration.test.ts`,
`native-failed-run-owner-release.test.ts`,
`native-startup-cleanup-recovery.test.ts`: 71 passed.
- `pnpm db:generate` produced `0441_lucky_chameleon.sql` (one `ADD
COLUMN`) and pruned to five snapshots. `pnpm --filter @paperclipai/db
check:migrations` passed. Server `tsc --noEmit` shows no errors in the
changed files.

## Risks

- Previous-release story: `native_run_finalizations` rows saved before
this column have `controller_host_hash = null`. They keep the previous
local dead-pid proof for this release. Making them unprovable would
leave every native run in flight at the upgrade in `awaiting_evidence`
with no TTL, because local takeovers have no lease fallback. Rows
written by this release onward are strict. Runtime-control stamps
without `hostHash` are strict now, because that path has a staleness
TTL.
- Owners built from other receipts (workspace finalization owner,
workspace stop proof) pass no host and keep the previous behavior. Track
2's shared `OwnerStamp` should absorb them.
- A new row on a host that really moved (redeploy onto a new machine)
waits for its lease or an operator instead of being taken over by pid.
That is the intended "different host is never proof" rule.
- No new I/O before `turn.start`. The hash is computed once at module
load.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used with tool use and
code execution in Claude Code.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 11:14:48 -06:00
DottaandPaperclip 4185e30865 Recover archived commits from original bare sources (#19855)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Workspace migration must preserve saved files before the web server
releases its local volume.
> - An archived Git checkout can retain a trusted branch-deletion
receipt after its private clone and generated project cache are removed.
> - The original repository can still exist at its recorded file URL,
but the archive owner did not check that source.
> - This pull request permits a narrow, verified original
bare-repository source for the exact recorded commit.
> - The existing encrypted snapshot and publication owners then preserve
the workspace through their normal migration path.

## Linked Issues or Issue Description

No public issue exists. Searches for the archived-source error and
original bare Git source found no duplicate change.

**What happened?**

Migration reported `archived_git_source_unavailable` for an archived
checkout even though its original bare repository still contained the
exact saved commit. The private clone and generated cache were absent.

**Expected behavior**

The canonical owner can use the recorded original local repository when
its full commit identity and source ownership are proven. Uncertain or
changed sources remain blocked.

**Steps to reproduce**

1. Create an isolated checkout from a local bare repository.
2. Close it through the normal cleanup owner and retain the successful
branch-deletion receipt.
3. Remove the private clone and generated cache, while retaining the
recorded original repository and full base revision.
4. Inspect the archived workspace for migration.

**Paperclip version or commit**

Reproduced on `84908d176b2800f4c792c4249c610ec0baebdd28`.

**Deployment mode**

A source installation with existing local Git workspaces and shared
object storage.

## What Changed

- Consider the exact configured file-URL repository only after the
canonical primary source is definitively absent.
- Require a matching source record and successful cleanup receipt, the
exact full base/deleted commit, a contained bare SHA-1 repository,
bounded standard configuration and alias-free metadata.
- Restrict this source path to local Git transport and a minimal
environment. Reconstruct the tree only in private migration scratch.
- Retain existing source fingerprints, journal identity, encrypted
upload/readback and final publication checks. Lock and recheck the
configured source and cleanup record before publication.
- Add real PostgreSQL/Git coverage for encrypted cold restore,
lost-reply retry and refused source or publication changes.

## Verification

- The unchanged owner failed the real four-object original-source
regression with an ineligible inventory.
- The review regressions failed before each repair: four
returned-primary retry/publication cases and two actual production
metadata/reaper cases.
- Final head `8925519310` passed all 40 focused PostgreSQL/Git tests in
56.38 seconds. Four positive formats use canonical/legacy metadata and
ordinary/verified-tip cleanup. Each imports an encrypted snapshot,
removes the original file URI from reach, then authenticates and
cold-restores exact files, executable mode and Git history.
- The same file covers source/receipt/writer guards, changed sources
before publication, authenticated lost-reply reuse and 18
returned-primary directory/alias/unreadable cases. Refused publication
retains returned edits and uploaded ciphertext.
- Server TypeScript, module-boundary checks and `git diff --check`
passed on this revision. Canonical test wrappers exited and cleaned up.
- An earlier focused run also passed 22 existing
archived-Git/restore-point controls. `pnpm verify --explain` completed
without executing extra suites. Required CI supplies final broad
coverage.
- Final head passed all 60 CI/review contexts: 56 successful checks and
four expected skips. Greptile reviewed this exact head at 5/5, with no
open review threads.
- One unchanged archive segment-boundary test timed out on both
framework attempts in the initial server shard. The one job-only retry
passed with unchanged code and timeout. The test and its resolved local
import graph match a green control revision. The original failures
remain retained; the cause is unresolved.

## Risks

- The fallback is intentionally narrow. Original repositories with a
different full tip, unsupported configuration, aliases (including an
aliased instance root), external object storage or more than 10,000
metadata entries remain blocked.
- The first source check and final publication check add bounded local
filesystem and Git work to this maintenance path. They add no recurring
worker or network Git access.
- Original repository files remain intact. Importing a retained
workspace does not make its configured local file URL portable for
future fresh project work. That separate configuration dependency still
requires preservation and qualification before volume removal.
- The local regression uses the real encryption and restore owners with
a local object-storage backend. It does not claim a hosted backfill or a
deployed migration.

## Model Used

OpenAI Codex, based on GPT-6, with reasoning, repository tools and code
execution. The exact serving snapshot and context-window limit are not
exposed in this session.

## 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
#` 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.1107.0-canary.17
2026-11-07 11:05:25 -06:00
DottaandClaude Opus 5.5 a9ac2e2039 feat(native): split local runner startup into stage, launch and session-load spans (#19852)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - When a person messages an agent, the agent must start its turn fast.
Time to first token (TTFT) and agent boot are first-class metrics.
> - The Phase 0 baseline (#19846) found that local runner startup costs
1.6–17 s at p50 and up to 50 s at p95. It is the largest part of TTFT.
> - The run log records that startup as one span
(`runner.session.bootstrap` or `runner.session.resume`). We cannot see
if the time goes to staging, to the `paperclip-runnerd` process launch,
or to the provider session load.
> - This pull request splits local runner startup into three sub-spans
in the run log.
> - The benefit: the follow-up work to cut follow-up-turn TTFT can
measure each step before and after.

## Linked Issues or Issue Description

Refs #19846 (TTFT baseline, finding 1 and gap "No sub-spans exist inside
local runner startup"). Refs #19838.

**What existing behavior does this improve?**
The run-log performance spans for local native runner startup.

**Current behavior**
The local path records one `runner.session.bootstrap` or
`runner.session.resume` span that covers runtime staging, the runner
process launch and the provider handshake together. The remote path
already records `runner.runtime.stage` and `runner.process.launch`; the
local path does not.

**Proposed behavior**
The local path records three sequential child spans of
`runner.session.startup`: `runner.runtime.stage`,
`runner.process.launch` and `provider.session.load`.

**Reason and benefit**
The TTFT work needs to know which step dominates before it changes the
runner lifecycle. Without these spans, a change to warm runner reuse
cannot show which cost it removes.

## What Changed

- `RunnerdCodexTransport` has a new optional `onStartupStep` callback.
`#start` and `#resume` time three milestones and call it synchronously.
A failing callback never changes startup. A nested startup (exact
adoption from `thread/start`) keeps the outer clock.
- `runner.runtime.stage`: launch preparation before the runner process
exists.
- `runner.process.launch`: spawn until runnerd accepts the run
(`run.prepare`, or the recovery attachment on resume). An adopted live
runner records no launch. This keeps the existing meaning in
`summarizeStartupReuse`: a launch span means a fresh runner.
- `provider.session.load`: `session.open` (or the recovery probe) until
the provider identity is confirmed.
- `recordNativeRunnerStartupStep` (`native-run-trace.ts`) records each
step as a `run.performance.span` row with `mode` and `target: "local"`.
It uses `trace.defer`, so the run-log write never delays `turn.start`.
- The native executor passes the callback only for a local target.
Remote launchers keep their own spans, so there are no duplicates.
- `redaction.ts`: allowlist `runner.process.launch` and
`provider.session.load` as native span names.
- `doc/run-log-events.md`: new section "Local runner startup sub-steps".

Data path (AGENTS.md §5.7): run log only, plus Observability spans when
an operator sets an OTLP endpoint. The attributes (`mode`, `target`,
`outcome`) are already in the closed span-attribute allowlist. No
Telemetry change.

## Verification

- `pnpm --filter @paperclipai/paperclip-runner exec vitest run
src/live/runnerd-codex-transport-startup-steps.test.ts`: a real runnerd
and the fake Codex fixture. Bootstrap and a cold resume each report
stage, launch and session load in order, with `ok` outcomes and no
overlap. A throwing sink does not change startup.
- `cd server && npx vitest run
src/services/native-runtime/native-run-trace.test.ts
src/__tests__/redaction.test.ts`: the rows have the correct parent,
offsets, durations and failed level. A stalled run-log sink does not
block the caller. The rows settle before
`task.provider_session.measured`. The names survive redaction.
- The existing `runnerd-codex-transport*` suites pass (412 tests). Under
host load (load average 16–27), the first full run had 7 timing failures
in suites that do not set the callback. Each failing file passed when I
ran it again alone. `runnerd-codex-transport-settlement` passed 3/3 for
the failing case alone.
- `tsc --noEmit` passes for `packages/paperclip-runner` and `server`.

## Risks

- Low risk. Without `onStartupStep`, the transport behaves as before:
the clock is not created and each milestone call is an optional-chain
no-op.
- More run-log rows: three per local runner startup.
- The step boundaries are controller-side observations.
`runner.process.launch` includes the runnerd authentication and the
`run.prepare` acknowledgement, not only the OS spawn.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with
tool use (shell, file edit, test execution).

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 10:57:31 -06:00
DottaandClaude Opus 5.5 e8a418cc4d fix(workspaces): settle an uncertain remote provisioning row once its sandbox is stopped (#19862)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A remote task workspace runs the project's provisioning commands in
a sandbox. Each command has a `remote_workspace_provision` journal row,
because an arbitrary command cannot be replayed after an ambiguous reply
> - A row left `running` with `uncertain: true` blocks every later
reservation of that workspace, and nothing ever settles it
> - A crash or lost reply during provisioning therefore leaves the
task's workspace unusable, with no next action
> - This pull request settles such a row only when the earlier run's
sandbox is provably stopped. Otherwise the 409 names the row, its run,
and the operator's action
> - The benefit is that a crashed provisioning attempt no longer locks
the workspace forever, and the effect is never assumed to have succeeded

## Linked Issues or Issue Description

**Bug description**
The Phase 0 ownership inventory (finding 4) lists
`remote_workspace_provision` rows left `running`/`uncertain`
(`remote-workspaces.ts`) as a record with no automatic release.
`reserveRemoteWorkspace` rejects every other run with
`remote_workspace_provision_unverified`. A command that simply exited
non-zero also stayed `uncertain`.

**Steps to reproduce**
1. Start remote provisioning and lose the runner's reply (or have the
command exit non-zero).
2. End the run and stop its sandbox.
3. Start another run on the task. Reservation fails with
`remote_workspace_provision_unverified`, and the next attempt fails the
same way.

**Expected behavior**
After the earlier sandbox is provably stopped, the row settles and the
workspace can be reserved again. While that cannot be proven, the error
says what to do.

Refs #19846

## What Changed

- `remote-workspaces.ts` `reserveRemoteWorkspace`: for each uncertain
provision row from another run, `provisionOwnerProvenStopped` requires
two things. The row's run is terminal, and every environment lease of
that run carries a provider stop or destroy receipt
(`hasRemoteTerminationReceipt` or `hasRecoveredCleanupStopReceipt`). A
proven row is settled as `failed` (`uncertain: false`, `settledBy:
"owner_sandbox_stopped"`) and logged as
`workspace.remote_provision_settled`. It is never marked succeeded.
- If a later reservation check (for example a writer hold) rejects the
attempt, the proven rows are re-proved and settled in their own
transaction, so they do not block again.
- An unprovable row still blocks. The 409 now carries `operationId` and
`runId` and names the action: stop the earlier sandbox, or set the
task's Workspace to "New isolated workspace". That picker option already
exists in `IssueProperties`.
- `provisionRemoteWorkspace`: a reported non-zero exit (not a timeout)
is recorded as `failed` with its `exitCode` instead of staying
uncertain.

## Verification

- `npx vitest run src/services/remote-workspaces.integration.test.ts
src/services/remote-workspace-save-concurrency.test.ts`: 126 passed.
- New case "settles an uncertain provisioning command once its run's
sandbox is provably stopped". A failed run alone still gets the 409 with
`operationId`/`runId`. After a stop receipt, the row settles, and the
activity is logged once.
- New case "records a provisioning command that reported a failing exit
as finished, not uncertain".
- Red before the fix: with the old `remote-workspaces.ts`, both new
cases fail. Green after.
- Server `tsc --noEmit` shows no errors in `remote-workspaces.ts`.
- Not run: a live browser check of the "New isolated workspace" path for
this error. I confirmed in code that it reserves a different workspace,
so it bypasses this check.

## Risks

- A settled row means "effect unknown". The next writer provisions again
in its own epoch, as every new writer already does. A partially applied,
non-idempotent command could see its partial effect. That is the same
exposure as any failed provisioning, and it happens only after the
earlier sandbox has stopped, so the two commands never run at the same
time.
- Previous-release story: no persisted shape changes. Uncertain rows
saved by the previous release are evaluated by the same proof on the
next reservation.
- No new waits before `turn.start`. The proof runs only when an
uncertain row exists, inside the existing reservation transaction.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used with tool use and
code execution in Claude Code.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 10:54:52 -06:00
DottaandClaude Opus 5.5 fa86162790 refactor(recovery): one process boot id and a cached controller identity (#19851)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Restart recovery decides when a record owned by a dead server
process may be released
> - One server process minted three different boot UUIDs, one per module
(`controller-process-identity.ts:13`, `native-restart-recovery.ts:284`,
`legacy-controller-lease.ts:8`)
> - The controller identity also ran `ps -o lstart` on macOS at every
native claim, preparation check and lease renewal, which is on the path
before `turn.start`
> - The durable ownership plan (Phase 1, step 1) needs one process
identity before it can add one owner stamp and one prover
> - This pull request makes the three modules stamp one boot id and
reads the process start time from the OS once
> - The benefit is one meaning of "owned by this process" across tables,
and no `ps` spawn on the turn path

## Linked Issues or Issue Description

Refs #19831 (plan:
`doc/plans/2026-11-07-durable-ownership-and-reconciliation.md`, Phase
1). Refs #19846 (Phase 0 inventory, finding 1, and the TTFT baseline row
"Controller identity").

**Problem**
One process has three boot ids, so the same fact ("this record is owned
by this process") has three encodings. The uncached controller identity
spawns `ps` on macOS on every claim and renewal.

**Expected behavior**
One boot id per process. The OS start-time read runs once per process.

**Impact**
Phase 1 needs one identity to build `OwnerStamp` and `proveOwnerGone`
on. The turn path loses one subprocess spawn per claim and renewal.

## What Changed

- `controller-process-identity.ts` owns the single process boot id. It
adds `currentProcessStartedAtMs()`, which caches a successful OS
start-time read for the life of the process and retries a failed read.
It also exports `currentProcessSpaceHash()` for the coming prover.
- `native-restart-recovery.ts`: `currentNativeControllerIdentity()` and
`isCurrentNativeControllerProcess()` use the shared boot id and the
cached start time. Its own `randomUUID()` boot id is removed. The
fallback to the server-info start time is kept.
- `legacy-controller-lease.ts`: `legacyControllerBootId` is now the
shared boot id.
- Tests in `native-controller-identity.test.ts`: one boot id across
native claims, legacy leases and creation intents; one OS read across
many claims and renewals; a failed read is retried, not cached.

## Verification

- `npx vitest run
src/services/native-runtime/native-controller-identity.test.ts
src/services/legacy-controller-lease.test.ts
src/__tests__/heartbeat-pending-cleanup-sweep.test.ts
src/__tests__/sandbox-creation-operations.test.ts
src/services/heartbeat/upgrade-readiness-post-result.test.ts
src/services/heartbeat/recovery.test.ts` (in `server/`): 6 files pass.
- `npx tsc --noEmit -p server`: no errors.
- I checked every reader of the three ids. Each one compares a stored
column only with the id of the module that writes that column
(`heartbeat_runs.controller_boot_id` with the legacy id,
`native_run_finalizations.controller_boot_id` with the native id,
metadata `bootId` with the creation id). No reader compares the ids of
two modules, so making them equal changes no decision inside one
process.

## Risks

- Low risk. No schema change and no persisted shape change.
- Previous-release story (`rules/upgrade-safe-run-state.mdc`): rows
saved by an older release carry that release's random per-module ids.
They never equal the new process's id, exactly as a row from any earlier
boot never did. They carry forward with no migration.
- The start-time cache is correct because a process's own start time
never changes. A failed read is not cached, so a transient `ps` failure
does not pin the fallback value.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, in Claude Code with
tool use and code execution.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] 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>
2026-11-07 10:49:12 -06:00
DottaandClaude Opus 5.5 6cfb69b4b0 docs(plans): Phase 0 ownership inventory and TTFT baseline (#19846)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A control-plane restart can land between two durable writes. It
leaves an in-progress record owned by a dead process. About 69
restart-chaos PRs fixed that shape one subsystem at a time.
> - The durable ownership plan (#19831) replaces those fixes with one
owner stamp, one prover, write-ahead intents and one reconciler. Phase 0
must first list every in-progress record kind and measure how fast turns
start today.
> - Without the inventory, Phase 1 cannot choose one stamp or one proof.
Without the baseline, no later phase can show that it kept turns fast
(invariant 8).
> - This pull request adds the Phase 0 inventory, the TTFT, boot and
restart-recovery baseline, and the run-log-only driver that produced the
baseline.
> - The benefit is one reviewed map of 83 record kinds to plan sections
A–G, and measured p50/p95 budgets per adapter.

## Linked Issues or Issue Description

Refs #19831. Refs #19838 (the TTFT anchor that the baseline uses).

**What is the problem?**
Phase 0 of the durable ownership plan has no inventory of in-progress
record kinds and no speed baseline.

**What is the expected outcome?**
Two reviewed documents in `doc/plans/`, plus a repeatable way to measure
the baseline again in later phases.

**Why does it matter?**
Phase 1 cannot start without them, and every later phase's exit criteria
compare against the baseline.

## What Changed

- `doc/plans/2026-11-07-ownership-inventory.md`:
- 83 durable in-progress record kinds (94 reader rows) across native
runtime, environments, workspaces, accounting and the wake queue;
- each kind with its table and column, writer, states, owner, release
rule, proof, obligation, invariants and plan sections A–G;
- the overlaps, the merge decisions and the zero-downtime upgrade record
kinds.
- `doc/plans/2026-11-07-ttft-baseline.md`:
- TTFT, agent boot, pre-turn steps and restart recovery per adapter
(codex, claude, opencode), local, cold and warm, p50/p95;
- restart recovery from the overnight chaos evidence, the slowest steps
ranked, and the gaps.
- `tests/restart-chaos/ttft-baseline.ts`: an on-demand driver on the
chaos supervisor. It reads the run log only, never Telemetry, and always
stops its embedded Postgres.
- `tests/restart-chaos/ttft-baseline-stats.ts` and its unit tests: pure
percentile, boot, pre-turn ranking and recovery statistics.

No runtime behavior changes.

## Verification

- `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts
tests/restart-chaos/ttft-baseline-stats.test.ts`
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`
- The baseline numbers come from the driver run named in the report. To
reproduce them, follow the report's "Method" section.
- The owner spot-checked the inventory's file:line claims at
`f2d8a51027`. One example: `processLossRetryCount` has no writer, and
`heartbeat/recovery.ts:2675` is its only reader.

## Risks

- Low risk. This adds documentation and an opt-in test driver. Nothing
runs in CI except the pure stats unit tests.
- The driver spends a small amount of provider credit when someone runs
it by hand.
- The baseline sample is small (6 per cell). The local host was shared
with other worktrees, so the p95 tails include load. The report states
both.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), via Claude Code with tool use,
code execution and five parallel read-only subagents for the inventory.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [ ] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 10:47:31 -06:00
DottaandPaperclip 54b83b15a3 Reuse authenticated nested workspace verification (#19854)
## Thinking Path

> - Paperclip preserves the work that its agents produce.
> - Portable workspace migration can save nested workspaces before their
parent.
> - The parent must prove that a committed child snapshot covers any
files it omits.
> - The existing owner downloads and restores the same immutable child
archive before and after parent capture.
> - The fresh local-file checks are required, but the repeated immutable
archive check is avoidable within one live source fence.
> - This pull request reuses only an authenticated archive proof with an
unchanged reference and provider identity.
> - It also bounds independent child branches at two workers while
retaining every fresh local and Git check.

## Linked Issues or Issue Description

Refs #19805 and #19688. A duplicate search found no separate fix for
repeated child archive verification.

**What existing behavior does this improve?**

Online Git parent migration after nested workspaces have committed
portable snapshots.

**Subsystem affected**

server/ — workspace migration and nested snapshot coverage.

**Current behavior**

Each parent verification pass downloads, authenticates and restores
every child archive, then captures the child's current local tree. The
second pass repeats the immutable download and restore. Independent
child branches also run in sequence. A large parent can exhaust its
existing budget before parent capture begins.

**Proposed behavior**

One private verifier belongs to the existing live source-owner
transaction. It can reuse a completed archive proof only when the full
snapshot reference, operation, child closure, ETag and byte length still
match. Missing or weak ETags and non-cancellation HEAD errors keep full
verification. Caller cancellation still stops work. A changed identity
forces fresh authenticated readback. The owner still captures current
local bytes and Git state in both passes.

**Reason and benefit**

Remove duplicate immutable work without treating an old local-file check
as current authority. Two independent topmost branches can overlap.
Recursive descendants stay serial within those branches, so the bound
does not grow with depth.

## What Changed

- Add a private per-fence verifier to the existing tree owner. It clears
its proof map at the end of the import and refuses a different owner
transaction.
- Reuse only completed ciphertext, GCM, plaintext, and canonical restore
verification with a matching fresh provider identity.
- Retain all current database, operation, recursive coverage,
local-file, Git and manifest checks on each pass.
- Bound independent branches at two. Abort queued work on failure and
await active peers before scratch cleanup or fence release.
- Use the verifier only in the existing online Git importer. Keep the
offline full-verification path and all capture deadlines unchanged.
- Document resource admission, fallback behavior and the hosted
acceptance limit.

## Verification

- Genuine baseline RED through the real parent importer: child GET
counts 2 + 2, canonical restores 4, maximum overlapping readbacks 1. The
four fresh local child captures already pass.
- Exact positive GREEN: child GET counts 1 + 1, canonical restores 2,
maximum overlapping readbacks 2. All four fresh local child captures
remain.
- The fixture uses real encrypted archives and the canonical S3
provider's local HTTP HEAD parsing with actual payload-derived ETags. It
uses fake fixture credentials only.
- Final focused suite: **13/13 PASS**, 148.36 seconds, at `eaf17d3340`.
This includes recursive max-two completion, failure/cancellation
joining, identity/ref/content changes, same-fence alias reuse and
stale-owner refusal.
- Both affected existing migration suites: **256/256 PASS**, 201.84
seconds, at initial head `eaf17d3340`.
- The HEAD-failure review repair has genuine two-case RED evidence and
three passing failure/cancellation controls. Full layered
source/test-tree typecheck and module boundaries pass at repaired head
`763648adad`. Final exact-head controls: **21/21 PASS**, 86.63 seconds
(all 16 new controls plus five existing parent cases).
- The first extended recursive fixture correctly failed because it
created a Git branch after taking an earlier snapshot. The fixture now
creates all branches before any import; the source-change guard remains
intact.
- Exact-head CI: 56 success, four expected skips, and one neutral
Paperclip Review. Required gates pass; Greptile is fresh 5/5. Its
optional aggregate-handler cleanup recommendation is resolved with the
intentional fail-closed rationale.
- Local verification is macOS arm64 / Node26.4.0. The observed hosted
parent still needs to complete within its unchanged budget after normal
delivery. No hosted timing improvement is claimed here.

## Risks

- Two simultaneous child verifications can use more I/O, memory and
scratch space. Operators must count logical verifications, even in one
process.
- HEAD availability and provider identity are required for proof reuse.
Providers without a usable ETag perform the full check.
- Reuse lasts only for one live transaction. A retry, changed reference,
changed provider identity, process exit or owner change requires fresh
verification.
- Faster verification does not waive changed child bytes or guarantee a
specific hosted completion time. No provider turn, authority marker,
source deletion or larger timeout is introduced.

## Model Used

OpenAI GPT-6 through Codex, with repository and code-execution tools.
The exact serving model revision and context-window size 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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-07 10:45:17 -06:00
DottaandClaude Opus 5.5 e7936fb1ad fix(heartbeat): gate process-loss retries on the shared failure budget (#19856)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - When a server restart loses a run's process, the orphan reaper
decides whether to retry the run
> - `doc/execution-semantics.md` says process loss uses the shared
durable failure retry counter, and `enqueueProcessLossRetry` already
schedules against that budget
> - The reaper still guards on `processLossRetryCount < 1`, but nothing
has written that column since the move to the shared budget, so the
guard does nothing
> - So an exhausted chain still says "retrying once" and skips the
interaction continuation path, and a legacy row with a non-zero counter
is refused a retry it is owed
> - This pull request replaces the dead guard with
`failedRunRetryBudgetSpent`, the budget that the retry actually spends
> - The benefit is one restart budget, read and written in one place,
with honest run messages

## Linked Issues or Issue Description

**Bug description**
The Phase 0 ownership inventory (finding 5) found that
`processLossRetryCount` has no writer and that its only reader is the
reaper guard at `heartbeat/recovery.ts:2675`. The guard always passes
for new rows. For rows written by earlier releases, it refuses a retry
even when the shared failure budget has room.

**Steps to reproduce**
1. Seed a legacy run that a restart stopped before adapter dispatch.
Spend its whole failure retry budget
(`executionRetryAccounting.failureRetries = max`).
2. Run `reapOrphanedRuns()`.
3. The run fails with "Process lost -- server may have restarted;
retrying once", but no retry is scheduled.

**Expected behavior**
An exhausted budget gives no promise of a retry. A legacy counter value
does not block a retry that the shared budget allows.

Refs #19846

## What Changed

- `heartbeat/recovery.ts`: `shouldRetry` now checks
`!failedRunRetryBudgetSpent(run)` instead of `processLossRetryCount <
1`. When the budget is spent, the reaper falls through to
`scheduleInteractionContinuationInfrastructureRetryIfEligible`, which is
what the old guard intended.
- `packages/db/src/schema/heartbeat_runs.ts`: a comment marks
`process_loss_retry_count` as legacy. There is no schema or migration
change.
- Tests: the three cases that used `processLossRetryCount: 1` to mean
"retry already spent" now spend the real failure budget. Two new cases
cover a legacy counter (a retry is still scheduled) and a spent budget
(no "retrying once", `retryEligible: false`, no retry run).

## Verification

- `npx vitest run src/__tests__/heartbeat-process-recovery.test.ts -t
"process-loss|failure budget|bypass unknown process outcomes|immediate
terminal-run cleanup|paused-tree work|restart stopped before adapter
dispatch|reached adapter dispatch"`: 7 passed.
- Red before the fix: with the old `recovery.ts`, both new cases fail.
Green after.
- Server `tsc --noEmit` shows no errors in the changed files.
- The full `heartbeat-process-recovery.test.ts` file (419 cases) stalled
locally at 0% CPU after about 15 minutes. That run was on a worktree
whose package builds were stale at first. I ran the focused subset
instead, and required CI runs the full file.

## Risks

- Low risk. The retry decision now follows the same budget that
`enqueueProcessLossRetry` already enforced. A run with budget left
behaves as before.
- Previous-release story: rows saved with `process_loss_retry_count > 0`
are no longer refused for that reason alone. The shared failure budget,
which earlier releases also wrote, bounds them. The column stays, so
earlier-release readers (`queue.ts` startup resume) keep working.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used with tool use and
code execution in Claude Code.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 10:37:39 -06:00
377bc87df1 feat(connectors): show and select personal AI account defaults (#19845)
Show a crown on each saved personal AI provider default. Add a provider-specific menu action to other eligible accounts. Share saved-state queries, block duplicate writes, and keep the saved crown until a write succeeds.

Keep account details available for accessible grants. Show session lookup errors with Retry. Resolve list permissions once per request and index account lookups. Add UI and API regression coverage and account documentation.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <Paperclip-Paperclip@users.noreply.github.com>
2026-11-07 10:26:08 -06:00
DottaandClaude Opus 5.5 824b25654c fix(ui): keep a Retry for retained-file holds whose source cannot be verified (#19840)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - When a native run finishes but its files cannot be saved, the task
stays blocked behind a retained-file recovery hold.
> - The server records that hold as blocked and retryable, and the task
notice tells the user to "Retry to try again".
> - The task page hides its own Retry for these holds and relies on the
retained-file recovery panel instead.
> - That panel only binds the recovery record when a read-only source
inspection returns `verified`.
> - Inspection can return `blocked` for normal reasons: a sandbox that
predates ownership receipts (state saved by an older release), or a
workspace that moved on after its files were recovered.
> - Then neither surface offers an action, and "Check file recovery
status" always answers "File recovery changed. Refresh the task", also
after a reload.
> - This pull request binds the record by its own identity, uses
inspection only to gate the physical file-recovery request, and keeps
the task Retry visible whenever the panel has no control of its own.

## Linked Issues or Issue Description

Refs #18848 (the change that added the retained-file recovery panel and
hid the generic Retry for these holds).

**What happened?**

A blocked task with an active
`native_workspace_sync_out_retry_exhausted` hold showed "The saved files
need recovery before work can continue", "The recorded source is
unavailable or has changed", and "Saving the agent's files failed after
automatic retries. Retry to try again". It showed no Retry and no
recover button. "Check file recovery status" always answered "File
recovery changed. Refresh the task to inspect the current recovery", and
a reload did not change it. The hold was saved by an earlier release:
`systemRecovery` exhausted with `allocation_ownership_ambiguous` /
`service_required`, and `workspaceRecovery` blocked with `retryable:
true`. The source inspection endpoint returned `status: "blocked"` for
it. A second task whose files were already recovered showed the same
"need recovery" and "has changed" text, because its inspection reported
a reference mismatch after recovery.

**Expected behavior**

A retryable hold always shows a working Retry or recover control. A
notice that says "Retry" has a Retry button. Status checks never loop on
"changed, refresh". Recovered files show as recovered. When the source
cannot be verified, the page names the reason and who must inspect it.

**Steps to reproduce**

1. Use an active retained-export hold whose `workspaceRecovery` is
blocked and retryable, and whose `systemRecovery` is exhausted.
2. Make the source inspection (`GET
/api/issues/:id/recovery-actions?inspectSourceRunId=…&inspectSourceActionId=…`)
return `status: "blocked"`.
3. Open the task as a board operator.
4. Before this change, the page shows no Retry and no recover button.
"Check file recovery status" reports "File recovery changed. Refresh the
task".

**Paperclip version or commit**

Observed on build `8812c19`. The regressions fail on master `0f5ce5050e`
before the production edits.

## What Changed

- `WorkspaceExportRecovery`: bind the recovery record by action, run and
schema identity, not by inspection status. Inspection status now gates
only the physical "Recover saved files" request (and the request
re-checks it before POST).
- `WorkspaceExportRecovery`: report through `onRecoveryControlChange`
whether the panel offers its own control. It reports `true` only while
the source is loading or verified and the files are not yet recovered,
or while it shows the previous-server stop confirmation.
- `IssueDetail`: hide the task Retry for retained-export holds only
while the panel reports that it owns the control. Otherwise the existing
task Retry (`POST /issues/:id/retry`) shows. The server renews a bounded
recovery generation and re-verifies the source.
- Show recovered files as recovered even when inspection no longer
matches after recovery.
- Name an unverifiable source in plain words (with the code for other
reasons). State that Paperclip does not apply files it can't verify.
- Recovery text never asks a person to inspect a sandbox or environment,
and never names an owner or administrator as the one who must act. This
covers the retained-file panel, the run source inspector, the copyback
wait, and the native cleanup quarantine message. The execution blocker
and the recovery action read model describe a spent recovery budget from
its current state, so text saved by older releases ("If Retry keeps
failing, inspect the run and its environment") is not shown.
- Task Retry uses the existing `createUuid()` helper, so it works on
plain-HTTP previews without `crypto.randomUUID`. An accepted Retry also
refreshes the inspected file-recovery state. After a failed accepted
request, the unverified-source reason stays visible; it hides only while
recovery runs.
- When the recovery record was replaced, "Check file recovery status"
reloads the task detail instead of telling the user to refresh.
- Tests: component regressions for an old-release hold, a recovered hold
with a mismatched source, verified ownership, and a replaced record. A
page regression proves Retry is shown and calls `retryTask` for
unverified and stale sources. A server safety test proves that Retry for
the old-release hold shape is accepted, opens one new bounded
generation, queues file recovery again, keeps the hold active, and is
idempotent.

## Verification

- RED (master `0f5ce5050e` production files, new tests): 5 component
tests fail. The failure output shows the exact staging text: "Check file
recovery status", "File recovery changed. Refresh the task to inspect
the current recovery.", "The recorded source is unavailable or has
changed." 2 page tests (unverified and stale) fail with `expected [
'Check file recovery status', … ] to include 'Retry'`.
- GREEN: `cd ui && npx vitest run
src/components/WorkspaceExportRecovery.control.test.tsx
src/components/WorkspaceExportRecovery.test.tsx
src/components/RunWorkspaceRecoverySurface.test.tsx
src/components/ExecutionBlockerNotice.test.tsx
src/pages/IssueDetail.test.tsx` passes 320 tests.
- Server: `cd server && npx vitest run src/services/task-retry.test.ts`
passes 81 tests, including the new old-release safety case. This is
safety evidence, not a red: the server path was already correct, and the
defect was in the UI.
- Full UI suite: 11564 pass. 4 tests in `IssuesList.test.tsx` and
`CompanyEnvironments.test.tsx` failed under full-suite load and pass in
isolation (106/106). They are unrelated to this change.
- `pnpm check:token-gates` is clean. `node scripts/run-tsc.mjs -p
ui/tsconfig.json --noEmit` reports no errors in the touched files. The
remaining errors are local artifacts that were not built (plugin SDK UI
entry, generated Storybook catalog).
- Staging was read only (GET requests). The inspect request did not loop
in a fresh tab: two calls in 30 seconds. Most of the tab's request
volume is periodic company issue-list refetches, which this change does
not touch.

## Risks

- Low risk. This is a UI-only behavior change. Server authorization,
source verification, stop receipts, and the bounded recovery policy are
unchanged and remain authoritative.
- The task Retry is now visible for retained-export holds whose source
does not verify. Retry renews one recovery generation. The worker
re-runs the same source-identity checks, so Retry cannot bypass an
ownership refusal. If the refusal is permanent (for example an ambiguous
sandbox allocation), the hold returns to exhausted and the page names
the reason. An automatic fallback for such sources is follow-up work.
- Wording: a parametrized UI test renders every recovery state and
asserts that no administrator, owner, or "inspect the environment" copy
appears. A server test proves that a stored old-release message is
replaced by state-derived text. Both failed before the change.
- Previous-release story: the old-release hold shape (board-owned,
`systemRecovery` revision 5 exhausted with `service_required`,
`workspaceRecovery` blocked and retryable) is covered by component,
page, and server tests. No persisted state shape changes.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- Claude Opus 5.5 (Anthropic), model ID `claude-opus-5-5`, used through
Claude Code with tool use (repository search, file edits, shell, test
execution, and a read-only browser).

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 10:24:03 -06:00
DottaandClaude Opus 5.5 630411af53 fix(recovery): require a 1 s start-time gap and treat EPERM as alive in owner proofs (#19848)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - After a restart, recovery code decides whether the process that owns
an in-progress run is gone
> - Two proofs compare process start times to the millisecond, but `ps
-o lstart` on macOS and BSD reports whole seconds
> - So a live owner can look like a reused PID, and recovery can release
it while it still runs
> - Signal-0 handling also disagrees: one check treats EPERM as dead,
while all others treat it as alive
> - This pull request puts one start-time rule and one EPERM rule in
`controller-process-identity.ts` and uses them at every site the Phase 0
ownership inventory named
> - The benefit is that no proof releases a live owner because of clock
precision or a permission error

## Linked Issues or Issue Description

**Bug description**
The Phase 0 ownership inventory (finding 1) found inconsistent "owner
gone" proofs. `native-failed-run-owner-release.ts`
(`recordedProcessMayBeLive`, `runProcessMayBeLive`) and
`native-session-executor.ts` (`verifiedRecoveryProcessIsAlive`) treat
any start-time difference of 1 ms or more as a reused PID.
`native-restart-recovery.ts` and `controllerProcessExited` require a gap
of at least 1 s. `local-service-supervisor.ts` (`isPidAlive`,
`isProcessGroupAlive`) treats EPERM as dead.

**Steps to reproduce**
1. On macOS, record a live process start time with millisecond
precision.
2. Run the abandoned-owner release pass, or the recovered-runner
liveness check, against that live PID.
3. The pass releases the owner, or the check reports the runner as gone.

**Expected behavior**
A sub-second start-time gap is ambiguous and keeps the owner. EPERM
means the process exists.

Refs #19846

## What Changed

- `controller-process-identity.ts`: export `processExists` (EPERM =
alive) and `processStartsDefinitelyDiffer` (gap >= 1 s; a missing time
proves nothing). `controllerProcessExited` uses the new helper.
- `native-failed-run-owner-release.ts`: the controller and run-process
checks use the shared helpers instead of exact millisecond equality.
- `native-session-executor.ts`: `verifiedRecoveryProcessIsAlive` accepts
a start time within 1 s as the same runner. Its signal path uses the
same check.
- `native-restart-recovery.ts`: `definitivelyDifferentProcessStart`
delegates to the shared helper. No behavior change.
- `local-service-supervisor.ts`: `isPidAlive` and `isProcessGroupAlive`
treat EPERM as alive.

## Verification

- New `controller-process-identity.test.ts`: the 1 s boundary, EPERM and
ESRCH for every signal-0 check, and a live spawned runner whose recorded
start is 500 ms off.
- `native-failed-run-owner-release.test.ts`: two new cases (controller
and run process) with a live child and a recorded start 500 ms off. The
owner stays `observed`.
- Red before the fix: with the old call sites, all 4 new cases fail
(`releaseAbandonedNativeOwners` returns 1 and releases the live owner;
EPERM returns false; the live runner reads as gone). Green after the
fix.
- `npx vitest run src/services/controller-process-identity.test.ts
src/services/native-runtime/native-failed-run-owner-release.test.ts
src/services/native-runtime/native-restart-recovery.test.ts
src/__tests__/local-service-supervisor.test.ts`: 59 passed.

## Risks

- Low risk. The change only makes proofs stricter: a sub-second gap or
EPERM now keeps an owner that the old code released. Such a row waits
for the existing lease, TTL, or real process exit.
- A recycled PID whose new process started within 1 s of the recorded
one now stays held until it exits. That is the same rule the takeover
path already used.
- Previous-release story: no persisted shape changes. Rows saved by the
previous release are evaluated with the stricter rule.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used with tool use and
code execution in Claude Code.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
canary/v2026.1107.0-canary.16
2026-11-07 10:06:35 -06:00
DottaandClaude Opus 5.5 e2d490c61b feat(observability): anchor native time to first token at the accepted wake (#19838)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - When someone messages an agent, the reply must start fast. The
durable ownership plan (#19831) makes time to first token (TTFT) a hard
invariant.
> - Phase 0 of that plan records a TTFT baseline per adapter and
environment, measured from the moment the message or wake is accepted.
> - Today `task.time_to_first_token` starts at the wake comment, the
answered question, or the run row's creation. A wake request that was
accepted earlier (deferred, coalesced or retried) is not part of the
measurement, and the span does not say which start it used.
> - This pull request moves the start back to the earliest accepted
input and names it on the span.
> - The benefit is that the baseline and later budgets measure what the
user waits for, with no new work before `turn.start`.

## Linked Issues or Issue Description

Refs #19831

**What is the problem?**
`task.time_to_first_token` does not include the time between an accepted
wake request and the creation of its run row. It also does not record
which input it started from.

**What is the expected outcome?**
The span starts at the earliest accepted input and carries an `anchor`
attribute: `comment`, `question_response`, `wake_request` or
`run_created`.

**Why does it matter?**
Phase 0 of the durable ownership plan sets TTFT budgets from this
baseline. A start that skips queue time hides the latency that deferred
wakes add.

## What Changed

- `resolveNativeTimeToFirstTokenStart` (pure) in `native-run-trace.ts`
picks the start and the anchor. `anchor` joins the closed span-attribute
allowlist.
- `readNativeRunWakeRequestedAtMs` reads
`agent_wakeup_requests.requested_at` for the run. It never throws.
- `native-session-executor.ts` calls it only after the first deliverable
text, so it adds no wait before `turn.start`. The span now ends at the
text commit time, not after the trace write.
- Updated `doc/run-log-events.md` and `doc/observability.md`.

## Verification

- `cd server && npx vitest run
src/services/native-runtime/native-run-trace.test.ts
src/services/native-runtime/native-run-wake-anchor.test.ts` (26 pass).
- `cd server && npx vitest run
src/services/native-runtime/native-session-executor.test.ts -t first`
(41 pass). The existing TTFT test now asserts the anchor for each
ingress.

## Risks

- Low risk. This changes only the run-log and OTLP span. It is not
Telemetry (AGENTS.md §5.7), and it does not change runtime behavior.
- One indexed primary-key read per native dispatch, after the first
text. A read failure drops the anchor and keeps the old start.
- TTFT values can grow for deferred wakes. That is the intended
correction. `startOffsetMs` can be negative when the wake request comes
before the trace start.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), via Claude Code with tool use and
code execution.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [ ] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 09:57:30 -06:00
DottaandClaude Opus 5.5 84908d176b fix(recovery): name the failed check in native_remote_recovery_lease_mismatch (#19839)
## 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>
2026-11-07 09:27:14 -06:00
Dotta 86bc3db693 feat(runner): search and load unassigned company skills (#19676)
## 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
2026-11-07 09:23:14 -06:00
DottaandPaperclip b8e02fc2b4 fix(native): preserve bounded larger web inventories (#19832)
## 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>
canary/v2026.1107.0-canary.15
2026-11-07 09:11:00 -06:00
DottaandPaperclip 061c84d280 fix: preserve local copies behind settled remote recovery holds (#19830)
## 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>
2026-11-07 08:57:39 -06:00
DottaandClaude Opus 5.5 a94881d1a7 fix(heartbeat): save a comment refused by a temporary hold and route post-quarantine input to its own gate (#19829)
## 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>
2026-11-07 08:54:14 -06:00
DottaandClaude Opus 5.5 0f5ce5050e docs(plans): plan durable ownership and reconciliation for restart safety (#19831)
## 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>
2026-11-07 08:36:05 -06:00
DottaandPaperclip 2e52dfa864 fix(slack): retain DM thread tasks after completion (#19486)
## Thinking Path

> - Paperclip manages AI agents and their work.
> - Slack conversations store that work in tasks and comments.
> - A native thread must keep its task when work finishes.
> - The server treated all DMs as linear conversations.
> - A reply in a completed DM thread therefore started a new task
without its earlier comments.
> - This pull request uses the native thread identity for task
continuity.
> - People can continue the same work with its saved context.

## Linked Issues or Issue Description

**What happened?**

A reply in a Slack DM thread created a new task after the preceding task
was Done or Cancelled. It also created a new task when the saved
conversation was completed. The new task lacked earlier comments.

**Expected behavior**

A native Slack thread keeps one task in channels and DMs. Later replies
retain earlier user and agent comments. Top-level DMs keep their
existing task generation rules.

**Steps to reproduce**

1. Start work in a Slack DM thread.
2. Mark its task Done.
3. Reply in the same thread.
4. Before this fix, the reply creates another task.

**Paperclip version or commit**

Reproduced from master at eb5c24b61c.

**Deployment mode**

Built from source. Tests use local PostgreSQL and provider fixtures.

Related PR: #16151 covers the DM mention exemption. This fix preserves
that behavior. GitHub searches found no duplicate fix for completion
routing.

## What Changed

- Detect Slack native thread roots in the Slack module.
- Classify native DM threads as persistent threads for task routing.
- Select the resource type from the actual DM destination. Keep DM
access checks.
- Preserve DM task controls. Select the latest DM task even after
completion. Require a delivered control receipt before an explicit new
task generation.
- Add completion, duplicate, saved-history, separate-root, Idle, and
explicit-control regressions.
- Document the task boundary. Correct only the inaccurate DM-thread
statement in the existing feature recipe.

## Verification

- Before the production edit, the new PostgreSQL regression failed in
all three threaded-DM completion cases: two conversations existed
instead of one. The channel-thread and top-level-DM controls passed.
- The initial eight routing cases passed. CI then found five existing
slash-control failures. All five reproduced locally. Greptile reported
the same regression at 4/5.
- The first correction passed all 16 focused PostgreSQL cases. Secondary
review then found that slash commands skipped Done, Cancelled, and
completed DM tasks. Four new cases failed before the lookup change.
Commit 23ff4f04f passed all 20 focused cases, including status, explicit
new/close, delivered receipts, and a new task generation. The original
same-thread continuity cases still pass.
- Command: `pnpm exec vitest run
server/src/__tests__/chat-channels.integration.test.ts -t 'preserves
Slack thread history across completion|keeps separate Slack DM thread
roots|returns two answered Slack turns to Idle|binds Slack DM slash
controls|treats D-prefixed Slack status|serializes duplicate Slack
admission with an owned provider-reply settlement|suppresses a queued
provider reply when destination reach|continues to accept authenticated
Slack slash close/new'`.
- Map validation and `git diff --check` passed. Map validation proves
structure only.
- All 107 neighboring Slack adapter, SDK runtime, and receipt tests
passed across three files.
- The initial complete server typecheck passed with
`PAPERCLIP_TYPECHECK_LOW_MEMORY=1 pnpm --filter @paperclipai/server
typecheck`, including all three test-tree shards.
- Full local recursive typecheck and build stopped because this
workspace has no `cargo`. A separate default server typecheck was killed
under concurrent memory load; its lower-memory rerun passed. The broad
local test run was interrupted to free memory, so no full-local-suite
pass is claimed. The complete server typecheck passed again for
correction 367b6f9b3, including all three test-tree shards. The [full CI
pipeline](https://github.com/paperclipai/paperclip/actions/runs/42250458200)
passed for exact head 367b6f9b3f. This
includes typecheck, build, all chat and other test lanes, browser E2E,
Runner, and aggregate verify. The first CI run passed broad typecheck,
build, E2E, Runner, and other test groups, but its chat failures remain
recorded above.
- The initial regression launch could not load the unbuilt plugin SDK.
It ran after building the required workspace dependencies. A test-only
resource assertion was corrected to read persisted DM resources, which
the settings list omits. The completed-conversation case preserves an
existing review status.
- The initial correction test used a follow-up timestamp older than the
delivered control. The chronology gate correctly rejected it. A current
provider timestamp now verifies the new generation without weakening
that gate.
- Greptile's map finding was addressed by removing recipe expansions and
all agent-access map changes. Both review threads have replies and are
resolved. Greptile reviewed exact head
367b6f9b3f at [5/5 with zero new
findings](https://github.com/paperclipai/paperclip/pull/19486#issuecomment-6689117840).
The secondary Paperclip Review rated that head 4/5 and found the
completed-task slash lookup error. Commit 23ff4f04f fixes this finding.
Greptile and Paperclip Review both rate head
23ff4f04f6 at 5/5. All three review
threads are resolved. The [full CI
pipeline](https://github.com/paperclipai/paperclip/actions/runs/42255926149)
now passes on this exact head. This includes typecheck, build, all test
lanes, browser E2E, Runner checks, Canary Dry Run, and aggregate verify.
All checks are terminal: 54 success, 1 neutral, and 3 skipped. Snyk also
passes. The PR has no merge conflict.
- The complete server typecheck passed again on 23ff4f04f, including all
three test-tree shards.
- The new test first asserted the conversation-list state. That API
displays terminal tasks as completed before the control is sent. The
test now reads stored binding state to check that completion waits for
the delivered receipt.
- No live Slack send or deployment was performed.

## Risks

- Native DM threads now reopen the existing task after Done or
Cancelled. Explicit new/close controls still release the binding after
their response is delivered. Top-level DMs still start new generations
after completion.
- Existing split tasks stay separate. Future replies use the latest
saved binding. This change does not merge old tasks or copy surrounding
DM history into a newly created thread.
- No schema, credential, provider scope, or permission changes are
required.

## Model Used

OpenAI Codex, based on GPT-6, with reasoning, tool use, repository
editing, and code execution. The runtime does not expose a more precise
serving model ID or context-window size.

## 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>
2026-11-07 08:35:29 -06:00
DottaandPaperclip c740cb9ded fix(ui): dismiss skipped confirmations when sending a message (#19496)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Tasks and Agent Chat show saved requests above the message composer.
> - A person can send a separate message while a confirmation is open.
> - The old code closed only question forms, after the message save
finished.
> - A confirmation could stay open or open again after a refresh.
> - This change closes the form when sending starts and keeps the
request in the feed.
> - People can open the request later and make the original decision.

## Linked Issues or Issue Description

**What happened?**

A confirmation stayed open when a person sent a separate message.

**Expected behavior**

The form closes immediately. The request stays in the feed. A person can
open it later. Sending a message does not record an approval.

**Steps to reproduce**

1. Open a task with a pending confirmation.
2. Send a separate message below the form.
3. Observe the form while the message save is pending.
4. Reload the task and open the request from the feed.

**Paperclip version or commit**

Reproduced at a434edf991.

**Deployment mode**

Source checkout and isolated Storybook fixtures.

## What Changed

- Close the composer request before the message save finishes.
- Keep earlier saved requests in history after a newer human message.
- Save browser dismissal IDs in one batch after a successful send.
- Restore the previous form after a failed send, including a reopened
request after an optimistic comment changes automatic selection.
- Track deliberate selection changes separately from automatic
filtering. Keep a newer request open when an earlier send fails.
- Allow a new request to open while an earlier message is being saved.
- Add tests for confirmation and checkbox forms in task and Agent Chat
modes.
- Add Send Instead Of Confirming and Reopen Skipped Confirmation
stories.
- Update the existing answer-later story to check the current answer
receipt.
- Update the behavior and Storybook documentation.

## Verification

- TDD: Six new cases failed before the main fix. Four more cases
reproduced the review finding before the recovery fix. They cover failed
sends with an optimistic comment and its rollback in both task and Agent
Chat modes.
- Component checks: 518 passed, one existing test skipped. Run `pnpm
--filter @paperclipai/ui exec vitest run
src/components/TaskChatThread.test.tsx
src/components/task-chat/TaskChatComposer.test.tsx`.
- UI typecheck passed after preparing the local plugin SDK dependency.
- Token gates passed.
- Catalog structure checks passed. The three catalog test files passed
all 30 tests.
- Six Playwright cases passed in an owned Storybook server. They cover
the initial form, sending, manual reopening, light theme, mobile, and
answering later. Play assertions passed. Accessibility reports had zero
violations.
- The stories use the production thread with simulated saves and
decisions. Existing optional fixture endpoints return 404. These do not
prevent the selected journeys.
- No visual baseline comparison was possible. The baseline manifest has
no published snapshots.
- `pnpm verify --explain` requested broader CI coverage. [Required
CI](https://github.com/paperclipai/paperclip/actions/runs/42253585800)
passed on fda44f2648.
- Greptile and Paperclip Review both report 5/5 with no findings on the
final head.
- The remote commit tree matches the tested local tree:
7e22201513.

## Risks

- Older pending requests now stay in history after a newer human
message. People must select the feed entry to open them.
- Dismissal changes presentation. Server approval checks and request
decisions keep their existing behavior.
- Browser fixtures do not prove live provider continuation or staging
behavior.

## Model Used

OpenAI GPT-6 Codex. The exact backend model identifier and context
window were not exposed to this run. Used reasoning, repository
inspection, edits, shell tools, component tests, and browser tools.

## 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>
2026-11-07 08:34:14 -06:00
DottaandClaude Opus 5.5 a4050187ec docs(rules): make upgrade safety part of run and recovery design (#19827)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs save session, checkpoint, lease, and launch state, and
recovery resumes that state later
> - Deploys and restarts often land between the save and the resume, so
one release resumes state that another release saved
> - On 2026-11-06/07, staging deployed four times. Most of 182 failed
runs were saved by one release and resumed by another (#19462, #19659,
#19755)
> - These refusals showed as the generic retryable
`native_session_interrupted`. Retries repeated them, and recovery
replaced a thread (now prevented by #19733). Remote-only code also
shipped without a test that applied Daytona confinement (#19743)
> - This pull request makes upgrade safety a required part of how we
design, fix, and review run and recovery code
> - The benefit is that authors, agents, and Greptile check the release
cutover before a change ships, not after a deploy breaks saved runs

## Linked Issues or Issue Description

Refs #19462, Refs #19516, Refs #19622, Refs #19659, Refs #19733, Refs
#19743, Refs #19755

**Issue type**
Missing contributor guidance for run and recovery code.

**Where is the issue?**
`.agents/skills/fix-failed-paperclip-run/`, `rules/`, `AGENTS.md`, and
`doc/execution-semantics.md`.

**What's wrong?**
No rule or skill step asks how state saved by the previous release is
handled. Fixes added one error code at a time. Saved-identity refusals
used a generic retryable code.

**Suggested fix**
Add a scoped repository rule and additive skill and contract text that
require a previous-release story, permanent named refusal codes, and
cutover tests.

## What Changed

- Add `rules/upgrade-safe-run-state.mdc`. It covers native runtime,
recovery, session preparation, runner, ACPX engine, and sandbox image
paths, and the `packages/db` schema for native run, checkpoint, session,
and run identity state. It requires authors to state how
previous-release state is handled. It requires a permanent, named code
for unknown or changed saved identity, a class-level fix, an
old-saved/new-resumed regression, a deploy-lands-mid-run case where
relevant, and a remote-confinement test for sandbox paths.
- Register the rule in `.greptile/files.json` with the same scope as its
`globs`.
- Link the rule from the "Scoped repository rules" section of
`AGENTS.md`.
- `fix-failed-paperclip-run` skill: step 1 asks if a deploy or restart
separated save from resume, by comparing served revisions. Step 2 asks
this early. Step 4 adds a "Persisted run identity across releases"
safety row and an upgrade-safety paragraph.
- `references/failure-patterns.md`: add "Saved run state meets a
different release".
- `doc/execution-semantics.md`: add §9.4 "Release compatibility of saved
run state".

## Verification

- Docs, skills, and rules only. No product code changes.
- Confirm that each rule glob matches real paths, for example
`server/src/services/native-runtime/`, `server/src/services/recovery/`,
`server/src/services/heartbeat/session-preparation.ts`, and
`packages/adapter-utils/src/acpx-engine/`.
- Confirm that `.greptile/files.json` parses and that its new `scope`
array equals the rule's `globs`.
- `rules/README.md` names no lint check for rules. I did not edit maps,
so `node scripts/check-maps.mjs` does not apply.

## Risks

- Low risk. The change adds guidance only.
- The rule scope includes all of `packages/paperclip-runner/**`.
Greptile can apply the rule to runner docs or UI files. The rule's
exception section says that changes with no persisted shape or identity
need no cutover test.

## 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
- [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>
2026-11-07 08:28:42 -06:00
DottaandPaperclip f2d8a51027 fix(ui): show skill descriptions and preserve composer line height (#19502)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - People write requests in the new-task and task-page composers.
> - Both composers use the shared Markdown editor for skill selection.
> - Skill names alone do not explain which skill to use.
> - Inline skill pills can increase the height of a text line.
> - This pull request shows descriptions in a full-width menu and gives
inline skills a color and icon.
> - People can choose a skill and read the request with normal line
spacing.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

Skill selection and inline skill display in the new-task and task-page
composers.

**Subsystem affected**

ui/ — shared Markdown editor and task composer stories.

**Current behavior**

The slash menu shows skill names in a narrow popup. Inline skills use
pill styles that can make a text line too tall.

**Proposed behavior**

Match the slash menu to the active composer width. Put each skill name
and description on one row. Truncate the description with an ellipsis.
Show each inline skill in purple with the Boxes icon and normal text
line height.

**Reason and benefit**

People can compare skills before selection. Inline skills remain easy to
find without changing paragraph spacing.

**Breaking changes**

None. Skill links retain the same Markdown labels and references.
Mention selection and task submission retain their existing behavior.

**Additional context**

Related: #19272 changes the submission key in these composers. The
browser cases here check skill links after task creation and reply
submission. Searches found no duplicate change for this display issue.
This is a small improvement to the shipped skills UI in ROADMAP.md.

## What Changed

- Align the slash menu with the active composer. Observe composer size
changes and keep the menu inside the viewport.
- Show each skill name and description on one row. Keep skills without
descriptions available.
- Use theme tokens for purple skill text and the Boxes icon. Preserve
the surrounding text line height. Include both prefixed and standard
mask declarations, consistent with the other icon rules.
- Add a composer anchor to the task-page surface. Keep the shared editor
responsible for menu placement and skill display.
- Increase the contrast of menu details and the task composer model
label in the covered stories.
- Add task-page stories and extend new-task stories. Cover desktop,
mobile, light and dark themes, keyboard insertion, and Markdown
submission.
- Add 18 browser cases for menu size and alignment, description
truncation, inline line height, and submitted skill references.
- Use the production mobile context in task-page stories. Check mobile
text size, long skill names without descriptions, and the saved new-task
skill URL.

## Verification

- Before the compatibility correction,
`a6371219ad1ce0ba908b63b55b7999e4e683cace`: all 130 focused editor,
mention, destructive-theme, and avatar tests pass across six files. No
existing test was changed.
- Current head `c7091dd5d34897f82b752bf93f94b1026cc7a82f` adds the
prefixed mask declaration requested by review. All 12 affected
theme/avatar tests pass. A fresh Storybook production build and all 18
composer browser cases pass on the identical source tree, with two
workers, traces enabled, and no retries. Token gates and whitespace
checks pass.
- Browser cases check both composers, desktop/mobile text size,
light/dark themes, popup width and alignment after resizing, single-row
ellipsis, names without descriptions, inline paragraph height, and
submitted skill links.
- The preceding head has a fully successful CI run (`42260195469`,
attempt 2). Current-head Greptile is 5/5 and its check completed
successfully. All four review threads are resolved. Current-head CI
[42270466448](https://github.com/paperclipai/paperclip/actions/runs/42270466448)
passed, including typecheck, build, all general and serialized test
shards, all Runner checks, and all eight end-to-end shards. The
aggregate verify gate passed.
- Screenshots for both composers were reviewed and accepted before this
PR was opened. Stories use production components with fixture APIs and
submissions. They do not prove backend persistence or agent execution.
Mobile uses a resized Chromium viewport with production mobile props.
The visual baseline is unpublished, so captures support inspection, not
pixel comparisons.
- CI run `42254435625` on `9193b47d` failed two existing
destructive-theme checks because the new skill-only theme blocks came
before the main blocks. The failure reproduced locally. Moving the
unchanged declarations into the main blocks made both checks pass.
- CI run `42258164662` on `7fab621f` caught one more existing CSS-reader
contract: the avatar test expects `--background` first in `.dark`. That
failure also reproduced locally. The final source puts the four skill
declarations at the ends of the existing blocks. A mechanical comparison
proves every pre-existing stylesheet line and all four skill declaration
values are identical.
- The first run also failed an unchanged Runner credential test during
its first lease acquisition. All 42 tests in that file pass in a local
diagnostic. The original CI cause remains unknown; no Runner fix is
claimed. The second run's Runner package job was cancelled by the
corrective push, so it supplies no passing verdict. The final-head
Runner package check passed, which shows recovery without explaining the
original lease error.
- One `7fab621f` mobile play function failed during keyboard insertion
(17 browser cases passed, one failed). Its saved DOM contains the skill
and an open link dialog without the trailing sentence. The cause remains
unknown. Three focused traced executions, a bounded frame-delay
diagnostic, and a later full 18-case run passed without source changes.
No reliability fix is claimed. The original failure remains recorded
separately from the final-head 18-case pass.
- Final-head CI run `42260195469`, attempt 1, failed one unchanged
runtime startup test in `idle-completed-history.test.ts`. Its HTTP
readiness check received connection refusals for 10 seconds. The same
test passed in 2.35 seconds in the preceding CI run; the only
intervening source change moved four CSS declarations. A focused local
run passed the case (one passed, 74 filtered out). A full-file local
diagnostic was interrupted after about four minutes without a verdict;
it is not counted as a pass. The startup failure cause remains unknown.
No runtime source or test was changed. Failed CI jobs were rerun once on
the same head; attempt 2 passed. This shows recovery, without explaining
the first startup failure.
- The broad local typecheck could not complete because Cargo is absent.
The affected-test selector requests broad CI for the browser test.
Required CI supplies broad typecheck, test, and build coverage. Linux
local checks used Node 25.9.0, pnpm 9.15.4, and Playwright Chromium 151.
- Earlier evidence retains a Storybook build interrupted by SIGTERM, a
dev-server play error during HMR, and a browser attempt that could not
launch before Chromium installation. Later successful runs do not
explain the earlier interruptions.

## Risks

The shared editor also serves other mention types. Their existing
placement path stays in place. Slash menu placement must stay aligned
during viewport and composer size changes. The browser cases cover both.
The mask correction was checked in Chromium; older WebKit was not
executed. Mobile cases use a resized Chromium viewport. They do not test
a physical phone or its virtual keyboard.

## Model Used

OpenAI GPT-6 through Codex, with reasoning, tool use, and code
execution. The runtime does not expose a more specific model ID or
context-window size.

## 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>
2026-11-07 08:26:19 -06:00
DottaandClaude Opus 5.5 2e7c66aac9 fix(ci): stop the PR merge review notifier from flooding the Actions queue (#19630)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Its GitHub Actions CI shares the `ubuntu-latest` runner pool across
all workflows
> - #19480 added the `PR merge review notification` workflow. It
triggers on `workflow_run`, on every completed `check_run`, and on every
commit `status`
> - One PR push completes about 60 checks, so it queues about 60
notifier jobs. Each job takes a runner
> - The Actions queue now holds about 340 queued runs. About 260 of them
are this notifier. Other `ubuntu-latest` jobs wait more than one hour
(for example, the `authorize` job of Runner Full-Stack E2E image builds)
> - This pull request skips events that cannot complete a green PR state
before a runner is allocated, and it collapses the remaining events to
the newest one per PR head
> - The benefit is that this unjams the Actions queue and keeps the
advisory review notification

## Linked Issues or Issue Description

Refs #19480

**What happened?**
The `PR merge review notification` workflow from #19480 schedules one
runner job for each `workflow_run`, `check_run`, and `status` event. One
PR push makes about 60+ jobs. The repository Actions queue has about 340
queued runs (about 260 from this notifier at the time of writing). Other
`ubuntu-latest` jobs wait more than one hour.

**What did you expect?**
The notifier requests one advisory merge review when PR verification is
complete. It must not use a runner for each intermediate check.

**Steps to reproduce**
1. Push to any PR.
2. Run `gh run list --workflow pr-merge-review.yml -L 200 --json
event,status`.
3. See tens of queued `check_run`, `status`, and `workflow_run` runs for
the same push.

## What Changed

- `.github/workflows/pr-merge-review.yml`: the job `if:` now schedules
the job only for events that can complete a green PR state:
- `workflow_run` from `pull_request` or `pull_request_target` with
conclusion `success`, `skipped`, or `neutral`.
- `check_run` from a non-Actions app (app id is not `15368`) with
conclusion `success`, `skipped`, or `neutral`. GitHub Actions check runs
are already covered by the `workflow_run` list.
  - `status` with state `success`.
- GitHub cannot filter `check_run` or `status` at the trigger level. A
skipped job makes a run record but does not take a runner.
- The job has a concurrency group `pr-merge-review-<PR head SHA>` with
`cancel-in-progress: true`. The notifier reads the current PR state, so
the newest event subsumes older ones. The group is on the job, not the
workflow, so a skipped event cannot cancel a scheduled notifier.
- The environment, permissions, trusted default-branch checkout, and
secret handling do not change.
- `.github/scripts/tests/notify-pr-merge-review.test.mjs`: a new test
pins the trigger kinds, the job condition, and the job-level concurrency
group.
- `doc/PR-MERGE-REVIEW-WEBHOOK.md`: describes the event filter and the
concurrency group.

**Expected runner jobs per PR push** (typical PR: `PR`, `Storybook
Visual`, `commitperclip PR Review` workflows; Greptile, Paperclip
Review, Socket (2), Superagent (2) external checks; one Snyk status):

| | Before | After |
| --- | --- | --- |
| Eligible events that take a runner | about 60–70 (every check run + 2
statuses + 3 workflow runs) | at most about 10 (3 workflow runs + 6
external checks + 1 status, only when green) |
| Jobs that run to completion | about 60–70 | usually 1–4: events that
arrive while a job is pending or running cancel it, so a burst collapses
to the newest event. In a backed-up queue, all events for one head
collapse to one job |

## Verification

- `node --test .github/scripts/tests/notify-pr-merge-review.test.mjs`:
22 tests pass.
- The new workflow test fails on the old workflow (red) and passes on
the new workflow (green).
- `actionlint .github/workflows/pr-merge-review.yml`: no findings.
- I confirmed with `gh run list` that `workflow_run.head_sha` for the
`pull_request_target` workflow (`commitperclip PR Review`) is the PR
head SHA. So one SHA key groups all event kinds for one PR head.
- I confirmed that all workflows that make PR checks are in the
`workflow_run` list (`CI routing` and `Trusted PR CI` are reusable
workflows called by `PR`).
- After merge: check that `gh run list --workflow pr-merge-review.yml`
shows mostly `skipped` runs for each push, and a small number of runs
that execute.

## Risks

- A check provider that reports a final green state through a GitHub
Actions check run from a workflow that is not in the `workflow_run` list
does not trigger the notifier. All current PR workflows are in the list.
The doc already tells authors to add new PR workflows to the list.
- A failed or cancelled check that later succeeds on re-run still
triggers the notifier, because the re-run completes with `success`.
- `cancel-in-progress` can cancel a notifier during delivery. The newer
job evaluates the same or a more complete state, and the Paperclip
idempotency key deduplicates deliveries for the same PR, head, and base.
- Each event still makes a workflow run record. Skipped runs do not take
a runner.
- Low risk otherwise: the notifier is advisory and `continue-on-error`.
It does not affect PR verification results.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code with tool use (shell, file edits, GitHub CLI).

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 08:24:45 -06:00
DottaandClaude Opus 5.5 b5b972ceb1 fix(runner): stop provider tool processes that leave the owned process group (#19727)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The local runner (`paperclip-runnerd`) supervises each provider
process, such as the ACPX sidecar for Claude, in its own process group
> - Run teardown stops that process group, and nothing else
> - Claude Code starts each Bash tool command in a new session
(`setsid`). A command such as `nohup ./bench.sh & echo $!` leaves a job
in that session, and the wrapper exits at once. The job is then a child
of init and outside the owned group
> - In a restart chaos storm, a run failed after a restart resume. Its
tool job kept running beside the task's next run, in the same workspace,
for more than four minutes
> - This pull request makes the supervisor record every process that its
provider starts, including processes that leave the group, and stop them
all before teardown reports success
> - The benefit is that a run's tools cannot outlive the run and cause
duplicate side effects in the next run

## Linked Issues or Issue Description

**What happened?**
A restart chaos storm ran a local ACPX Claude task. After a restart
resume, the agent ran `nohup ./bench.sh <nonce> > log 2>&1 & echo
"started pid $!"` and then ended its turn without a result. The run
failed with `native_finalization_missing`. The runner tore down the
sidecar group, and the guardian killed the provider group. `bench.sh`
was in the session that Claude Code made for the tool (pgid and sid =
the exited wrapper, ppid = 1). It was never signalled. It kept forking
`sleep 1` while the task's next run started and finished in the same
workspace.

**Steps to reproduce**
1. Run a local ACPX Claude task whose agent runs a Bash tool command
such as `nohup ./bench.sh x > log 2>&1 & echo $!`.
2. End the run (any outcome, including failure after a restart resume).
3. Run `ps -axo pid,ppid,pgid,command | grep bench.sh`. The job is still
running with ppid 1.

**Agent adapter(s) involved**
Claude Code (local ACPX). The gap is in the shared supervisor, so any
provider whose tools leave the group is affected.

**Root cause**
`SupervisedProcess`
(`packages/paperclip-runner/runner/crates/runner-core/src/process_supervisor.rs`)
spawns with `process_group(0)` and stops only that group in
`terminate_group`, `terminate_group_immediately`, `wait` and `Drop`. Its
own comment said that a caller that must exclude escaped or re-parented
descendants needs a separate lifetime fence. No caller had one. The ACPX
sidecar transport (`acpx_sidecar_transport.rs`, `shutdown` and `poison`)
and the ACPX provider guardian (`installation-integrity.ts`, which kills
only its own group) therefore left every `setsid` tool job behind. The
failure path after the resume was not special: the same gap applies to
every teardown.

**Expected behavior**
When a run ends for any reason, every process that its provider or tools
started stops, and teardown proves that they are gone. Identity comes
from PID, group, session and start time, never from a command name.

## What Changed

- Add the `paperclip-process-table` crate. It reads PID, parent, process
group and start time for every process (libproc on macOS, `/proc` on
Linux). `runner-core` forbids unsafe code, so the two checked libproc
calls live in this small crate.
- Add `process_lineage.rs` to `runner-core`. A sampler thread (every 50
ms) records each descendant of the supervised process by PID and start
time. It also records each group and session that a descendant leads,
with the leader's start time. A process belongs to the lineage when its
parent belongs, or when it is in a recorded group or session and started
no earlier than its leader. Groups and sessions that the supervised
process only inherited (runnerd's session) are never recorded. A
recorded ID whose PID now names a different process is dropped.
- `SupervisedProcess` samples the lineage just before it signals, and
after the leader is reaped it stops every live member: TERM, then KILL
after the shutdown grace. It fails with an error until it proves that no
member is alive (grace plus 5 s). This applies to `terminate_group`,
`terminate_group_immediately`, `wait` and `Drop`.
- If the lineage sampler cannot start, the supervisor stops the process
it just started and reports the failure, so a provider never runs
without tool cleanup.
- Table reads and state updates happen under one lock, so an older read
cannot erase members that a newer read recorded.
- The process table reports processes that exist but cannot be read. A
recorded member that cannot be read stays unresolved, and teardown fails
until its exit is proven.
- The Runner workspace now has two members.
`scripts/runner-release-crate-cache.mjs` and
`scripts/runner-debug-crate-cache.mjs` cache and mark current the
process-table lib unit with the Runner crate (otherwise Cargo rebuilds
the Runner crate after every restore). The debug contract accepts only
this library-only member and still refuses any other workspace change.
The workflow cache path lists and the debug clean step are updated to
match.
- `terminate_group_immediately` now accepts `EPERM` from the group
signal when the leader has already exited. macOS returns `EPERM` for a
group whose only member is the unreaped leader. The lineage pass then
proves that no other member remains.

## Verification

- `cargo test -p paperclip-runner-core --lib -- process_lineage tool_job
immediate_group_fence` (macOS, local).
- New real-process tests in `process_supervisor.rs`: a supervised shell
starts a wrapper with `setsid` (Perl `POSIX::setsid`), the wrapper runs
`nohup <loop> &`, writes the job PID and exits. The test asserts that
the job is outside the owned group and re-parented, then calls
`terminate_group`, `terminate_group_immediately` or drops the process,
and asserts that the job is gone when the call returns.
- Red: with the tracker disabled, all three fail with `tool job <pid>
outlived its provider`.
  - Green: all three pass.
- Unit tests in `process_lineage.rs` cover a re-parented job, a job that
left its group but stayed in the recorded session, a session made after
the first sighting, PID reuse, and no claim through the supervisor's
inherited session.
- `node --test scripts/__tests__/runner-release-crate-cache.test.mjs
scripts/__tests__/runner-debug-crate-cache.test.mjs` (new case: the
process-table library is cached with the crate, and any other member
still needs review).
- `cargo test -p paperclip-process-table` (includes an exited process
reported as gone), and `cargo test -p paperclip-runner-core --lib` (570
passed).
`acpx_provider_backend::tests::unconfirmed_suspension_state_becomes_recoverable_only_after_cleanup_and_suspend`
fails sometimes under full-suite load. It fails at the same rate with
the tracker disabled, so it is a flake that already existed.
- Sampler cost: one table read takes about 0.4 ms for 1,200 processes on
macOS (about 0.8% of one core for each supervised process).
- The restart-chaos replay (`--replay w-20261107T023921-s7106/t67`) did
not run. The local session was not permitted to load provider
credentials. It is not proven against a live Claude run.

## Risks

- Teardown now stops all the processes that a provider started, also
intentional background jobs. This is the intended contract: nothing
outlives the run.
- A wrapper that lives less than one sample (50 ms) and orphans a job
before the sampler sees it is not recorded. Claude Code's wrapper
sources a shell snapshot and lives for seconds. On Linux, a
child-subreaper would close this gap. That is not in this change.
- When `runnerd` itself is killed with SIGKILL, this code does not run.
The provider guardian still kills its own group, but escaped tool
sessions are not stopped in that case.
- Teardown can return a new error when it cannot prove the lineage
stopped. Callers already handle `terminate_group` errors as provider
shutdown failures.
- The server does not yet gate the next run on the tool lineage of the
prior run directly. It relies on the runner finishing its teardown
before the runner exits.

## 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>
2026-11-07 08:24:41 -06:00
Nicky Leach 45c2bd94c6 fix(native): name the interrupted attempt when an exact resume is refused (#19709) 2026-11-07 06:21:46 -08:00
DottaandClaude Opus 5.5 8373ae905a fix(accounting): settle a never-started run whose recorder bound an empty fence map (#19826)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Budgets hold new work while a native run's final usage receipt is
outstanding
> - A run that never started a provider has nothing to account. #19700
added a rule that settles such a run at once.
> - In restart chaos waves, Retry runs that failed before any provider
start still held company admission for up to 10 minutes
> - This pull request fixes the condition that stopped the rule from
matching
> - The benefit: a failed run that provably never started a provider
settles at once and never holds other work

## Linked Issues or Issue Description

Refs #19700, #19724.

**What happened?**
An OpenCode Retry run failed at once with
`native_session_cleanup_quarantined`. It had no model, no usage
receipts, no process start and no provider events, yet it held company
admission for the full 600-second closure bound. Two more waves had the
same shape, with holds of 309 and 324 seconds.

**Root cause**
`nativeProviderWorkNeverStarted` (`run-cost-accounting.ts`) required
`usage_json.accountingReceiptSourceFences` to be absent.
`createRunUsageRecorder` binds every run with that key, as an empty map
`{}`, even when no earlier receipt source exists. So the rule never
matched a run that had a usage recorder. The run then waited for the
closure bound as a run whose receipt was still closing.

The restart-evidence check for the unpriced-usage exemption
(`budget-exposure.ts`, from #19724) had the same flaw: it treated the
presence of the key as restart evidence.

**Expected behavior**
A failed native run with no receipts, no replaced receipt source and a
process that never launched (or was verified stopped) settles at once
with no charge. A run whose provider might still be spending keeps
holding.

## What Changed

- `run-cost-accounting.ts`: the never-started rule treats an empty fence
map as "no replaced source". Only a non-empty map shows that provider
work began.
- `budget-exposure.ts`: restart evidence also requires a non-empty fence
map.
- `doc/SPEC-implementation.md`: documents the rule.

## Verification

- Red then green, `closed-native-receipt-accounting.test.ts`:
- "settles a quarantined Retry whose recorder bound an empty fence map
at once". This is the exact chaos row: failed,
`native_session_cleanup_quarantined`, terminal_failure finalization,
receipt sequence 0, empty fence map, no receipts, no process events.
- "does not count an empty fence map as restart evidence for a timed-out
closure".
- Existing tests still pass. A run with a recorded but unverified
process still waits for the bound. The restart-derived exemptions still
apply to runs with a non-empty fence map or a recovery transition.
- Local: `closed-native-receipt-accounting` (51), the budget suites,
`heartbeat-cost-accounting`, `adapter-cost-failure-paths`,
`native-stop-accounting` and `native-session-executor` all pass. Server
`tsc --noEmit` is clean.

## Risks

- Low. The change only narrows what counts as a replaced receipt source.
A run whose source was really replaced has a non-empty map and behaves
as before.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), Claude Code agent with
tool use and code execution.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
canary/v2026.1107.0-canary.14
2026-11-07 08:15:49 -06:00
Dotta 3112e4c955 Verify original Dot OAuth authority for signed Cloud requests (#19819)
## Thinking Path

> - Paperclip manages AI agents and their work.
> - Dot uses a user OAuth grant to act in one company.
> - Cloud must use that original grant when it resolves a selected
user's identity.
> - A Cloud signature alone must not create user or board authority.
> - The tenant's OAuth owner must check the token and its current
restrictions.
> - This pull request adds a signed private HTTP boundary for that
check.
> - The result contains only the checked identity fields. It grants no
task or board access.

## Linked Issues or Issue Description

**Subsystem affected**

Server external-agent identity, public MCP OAuth, Cloud control
verification, and HTTP logging.

**Problem or motivation**

Cloud needs a private way to resolve the original Dot token for the
selected user, client, company and resource. The existing generic Cloud
control route creates an operator actor. That route is not the right
owner for this identity check.

**Proposed solution**

Add `POST /mcp/runner/oauth/cloud-principal`. Require the existing Cloud
signature and replay verifier. Bind the whole request to a new, closed
`mcp-dot:principal` action with a maximum 60-second lifetime.
Authenticate the original token once through the normal Dot OAuth owner.
Check current namespace authority before and after that call.

**Alternatives considered**

A Cloud-created grant or a board actor would change the authority owner.
Neither is used. The private route reuses the existing OAuth and Cloud
identity owners.

**Roadmap alignment**

This is a narrow boundary for the existing MCP and Cloud areas in
`ROADMAP.md`. It adds no work queue, public hosted profile, or provider
enablement.

Related public work: Refs #14846, #19427, #19750 and #19776. Searches
for the exact private path and action found no duplicate PR. The
matching private Cloud caller has merged. The current App head passed
its required checks and review.

## What Changed

- Add an original-token principal reader. Keep the original client,
selected user, company, resource, purpose, scope, membership and feature
restrictions.
- Add a 16 KiB raw JSON private route. Reject compressed, invalid UTF-8,
malformed and unknown-field bodies. Bind an ordered whole-body digest to
the signature and existing nonce fence.
- Compare the current stack, claim, instance, origin and connector
identity before and after OAuth. An absent owner or namespace fails
closed.
- Mount the route in normal `createApp` after hostname, runtime-claim
and standby checks, before session/actor work. Do not add it to generic
board-control routing.
- Keep private request and response logs content-free, including raw
buffers, headers, query data and reflected errors.
- Add controlled HTTP/privacy tests and a real normal-App
OAuth/Cloud-signer integration case. Document the machine-only contract
and correct the existing `assistant-connections/client-use` access
entry. No new capability ID or agent API entry is added. No schema,
lockfile, workflow, Runner, Muse or useful-task fixture changes belong
to this PR.

## Verification

- Current `70c03593`: normal module-boundary policy and all seven App
compiler programs passed. The public module factory has an explicit
portable Express return contract. Whole HTTP/log controls passed again:
30/30. The existing real paired OAuth/Cloud-signer/private HTTP case
then passed again on a fresh PostgreSQL fixture: 1/1, zero skipped. All
known child processes and five database connections closed; the final
independent SQL audit was empty.
- The original remote policy failure rejected an App import of a
module-internal path; dependent E2E/verify gates stopped on that
failure. The fix delegates through the public module API. A first local
declaration run reported TS2883 for its inferred return type; the
explicit public type fixed it. Original logs remain preserved. No
checker, compiler option, test body or authority check was weakened.

- The normal full App graph passed on the integrated source. The later
fixture-only repairs each passed the complete affected owning
program:405 entries, including 403 source roots. The remaining programs
and own dependency outputs carried only where their source was exact.
- Whole private HTTP/log files passed:30 tests, zero failures or skips.
The HTTP controls use a controlled OAuth owner with real signatures. The
log controls use the actual HTTP logger.
- The paired Cloud source passed its normal strict build and whole 26
signer/client/namespace and credential-error controls. These are
controlled caller tests, not provider tests.
- One normal-App paired case passed with a dedicated PostgreSQL
database. It used real DCR 201, PKCE, consent, token exchange and a
persisted unpaired grant; actual signed metadata publication and a
committed Cloud receipt; the real Cloud signer/client and actual App
HTTP; exactly-once authentication; refresh; current namespace
restrictions; and revocation. The Cloud registry/provider were the
existing controlled in-memory/Fake fixtures. Recorded inbound Hosts
matched, with zero metadata overflow. No grant, readiness or mirror ACK
was fabricated.
- The original principal helper's 21 controlled tests and two real-OAuth
tests passed before the HTTP slice. Those three files remained unchanged
through integration. They are separate source-scoped evidence, not an
extra combined run.
- Preserved failures: a helper import and test typing error were
corrected before the passed checks. An earlier suite failure did not
serialize its hook error and remains unexplained. Later runs captured a
missing normal managed-config initialization and HTTP 404 from the
loopback mapper. The fixture now calls the normal initialization owners
and uses the existing Host-preserving canonical HTTP adapter. All
original authority assertions and deadlines remain.
- Reproduce the local files with `pnpm --filter @paperclipai/server exec
vitest run src/modules/external-agents/cloud-principal-http.test.ts
src/modules/external-agents/cloud-principal-log.test.ts --maxWorkers=1
--no-file-parallelism`. The real integration file is
`server/src/__tests__/cloud-principal-normal-app.postgres.test.ts`; it
requires its explicit dedicated test DB and an own compiled matching
Cloud checkout.
- The normal `node scripts/check-maps.mjs` check passed for 35 areas and
192 capability IDs. This validates map structure, not business
acceptance. The later edits are documentation/map only; all executed
business source and own outputs from the paired case remained exact.
- Current-head remote CI passes: 59 successful checks and three
intentional skips. Greptile reviewed this exact head at 5/5 with no
findings or review threads. The prior Ponytail review was Ship 5/5 at
the earlier head; no current-head Ponytail turn is present, which is
nonblocking under the authorized review policy. The three
full-repository commands were not repeated locally; the focused normal
App graph and affected tests above passed, and required remote CI
supplies the broad gates. No deployed, provider-capacity, cold-wake or
full hosted lifecycle result is claimed.

## Risks

- This is a security boundary. Signature, current namespace and original
OAuth restrictions must continue to fail closed. The response gives
identity only; callers still need normal operation authority.
- The request carries the original token. The reserved route uses a
closed log projection on both accepted and refused requests. Privacy
regressions remain a material risk.
- Both repositories must agree on the action and digest contract. Older
code rejects the unsupported action; no fallback creates authority.
- This is a prerequisite private boundary. The remaining hosted
discovery, wake, capability, operation, event, retry/reactivation and
revocation lifecycle work, plus Muse, remains open. No profile or
capability is enabled here.
- No migration is needed. Rollback removes the private boundary and
paired caller; normal OAuth and Personal MCP remain with their existing
owners.
- The engineering documentation describes this private machine boundary
and its access restriction. No operator-facing feature is enabled.
Documentation for full hosted enablement remains with that later work.

## Model Used

OpenAI `gpt-6.1-sol` (Sol6.1), through Codex, produced the source and
tests with tool use and local compiler/test execution. A GPT-6-based
Codex coordinator reviewed the work. Its exact model ID is unavailable.
Context window sizes are unavailable.

## 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
2026-11-07 08:02:42 -06:00
DottaandClaude Opus 5.5 b2af45f0f1 fix(runner): let OpenAI API-key Codex agents generate images with their existing key (#19654)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - New users often connect a Codex agent with an OpenAI API key during
onboarding.
> - A user asked such an agent for a logo. The agent said that the
built-in image generator was not available and that its fallback needed
a separate API key. It then drew an SVG by hand.
> - #19516 removed `--disable image_generation`. That change is correct,
but it does not reach this path. The pinned Codex 0.160.0 shows its
`image_gen` tool only when Codex signs in with ChatGPT. The check is
`image_generation_available` in `codex-rs/core/src/tools/spec_plan.rs`.
An API key never gets the tool.
> - The runner keeps the provider key out of the agent's shell on
purpose, so the skill's `scripts/image_gen.py` fallback cannot run.
> - This pull request adds a Paperclip `generate_image` tool. The server
makes the image with the agent's existing OpenAI key. The key never goes
to the agent process or the shell.
> - The benefit is that API-key Codex agents can make images with no
second key. The spend goes to the agent's budget and the budget can stop
it.

## Linked Issues or Issue Description

Refs #19516

**What happened?**
An agent connected through onboarding with an OpenAI API key (Codex
harness, `gpt-5.6-sol`) could not generate images. The agent said: "The
image skill's built-in generator isn't available in this run, and its
fallback would require a separate API key." It then drew an SVG instead.

**Steps to reproduce**
1. Create a company with a Codex agent that uses an OpenAI API key.
2. Ask the agent for a raster image, for example a logo PNG.
3. The agent reports that image generation is not available. It does not
make the image.

**Expected behavior**
The agent makes the image with the OpenAI key that already powers it.

**Root cause**
The agent ran on the native runner's Codex app-server path, with
`auth.json = {"OPENAI_API_KEY": ...}`. Codex 0.160.0 removes
`image_gen.imagegen` from the model's tools unless
`current_auth_uses_codex_backend()` is true. That function is true only
for a ChatGPT sign-in.

I proved this with the real pinned binary. I gave it the runner's
isolated launch arguments and the API-key auth file, and pointed it at a
local Responses endpoint. The first model request offers only `exec`,
`wait`, `request_user_input` and the collaboration tools. #19516's test
checked only `codex features list`, so it could not see this gate.

## What Changed

- **New `server/src/services/native-runtime/generate-image-tool.ts`:**
  - The tool schema and definition.
- `imageGenerationBindingForRun`. It applies only to native Codex runs
on a direct OpenAI `api_key` connection with no router selection.
- The OpenAI Images API call (`gpt-image-2`, `b64_json`, PNG check, 180
s timeout). The reply is read as a stream with a byte cap before it is
parsed. A decoded image larger than 20 MiB is refused.
- A reviewed rate card for `gpt-image-2`: text input $5, image input $8
and image output $30 per 1M tokens, from
developers.openai.com/api/docs/pricing, retrieved 2026-11-06.
  - A create-only, symlink-confined local write.
- **`paperclip-runner-tool-authority.ts`:**
  - Adds the `imageGeneration` binding.
- Advertises the tool only for standard mode with a workspace. A remote
target must also have an installer. Dot, Muse, Grok and native review do
not get the tool.
- Adds the `#generateImage` handler. Before any spend, it checks again:
the run and task, budget authority, `issue:mutate`, case handoff, and
the connection.
- Stores a claim-before-spend per-call receipt in
`resultJson.imageToolReceipts`. A retry or recovery replays the receipt
and does not pay again.
- Saves a `charged` receipt (usage, price, request ID and charge time)
as soon as OpenAI returns. A failed save, failed accounting write or
crash can then still settle the spend. A retry or recovery records the
cost once and never buys again.
- Records one `cost_events` row through `createCostEventInTransaction`.
The row is `metered_api` with `costStatus: "estimated"`, and the budget
is evaluated. The row is recorded even if saving fails.
- Checks the task's project budget, as well as the company and agent
budgets, before spending.
- Writes content-free `runner.image_generate` activity entries: started,
completed, failed and unconfirmed.
- **`ai-connections.ts`:** adds `selectImageGeneration`. It checks again
that the connection is an active, healthy OpenAI `api_key` connection
with no custom routing. It also checks the responsible user's
membership, the grant audience and the agent or company installation.
These are the same rules that runtime selection uses.
- **`native-session-executor.ts`:** gets the binding from the run's
admitted `contextSnapshot.aiConnection`. The value comes only from the
server, never from tool input.
- **`native-session-resume.ts`:** the native tool contract changes from
`v18` to `v19` and now lists `generate_image`. A persisted provider
thread cannot add tools when it resumes, so older threads must rotate.
- **`doc/runner-api-tools.md`:** a new section about image generation
for OpenAI API-key Codex agents.
- **#19516: kept, not reverted.** Its changes have no effect on the
API-key path. They are still correct for Codex agents that sign in with
ChatGPT, because `image_gen` works there and saves to
`CODEX_HOME/generated_images`. Those agents need the read grant that
#19516 added. On the API-key path, the new tool replaces the built-in
tool. The tool's description tells the agent to use it instead of
`image_gen` or `scripts/image_gen.py`.

## Verification

**Red on `master` (e482f0f632).** Behavioural red with the first version
of the new test:

```
 FAIL  src/services/native-runtime/generate-image-tool.test.ts > generate_image for OpenAI API-key Codex agents (N-12) > lets an API-key Codex agent make an image with its existing OpenAI key, saved in its workspace and billed
AssertionError: expected [ 'connections_search', …(51) ] to include 'generate_image'
 ❯ src/services/native-runtime/generate-image-tool.test.ts:78:60
      Tests  1 failed | 3 passed (4)
```

The final test file on master's source also fails. The reason is that
the module does not exist: `Cannot find module
'./generate-image-tool.js'`.

**Real pinned Codex binary.** The new test is
`packages/paperclip-runner/src/drivers/codex/codex-api-key-image-tools.test.ts`.
It uses the runner's isolated launch, the API-key `auth.json` and a
local Responses endpoint. It shows that the model gets no `image_gen`,
`imagegen` or `image_generation` tool, and that a server-advertised
`generate_image` tool does reach the model:

```
 Test Files  1 passed (1)
      Tests  1 passed (1)
```

**Green after the fix:**
`server/src/services/native-runtime/generate-image-tool.test.ts` uses
embedded PostgreSQL and a stubbed OpenAI fetch.

```
 Test Files  1 passed (1)
      Tests  11 passed (11)
```

The 11 tests cover these behaviours:

- **Main path.** The tool is advertised. The request goes to
`api.openai.com/v1/images/generations` with the existing key. The PNG is
written to the workspace. The key does not appear in the result or in
activity. One `cost_events` row is written: 50 text tokens and 1000
output tokens, which is $0.03025, estimated, from the rate card. A retry
and `recover()` replay the receipt with no second fetch and no second
cost row.
- **No binding:** the tool is not offered and `execute` refuses.
- **Budget hard-stop.** The stop pauses the agent, so the call is
refused before any fetch and no file is written.
- **Bad grant:** the call is refused before any fetch.
- **Project budget stop.** The task's project is paused by its budget,
so the call is refused before any fetch.
- **Charge settlement.** A crash left a `charged` receipt. The retry and
`recover()` record the cost once, report the image path and do not buy
another image.
- **Oversized reply.** The read stops before the body is parsed.
- **Save failure.** The spend is still recorded. A same-call retry does
not pay again and reports the failure.
- **Binding gate.** Only Codex on a direct OpenAI API key gets the
binding. A subscription, ACPX, OpenRouter or a routed connection does
not.
- **Rate card math.**
- **Provider errors.** The error text does not contain the key, and
non-PNG data is rejected.

**Neighbouring suites:**

- `src/drivers/codex` in the runner package: 24 files, 350 tests passed.
- Server: `paperclip-runner-tool-authority.test.ts`,
`native-session-resume.test.ts`, `fast-responses.test.ts`,
`ai-connection-session.test.ts` and the new suite. 157 of 158 passed.
- The one failure is `native-session-resume.test.ts › archives a damaged
prior epoch … with real runnerd` (ENOENT on `authority-epochs`). It
fails the same way on unmodified `master` in this local environment, so
this change did not cause it.
- `tsc --noEmit -p server/tsconfig.json`: clean.
- `pnpm verify` exits 2 (broad). Required CI supplies the broad
coverage.

## Risks

- **New security surface: this needs maintainer sign-off.** The server
now sends the agent's OpenAI API key to `api.openai.com` on the agent's
request. The tool is limited in these ways:
  - The key stays in the server.
- Each call checks run, task, budget, mutation and connection authority
again.
- The destination name comes from the server and must match
`^[a-z0-9-]+-[0-9a-f]{8}\.png$`. The write is create-only and confined
to the workspace root. On macOS it uses `O_NOFOLLOW_ANY`. On Linux it
uses a `/proc` descriptor walk.
- **Spend.** Each call costs money. The tool description tells the agent
to make only what the task needs. The cost is an estimate from a rate
card (`costStatus: "estimated"`), not an invoice. The rate card applies
the image-output rate to all output tokens, so the estimate errs on the
high side.
- **Checkpoint rotation.** The tool-contract bump from v18 to v19 makes
existing native provider threads start fresh once at their next turn.
This is what the contract requires when the tool surface changes.
- **Pricing drift.** If OpenAI changes `gpt-image-2` prices, the rate
card must be updated.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code with tool use (shell, file edits, sub-agents) and extended
reasoning.

## 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>
2026-11-07 07:48:39 -06:00
DottaandClaude Opus 5.5 5eac099fbb fix(agents): batch independent follow-up questions into one card (#19634)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Agents ask users for input with the `request_human_input` tool or
the `ask_user_questions` interaction. Paperclip shows each call as one
question card.
> - In an onboarding walk, a Codex execution agent asked five
independent questions (name, menu and prices, contact, style, output).
It posted five cards, one after the other. Each card came 20-60 s after
the previous answer.
> - The tool description said: "For one question at a time, ask only the
next unanswered question and wait for its real answer." Agents read this
as a rule. The paperclip skill showed only a one-question example and
did not tell agents to batch.
> - No server cap exists. A question set accepts up to 64 questions.
> - This pull request changes the guidance. Agents now ask independent
inputs together in one card. They use a follow-up card only when a
question depends on an earlier answer. One-at-a-time stays available
when the user asks for it.
> - The benefit is fewer waits for the user: one card and one agent turn
instead of five.

## Linked Issues or Issue Description

**What happened?**
An execution agent asked a user five independent follow-up questions in
five separate question cards. Each card appeared only after the user
answered the previous card and the agent finished another turn (20-60 s
each).

**Expected behavior**
When an agent needs several independent inputs, it asks them together in
one multi-question card. The onboarding interview already does this ("1
of 4" pager).

**Steps to reproduce**
1. Complete onboarding on the Codex API path and hand off a task such as
"Create printable cake menu and logo".
2. Watch the execution agent's first turns.
3. See five sequential single-question cards.

**Paperclip version or commit**
master at 4938a930d6 (local dev server).

**Agent adapter(s) involved**
Codex (gpt-5.6-sol). The guidance is shared by all adapters that use the
Runner `request_human_input` tool or the `paperclip` skill.

## What Changed

-
`packages/paperclip-runner/src/protocol-actions/request-human-input.ts`:
replace the ambiguous "For one question at a time…" sentence. The new
text tells agents to ask independent inputs together in one questions
card. They use a follow-up card only when it depends on an earlier
answer. It keeps one-at-a-time for when the user asks for it.
- `skills/paperclip/SKILL.md` and
`skills/paperclip/references/api-reference.md`: add the same batching
rule to the human-input sections.
- `packages/mcp-server/src/tools.ts`: add the batching hint to the
`paperclipAskUserQuestions` tool description.
- Regenerated runner contracts (`semantic-tool-contracts.json`,
`protocol/manifest.json`, `native-execution-seeded.json`) and updated
the legacy MCP alias semantics in `spec/capability/mcp-tool-map.yaml`.
- New tests: `core-contracts.test.ts` (tool description) and
`paperclip-skill-utils.test.ts` (skill and API reference).

## Verification

Red on master (tests added, source unchanged):

```
packages/paperclip-runner: npx vitest run src/protocol-actions/core-contracts.test.ts -t batch
 × tells agents to batch independent questions into one card instead of asking them one by one
AssertionError: expected 'Create a durable human question or ap…' not to contain 'For one question at a time'
 Tests  1 failed | 20 skipped (21)

npx vitest run server/src/__tests__/paperclip-skill-utils.test.ts -t batch
 × tells agents to batch independent human-input questions into one card
AssertionError: expected ' For an open answer, use a text field…' to contain 'ask them together in one card'
 Tests  1 failed | 21 skipped (22)
```

Green after the fix:

```
packages/paperclip-runner: npx vitest run src/protocol-actions src/drivers/runner-tool-bridge.claude-schema.test.ts
 Test Files  4 passed (4)   Tests  46 passed (46)
npx vitest run server/src/__tests__/paperclip-skill-utils.test.ts            Tests 22 passed (22)
npx vitest run server/src/services/native-runtime/paperclip-runner-tool-authority.test.ts   Tests 41 passed (41)
node scripts/generate-semantic-contracts.mjs --check / generate-capability-contract --check / generate-semantic-action-catalog --check / generate-replay-goldens --check: current
```

`packages/mcp-server` has one failure ("allows create issue requests to
omit status…"). It also fails on master without this change, so it is
not related.

This is a guidance change. The automated check proves that the guidance
the agent receives tells it to batch. I did not run a live-model eval.

## Risks

- Low risk. The change is text only: tool descriptions and skill docs.
No schema, API, or storage change.
- An agent could put a dependent question into the same card. The text
tells agents to use a follow-up card for dependent questions.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with tool use and
code execution.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] 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>
2026-11-07 07:48:28 -06:00
DottaandPaperclip 8812c1941d perf(storage): overlap bounded S3 multipart payloads (#19817)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The storage service uploads large workspace snapshots through S3
multipart uploads.
> - The current uploader permits four requests but fills each streamed
body before it starts the next one.
> - Slow payload delivery therefore remains serial even when request
acknowledgements overlap.
> - This pull request fills bounded part buffers and sends those
payloads concurrently through the existing SDK streams.
> - This lets the four-request window carry payload data while it
retains backpressure and failure cleanup.

## Linked Issues or Issue Description

Related implementation: #18659. This fixes payload serialization within
that existing multipart owner. A search found no duplicate open storage
multipart fix.

**What happened?**

A controlled 24 MiB upload started part 2 only after part 1 had
delivered 99.22% of its bytes. The same sequence occurred for part 3.
The four-request cap overlapped acknowledgement waits but did not
provide useful overlap during payload delivery.

**Expected behavior**

Up to four part payloads can travel at the same time. Memory stays
bounded. Exact lengths, bytes, ordered completion, cancellation and
abort cleanup still apply.

**Steps to reproduce**

Run the new slow-consumer test against the previous uploader. Hold all
part consumers until four requests have started. The previous
implementation starts one request and the assertion fails. The revised
implementation starts four requests and then verifies every payload hash
and byte count.

**Paperclip version or commit**

Reproduced on the source from `585c885168`. The final branch is rebased
onto `ae1f781cac`.

**Deployment mode**

The S3 storage provider. Local tests use fake credentials and an owned
loopback HTTP server. No hosted writes were made.

## What Changed

- Share a 32 MiB part-buffer allowance across uploads. Stream up to four
buffered payloads concurrently through the existing SDK requests. When
the allowance is full, use the existing streaming path without an upload
queue.
- Keep the original streaming path for dynamic parts larger than 8 MiB.
This retains the provider part-count limit without larger buffers.
- Extend the existing tests for concurrent slow consumers, the shared
memory window and cancellation in both paths. Verify allowance release
after source and request failures. Check part identity independently of
acknowledgement order.

## Verification

- Baseline RED: the new controlled test failed with one request started
instead of four. Its cleanup released and awaited the upload.
- Final review-repair head: 26/26 tests passed across
`storage-s3-multipart.test.ts`, `storage-s3-failure-diagnostics.test.ts`
and `storage/config.test.ts`.
- These tests include the real S3 HTTP client, exact payload hashes and
ordered parts, excess and truncated input, early acknowledgements,
sibling failure, abort cleanup, stalled input and the bounded four-part
window.
- Strict server TypeScript, module boundary checks and `git diff
--check` passed on the final review-repair head. CI then caught an
optional SDK `Key` type in the new test fixture. A focused test
typecheck reproduced both TS2345 errors. The fixture now binds its
required controlled key; that typecheck and the named shared-window
regression pass. The uploader source is unchanged by this repair.
- Controlled 24 MiB comparison: 640.58 ms before and 393.88 ms after.
The new requests started within 7.37 ms. Both runs used 1 ms per
consumed chunk and 250 ms acknowledgement delays. This measures the
controlled fixture only. It does not predict hosted S3 throughput or
total migration time.
- Review RED: two blocked uploads consumed 64 MiB of source chunks under
the original per-upload buffer scheme. The new shared allowance kept the
source reads within 32 MiB plus stream lookahead. Three new controls
passed for concurrent uploads and allowance release after source/request
failures.
- The first treatment run passed 12/13 cases. Its remaining test assumed
completion order matched part order. The test now binds lengths to part
numbers and still checks the ordered completion request. An initial
typecheck setup attempt lacked the built plugin SDK; the canonical
dependency build and final typecheck passed.

- Final head `fbadf4f1cf427b896bbc255ffe2e7ad480ce06d3`: [full
CI](https://github.com/paperclipai/paperclip/actions/runs/42373571408)
passed, including build, test shards, Typecheck + Release Registry and
`ci / verify`. All 61 reported contexts are terminal: 56 success, four
skipped and one neutral review. Greptile and Paperclip both gave a fresh
5/5 for this head. No review threads remain open.

## Risks

- Multipart uploads share at most 32 MiB of live part buffers per
process, plus existing stream and source-chunk overhead. The previous
path used smaller streaming buffers. With no buffer allowance left, an
upload uses the original streaming path. It does not wait for a shared
queue.
- Objects whose dynamic part size exceeds 8 MiB retain the current
serial payload behavior. No new object-size ceiling is introduced.
- Request bodies remain non-replayable streams. The SDK retry policy,
multipart protocol, cleanup policy, object format and public storage API
remain unchanged.
- No disk spool or cache is added. All current-head CI gates and both
reviews passed.

## Model Used

OpenAI `gpt-6.1-sol`, with reasoning, tool use and code execution. The
context window size is not exposed by this session.

## 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.1107.0-canary.13
2026-11-07 06:58:10 -06:00
DottaandPaperclip c92bc9e5ec perf: reuse authenticated workspace upload readback (#19811)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Workspace import saves local work to an encrypted immutable snapshot
before it changes storage authority.
> - The snapshot uploader reads the whole object back to verify its
ciphertext.
> - The importer then downloads that same object again for authenticated
decryption and restore.
> - Both reads are required by the current call sequence, but they need
not transfer the object twice.
> - This PR lets that caller retain the authenticated upload readback in
private scratch.
> - The importer still restores the full archive, recaptures the source,
checks ownership, and publishes through the existing CAS.

## Linked Issues or Issue Description

Refs #19642, #19805, and #19810 for the existing workspace import and
preservation owners.

**What happened?**

A fresh workspace import performs a PUT and two full GETs of the same
encrypted snapshot. The first GET verifies and discards the object. The
next GET decrypts it for restore. Already-uploaded and lost-reply
recovery paths also repeat the GET.

**Expected behavior**

Use one authenticated readback for the exact final winning reference.
Preserve the default upload behavior and returned reference. Keep every
restore, source, recovery, ownership, and publication check.

**Steps to reproduce**

1. Capture a local source through the real workspace archive codec.
2. Upload it through the real snapshot ledger and filesystem object
backend.
3. Load and restore its archive. Count the object GETs.
4. The three baseline cases each make two GETs. The opt-in path now
makes one and restores the same bytes after the original source is
removed.

**Paperclip version or commit**

Baseline `585c885168`. The first PR head was `5b18121609`; the follow-up
repairs the review findings without changing the storage optimization.

## What Changed

- Add optional `verifiedArchivePath` to the existing upload owner. The
default path and `WorkspaceSnapshotRef` return contract stay unchanged.
- Authenticate each readback with the existing ciphertext, GCM,
key-binding, and plaintext checks.
- Bind retained files to the full winning reference. Recheck private
file ownership and plaintext SHA/size before atomic publication to a new
caller-owned path.
- Clean all invocation scratch on success or failure. Do not overwrite
caller files or reuse state across calls.
- Use this path in POSIX workspace import. Keep full extraction, the
second source capture, closed alias/source/F/writer/task checks, and CAS
publication. Windows keeps the existing download path and does not
bypass private-file checks.
- Cover real cold restore, fresh/replay/lost-reply GET counts, late
winners, private-path rejection, local tampering, and GCM failure.

## Verification

- Genuine baseline RED: three real PostgreSQL/crypto cases each report
two GETs where the new contract requires one.
- GREEN: all 30 snapshot storage cases, one actual online migration
case, and seven focused migration controls pass; 38 total. The other 232
cases in those files were unselected locally.
- Review RED: four obsolete second-GET fault triggers and one
uploaded-journal retry case failed before repair. The triggers now fire
on the retained readback and assert that they fired. The
uploaded-journal case interrupts after its real ledger commit and proves
recovery and cold Git restore through the same journal.
- A focused platform-selection control proves Windows retains the
original two-GET path. This is not native Windows acceptance.
- Strict server TypeScript, module boundaries, and `git diff --check`
pass.
- Local 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-snapshot-storage.test.ts
src/services/remote-workspace-online-source.test.ts
src/services/remote-workspace-migration.test.ts --testNamePattern
'remote workspace snapshot storage|imports an alias with
unrelated|removes plaintext verification files|rechecks a service that
becomes|rechecks a writer that appears|preserves a local edit|keeps the
existing download path|missing-base-uploaded-retry' --hookTimeout 90000
--maxWorkers 1 --no-file-parallelism --fsModuleCache`.
- The codec timing control used an exact earlier owner and a synthetic
72.9 MB archive. Discarding final-capture output saved only 3%; this PR
keeps that capture unchanged.
- This removes one full object GET. It does not claim a measured hosted
wall-clock improvement.
- Fresh Greptile review is 5/5 on `0933a0a29a`, with no open findings.
Exact-head CI is complete and green: 56 successful checks, four intended
skips, and one neutral result. The second review returned Ship (4/5);
its optional test-maintainability suggestion is deferred. No source
change or CI restart was made for that suggestion.

## Risks

- A late upload can win with a different reference. The retained file
must match all fields of that final reference; otherwise the owner
authenticates the winner.
- A private file can change before relay. Ownership, no-follow reads,
stable file identity, plaintext SHA/size, and no-overwrite publication
protect that boundary.
- Authentication may overlap the encrypted upload scratch. Peak local
disk use can grow by up to one ciphertext file. Data remains streaming;
no whole-object memory buffer is added.
- Opt-in callers must supply a canonical private directory owned by the
process. Existing callers that omit the option retain their behavior.
- No archive format, database schema, provider lifecycle, writer
protocol, admission, or background polling changes.

## Model Used

OpenAI `gpt-6.1-sol`, with reasoning, tool use, and local code
execution. 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>
canary/v2026.1107.0-canary.12
2026-11-07 06:33:12 -06:00
DottaandPaperclip ae1f781cac fix(native): preserve web files without settling remote recovery (#19814)
## Thinking Path

> - Paperclip manages work that AI agents perform for users.
> - Native sessions retain files on the web server and state in a remote
provider.
> - Replacing the web server must preserve its files without changing
remote recovery authority.
> - The existing maintenance owner requires destroyed-provider receipts
even when all web writers have settled.
> - This can keep the old web volume required only because a remote
recovery obligation remains unresolved.
> - This pull request adds a closed web-byte evidence mode to the
existing preservation owner.
> - The benefit is durable web-file coverage with unchanged remote
references, failures and continuation policy.

## Linked Issues or Issue Description

Refs #19748, #19806. A search for native harness filesystem evidence and
open web/harness changes found no duplicate implementation.

**What happened?**

A settled successful native source can retain a raw controller, a
separate warm supervisor and provider-home files while its historical
external lease has no termination receipt. The existing
filesystem-independence owner refuses before it can preserve these web
files. One retained skill file can also remain physically present after
its version row was deleted.

**Expected behavior**

Preserve every nonexcluded web file as authenticated evidence. Keep
missing files absent and deleted skill versions deleted. Leave VM
references, recovery actions, failures and execution authority
unchanged. Validate the published web-byte receipt on an empty
replacement using database bindings only.

**Steps to reproduce**

Use the included successful-source fixture with a settled web owner, an
unresolved external lease, real controller/supervisor files and a
retained skill whose version row is deleted. The baseline rejects at its
destroyed-provider guard. The new explicit maintenance mode preserves
the files and validates on a replacement. A separate fixture uses a
prior successful source, a failed retained last run, a genuinely absent
controller, a warm supervisor and 267 durable checkpoint parts.

**Paperclip version/commit**

Baseline `384fa2b7be`; final branch rebased onto `585c885168`. Current
head: `990210d7e9`.

**Deployment mode**

Original-volume maintenance followed by an ephemeral replacement. No
hosted operation or provider request is part of this PR.

## What Changed

- Add explicit `web_evidence` maintenance mode and a separate
`paperclip.native-harness-web-evidence.v1` receipt. Keep the existing
terminated-provider mode and old manifest shapes.
- Archive the closed canonical session-root inventory, the separate warm
supervisor and exact admitted skill manifests through the existing
encrypted `original_volume_source_evidence` owner. Preserve bytes and
modes. Keep canonical credential exclusions.
- Preserve local skill edits and deleted-version files as forensic
evidence. Create no skill version, executable checkpoint, provider
backup or recovery authority.
- Require original-volume capability, settled web writers,
physical/finalizer locks, exact task/source/cohort/input bindings and
final byte/mode/membership comparisons. Keep genuinely absent raw
controllers absent.
- Bind allocation and stamp identity while allowing later external
status and cleanup observations. Validate replacements from durable
database and archive bindings without the original anchor, filesystem or
storage reads.

## Verification

- Node 24.22.2, PostgreSQL 18.1, macOS arm64. The unchanged baseline
fails at the real destroyed-provider guard. The final revision passes
all 85 owner, original-volume capability and unchanged
continuation-policy cases in 21.14 seconds.
- Tests perform real encryption, persistence through the S3 storage
interface backed by the owned local fixture, authenticated restore and
exact byte/mode comparisons. The 281-byte file remains present while its
version row remains absent. The evidence archive stays separate from the
provider-session backup scope.
- The supervisor-only case preserves 267 database checkpoint parts,
keeps the raw controller absent and validates without original-volume
identity. Existing strict-mode and old receipt controls pass.
- Negative controls cover active web writers, expired but owned
finalizers, physical owners, foreign controller/supervisor identities,
missing manifests, symlinks, changed bytes/modes/membership/source
during upload, changed durable bindings, incomplete receipt coverage and
invalid late publication. Concurrent attempts retain one valid winner
and one audit event.
- Server TypeScript, module boundaries, diff checks and private-data
scans pass. The verification planner's first invocation used a wrong
script path; its canonical invocation refused a plan because fixture
inputs changed during selection. Neither attempt counts as a pass.
Required CI will provide broader application coverage.
- Earlier fixture and treatment failures remain recorded. They exposed a
missing helper, JSONB property-order instability, missing
separate-supervisor mode coverage and fixture timing/source/count
errors. The final same owner suite passes without weakened assertions.
- A canonical cleanup audit found two additional observation fields: the
cleanup controller and manual-attempt ID. Their real publication updates
reproduced false receipt invalidation on the first PR head. The narrow
correction keeps WEB coverage valid through those observations while
changed allocation identity still fails. The exact same 85-case suite
passes afterward. The final `990210d7e9` head has fresh Greptile 5/5
with no comments or unresolved threads. Required CI run
[42370607882](https://github.com/paperclipai/paperclip/actions/runs/42370607882)
passes: 55 successful checks and four intended skips, plus successful
Snyk. The old head's CI was superseded by this correction and is not
counted as a pass.

From `server/`:

```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 src/services/native-runtime/native-harness-original-volume.test.ts src/services/native-runtime/native-harness-filesystem-independence-continuation.integration.test.ts --hookTimeout 90000 --maxWorkers 1 --fsModuleCache
```

## Risks

This changes a maintenance trust boundary. It proves web-byte
preservation, not provider recovery or termination. External recovery
obligations and existing resume failures remain. Unknown layouts,
missing manifests, active local owners and changed authority fail
closed. Invalid receipts are not automatically replaced.

The inventory has independent limits of 128 entries, 16 MiB per file and
64 MiB total. Larger valid data stays on the original volume and blocks
this mode. Canonical credential exclusions remain excluded. Evidence
uses the existing encrypted ledger and retention roots; it does not
become an executable backup. Old receipts remain byte-compatible.

Startup adds bounded per-retained-scope database reads. Measure the
affected cohort's actual serving startup cost before enrollment.
Validation adds no filesystem or object read, worker or recurring poll.
Live source qualification and publication remain separate after review,
merge and normal delivery.

## Model Used

OpenAI Codex, GPT-6 family, with reasoning, repository tools and
PostgreSQL/filesystem test execution. The runtime does not expose a more
specific serving model ID or context-window size.

## 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>
2026-11-07 06:16:04 -06:00
DottaandPaperclip 585c885168 fix: preserve prepared sources held by provider artifact failures (#19810)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Online workspace migration archives local files before it changes
their storage authority.
> - A failed provider session can leave an active board-owned recovery
hold over a prepared local source.
> - The existing migration owner accepts finalized sources and
workspace-export failures.
> - It rejects a settled provider-artifact failure even when the
prepared source and recovery allocation have complete bindings.
> - This PR permits that exact preservation case under the existing
source and finalizer locks.
> - The remote recovery hold remains active. The archive preserves local
bytes and the existing recovery recipe.

## Linked Issues or Issue Description

Refs #19642 and #19805 for the existing online migration and
source-closure owners.

**What happened?**

Online workspace migration rejects a prepared source held by a terminal
`runner_remote_provider_artifact_incompatible` recovery. This also
blocks another local workspace that shares the Git source. The provider
session had executed work. A pre-execution exemption would be unsafe.

**Expected behavior**

Preserve the local source through the existing immutable archive owner
when the board recovery, task version, terminal run, finalizer, and
declared allocation all match. Keep the remote recovery action and
authority unchanged.

**Steps to reproduce**

1. Create a prepared native source and a failed run with provider
session and tool events.
2. Set its finalizer to an unowned terminal artifact failure. Add the
matching active board recovery and task status version.
3. Remove the local descriptor and inspect or migrate a shared local Git
alias.
4. Before this change, the source proof is rejected. The regression
fixture reproduces this failure.

**Paperclip version or commit**

Reproduced on master `1674902b4f`. The final branch is based on
`5a92d53f7d`.

## What Changed

- Accept only the matching active board-owned artifact failure with a
current task block, no live execution authority, an exact producer
fingerprint, and a declared prepared-source allocation.
- Include the task version and complete existing source/recovery
evidence in publication revalidation.
- Lock the proven recovery tasks in sorted company scope before
publication checks. A held descendant task produces the existing busy
error.
- Add a real PostgreSQL regression with 124 tool executions, a missing
descriptor, shared Git aliases, immutable download, and cold restore of
the local archive.
- Add stale, foreign, owned, controlled, malformed, and publication-race
rejection cases.
- Document that this archive preserves local bytes and the recovery
recipe. It does not prove remote export or release the remote hold.

## Verification

- Genuine baseline RED: the positive migration regression rejects the
source before the production fix.
- Focused treatment: 12/12 PostgreSQL cases pass.
- Review-race RED: both a selected source and a Git companion published
while a descendant recovery task was locked. The corrected tests reject
both cases.
- Two affected owner suites after the lock repair: 91/91 pass in 23.18
seconds.
- Strict server TypeScript check passes after the lock repair.
- Module boundaries and `git diff --check` pass.
- [Current-head
CI](https://github.com/paperclipai/paperclip/actions/runs/42367152081)
passes: 56 successful checks, four conditional skips, and one neutral
review check. No checks remain pending or failed.
- Greptile and Paperclip reviews are both 5/5 on `0a6e6c3ece`, with no
open findings.
- The first descendant fixture attempt hit the immutable task/privacy
guard before it reached the race. The corrected fixture creates that
task binding at admission and produces the two genuine REDs described
above.

## Risks

- A broad classification could let a migration move local authority
without a complete recovery recipe. The new branch requires the exact
failure, producer, task version, source allocation, and released
execution owners.
- Evidence can change during upload. Existing locks and full publication
revalidation reject those races.
- The remote allocation may contain changes absent from the local
source. This PR preserves only local bytes and the durable recovery
route. It does not mark remote work exported, finalized, or safe to
discard.
- No schema, provider lifecycle, admission, or polling behavior changes.

## Model Used

OpenAI `gpt-6.1-sol`, with reasoning, tool use, and local code
execution. 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>
2026-11-07 05:27:55 -06:00
DottaandPaperclip 384fa2b7be Allow context compaction while Agent Chat waits (#19801)
## Thinking Path

> - Paperclip manages agents and their work.
> - Agent Chat can wait for a user between useful turns.
> - The task Properties panel already lets that user request context
compaction.
> - The queue treated that maintenance request as an ordinary wake and
refused it while the chat waited.
> - Compaction must maintain that session without starting a new
conversation turn.
> - This change admits the authenticated request, preserves the waiting
state during execution, and prevents competing requests from saving
extra maintenance work.
> - Ordinary and unauthorized wakes remain blocked.

## Linked Issues or Issue Description

Refs #19378, which introduced the existing Compact now action. Refs
#19377, whose review identified this independent queue admission gap.
Searches for `context_compaction` with `waiting` and `Compact now` with
`waiting` found no existing fix for this admission bug.

**What happened?**

An owner clicks Compact now for a waiting Agent Chat conversation. The
real compaction service reaches the heartbeat queue, receives no run,
and reports that compaction did not start.

**Expected behavior**

The authenticated owner can request one separate compaction run while
the chat waits. The request must keep its existing actor, task, agent,
budget, and execution checks. Ordinary waiting-chat wakes must stay
blocked.

**Steps to reproduce**

1. Use an open Agent Chat conversation whose agent supports compaction.
2. Wait until the conversation is waiting for its owner.
3. Request Compact now from the task Properties panel.
4. Observe the refusal before this fix. The new database regression
calls the real compaction service and queue to reproduce it.

## What Changed

- Admit only the existing complete manual compaction envelope. Require a
user actor, no coalescing, the compaction intent, and matching nonempty
request IDs.
- Preserve the waiting conversation state, session generation, reply
boundary and unanswered questions in the existing conversation
preparation owner.
- Recheck active and deferred maintenance under the existing issue
admission lock. Competing clicks cannot save a second maintenance
request.
- Add execution coverage with and without a pending question. Add
concurrent admission and deferred-request controls, including admission
after the pending receipt is cancelled. Keep the existing authorization
and ordinary conversation controls.

## Verification

- The original service-to-queue request failed before the admission fix.
- The execution regression then caught the review finding: compaction
succeeded, but both waiting-state controls failed. The same complete
controls passed after the conversation preparation fix.
- A real concurrent request test recorded two durable maintenance
receipts before the atomic admission fix. It counts deferred authority
as well as admitted runs.
- Current integrated candidate: normal current Runner core and surface
TypeScript exports passed. All seven normal App typecheck programs
passed. The whole queue/execution suites passed 86/86, the whole
context-compaction suite passed 5/5, and 11 selected ordinary
conversation controls passed. There were 102 actual passes; the 29
unselected conversation cases are not counted as passes.
- Database controls use fresh dedicated PostgreSQL 17 databases, Node
24, one test worker and pools capped at four. Private fixture adapters
use the existing dedicated database and normal migrations. They keep
test bodies, assertions and time limits intact.
- Earlier fixture and setup failures remain recorded separately from
product failures. These include an omitted external URL, an invalid
closed-task fixture, a missing crypto import, an incorrect
result-envelope matcher, and an unavailable test-library helper. Each
was corrected at its owning boundary. A test-output text matcher also
missed Vitest's abbreviated failure; the raw assertion stack and failure
were retained.
- Local session tests exercise the actual admission, native Runner
lifecycle, accepted result and persisted conversation state through the
supported test backend. They do not prove live provider compaction, UI
rendering or deployed suspension.
- Exact-head remote CI passed: 59 successful checks, three intentional
skips and one neutral check. Greptile and Ponytail both reviewed this
head at 5/5 with no actionable findings. Ponytail explicitly marked it
Ship.
- Master advanced during review. A local merge evaluation preserves
every unrelated master change and all four PR files. The current App
programs and all three execution controls passed again on that projected
squash tree to check the workspace-finalization seam.

## Risks

The change affects admission and preparation for one maintenance
request. A broad exception could start ordinary work or reopen a waiting
chat. The request therefore keeps the complete existing manual envelope
and authorization checks. Conversation preparation still checks the
session generation. The existing issue lock owns duplicate admission.
There is no new timer, database migration or provider protocol.

## Model Used

OpenAI Codex based on GPT-6 managed the change and reviewed the shared
queue fix. OpenAI `gpt-6.1-sol` authored the focused regression
coverage. Both used reasoning, repository tools and local code
execution. The runtime did not expose the context-window size.

## 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 (the
existing Compact now documentation remains accurate; no new
configuration is added)
- [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.1107.0-canary.11
2026-11-07 05:11:44 -06:00
5a92d53f7d fix(adapter-utils): handle closed child stdin safely (#14057)
Handle closed child stdin without losing the child result.

Keep each child stdin error handler scoped to that child. Expected EPIPE
and stream-destroyed failures no longer crash the host. The child close
event remains the source of its exit result.

Verification: 2,024 tests passed and 12 live/platform cases skipped across
87 complete adapter-utils suites. The final 140-test server-utils suite
passed. The regression reproduces the EPIPE crash on unchanged master.

Final qualification: all applicable CI passed and exact-head Greptile Apex
scored 5/5 with no unresolved findings.

Co-Authored-By: Grzegorz Golaś <193330749+greegorij@users.noreply.github.com>
Co-Authored-By: Devin Foley <devinfoley@users.noreply.github.com>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-11-07 03:01:03 -08:00