docs(plans): record the Pi steer duplicate after a restart and its follow-up (#19982)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A person can steer a running Codex or Pi turn, and since #19970 a
queued follow-up steers automatically
> - Restart recovery (#19962) delivers a steered message that the model
did not read before the restart, exactly once for Codex
> - Pi gives the run log no read evidence for a steer, so after a
restart a Pi steer that the model already read can arrive again
> - The fix needs changes to the closure-pinned Pi distribution, which
waits for the next Pi re-qualification
> - This pull request records the limit and the follow-up plan so they
are not lost
> - The benefit is that the next Pi re-qualification can include the
definitive fix

## Linked Issues or Issue Description

**What is the documentation problem?**
The Pi steering limit after a restart, and its planned fix, were only in
pull request text and session notes.

**Where?**
`doc/plans/2026-11-07-pi-steer-restart-limit.md` (new).

**What should it say?**
Codex delivers an unread steered message exactly once after a restart.
Pi can duplicate a message it already read, and never loses one. The
note also gives the reasons, the follow-up plan (option A), its costs,
and the rejected options.

Refs #19962, Refs #19970

## What Changed

- New `doc/plans/2026-11-07-pi-steer-restart-limit.md`. It covers the
limit, why Pi has no read evidence, the follow-up (forward Pi's native
user `message_start` through pi-acp, acpx, the sidecar and Rust into a
`userMessage` item), its re-qualification costs, and the rejected
options.

## Verification

- Documentation only. Read the note. The facts match
`packages/paperclip-runner/src/steering-delivery.ts`,
`patches/pi-acp@0.0.33.patch`, `pi-closure-pins.ts` and
`rules/acpx-profile-identity.mdc`.

## Risks

- None. Documentation only.

## Model Used

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

## 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>
This commit is contained in:
DottaandClaude Opus 5.5 authored and GitHub committed 2026-11-07 23:51:57 -06:00
1 parent ccce55ab11
commit 46bf913fb9
1 file changed
+36
@@ -0,0 +1,36 @@
# Pi steering after a restart: known limit and follow-up
Status: accepted limit, 2026-11-07. Follow-up planned for the next Pi re-qualification.
Context: [#19962](https://github.com/paperclipai/paperclip/pull/19962) (restart recovery delivers unread steered messages) and [#19970](https://github.com/paperclipai/paperclip/pull/19970) (automatic steering).
## Summary
- **Codex: exactly once.** Restart recovery reads the run log. Each `userMessage` item after the turn's own input reads the oldest unread steer in that turn. If the read arrives before its acknowledgement, it waits for the next acknowledgement in that turn. The restart continuation delivers only the acknowledged messages that have not been read.
- **Pi: never lost, sometimes duplicated.** Pi gives the run log no read evidence for a steer. If Pi read a steer and a restart then interrupts the same turn, the restart continuation delivers that message again. The agent sees it twice. It is never lost.
- **This is rare and visible:** it needs a steer, a read, and a restart in the same turn, and the duplicate shows in the transcript.
## Why Pi has no read evidence
- Pi's native runtime emits a `message_start` with role `user` when it delivers a queued steer into the conversation.
- The pinned pi-acp wrapper (`patches/pi-acp@0.0.33.patch`) does not forward that event, and acpx has no parser for ACP `user_message_chunk` updates.
- Runner-side inference is unsafe. For example, "an assistant message started after the acknowledgement": Pi checks its steering queue before the model call, but the assistant `message_start` arrives only when the response starts streaming. A steer queued in that gap would count as read and be lost.
- A restored session's history does not help either. A resumed ACPX session loads on its first prompt, so Pi replays its saved history after the continuation prompt is already sent.
## Follow-up for the next Pi re-qualification (option A)
1. In pi-acp, forward Pi's native user `message_start` as an ACP update with Pi-native provenance, for the turn's own input and for each delivered steer.
2. Map it through acpx, the ACPX sidecar and the Rust ACPX backend to a `userMessage` item in the run log.
3. The existing rule in `packages/paperclip-runner/src/steering-delivery.ts` then covers Pi unchanged. The first `userMessage` of a turn is its own input; each later one reads a steer.
4. Add a red/green test with a Pi fixture: a steer read before the crash is not delivered again, and an unread one is delivered once.
Cost, which is why this waits for a re-qualification:
- The wrapper patch and `pi-acp-runtime.ts` are part of the Pi distribution closure. All three closure pins (`pi-closure-pins.ts`) must be regenerated on their target hosts.
- The Pi `commandDigest` changes, so saved Pi sessions start fresh.
- Pi paid qualification must run again.
- The new runtime event is a Runner/pack contract and changes the pack `contractDigest` (`rules/acpx-profile-identity.mdc`).
## Options considered and rejected
- **Dedupe against Pi's replayed history at restore (eager load).** It works only with an eager session load before the continuation, plus changes in five places: an acpx patch, a sidecar command, a Rust pass-through, a harness method, and the recovery filter. Not worth it for a rare duplicate.
- **Steer the unread messages into the continuation after the history replay.** This is racier, and it depends on when Pi takes a steer.