Skip to content

Commit 5230c55

Browse files
authored
Merge pull request #45 from narumiruna/fix/pi-goal-interruption-continuation
fix(pi-goal): pause continuations after interruptions
2 parents 1005190 + 98075e1 commit 5230c55

3 files changed

Lines changed: 435 additions & 31 deletions

File tree

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
# pi-goal interruption research
2+
3+
This note investigates why `@narumitw/pi-goal` can lose momentum or continue at the
4+
wrong time, and compares it with the native goal runtime in `third_party/codex`.
5+
6+
## Scope
7+
8+
Reviewed active code and docs only. Deprecated extensions are out of scope.
9+
10+
Primary sources:
11+
12+
- `extensions/pi-goal/src/goal.ts`
13+
- Pi extension docs for `before_agent_start`, `agent_end`, `sendUserMessage`,
14+
`appendEntry`, `ctx.isIdle()`, and `ctx.hasPendingMessages()`
15+
- `third_party/codex/codex-rs/core/src/goals.rs`
16+
- `third_party/codex/codex-rs/core/src/context/goal_context.rs`
17+
- `third_party/codex/codex-rs/core/src/context/turn_aborted.rs`
18+
- `third_party/codex/codex-rs/core/templates/goals/*.md`
19+
- `third_party/codex/codex-rs/tui/src/bottom_pane/pending_input_preview.rs`
20+
- `third_party/codex/codex-rs/tui/src/chatwidget/input_queue.rs`
21+
22+
## Current `pi-goal` behavior
23+
24+
`pi-goal` is implemented entirely as a Pi extension:
25+
26+
1. `/goal <objective>` creates an in-memory `activeGoal`, persists it as a
27+
custom session entry, updates the statusline, and calls `pi.sendUserMessage()`
28+
with a kickoff prompt.
29+
2. `before_agent_start` appends active-goal rules to the system prompt.
30+
3. `goal_complete` marks the goal complete, clears persisted state, and returns
31+
`terminate: true`.
32+
4. `agent_end` increments the goal iteration, updates usage, checks token
33+
budget, and sends a continuation prompt if the same goal id is still active.
34+
5. Continuation delivery is delegated to Pi messaging:
35+
- if `ctx.isIdle()` is true, call `pi.sendUserMessage(prompt)`;
36+
- otherwise call `pi.sendUserMessage(prompt, { deliverAs: "followUp" })`.
37+
38+
This is small and idiomatic for an extension, but the continuation policy is not
39+
owned by the agent runtime. That is the main source of fragility.
40+
41+
## Where interruptions can break `pi-goal`
42+
43+
### 1. User interrupt is not goal-aware
44+
45+
`pi-goal` has no explicit `turn_aborted` or `interrupt` event. If Pi surfaces an
46+
aborted assistant message through normal turn/agent events, `agent_end` still has
47+
enough information to send another continuation unless `pi-goal` separately
48+
checks for aborted/error stop reasons.
49+
50+
Codex treats `TurnAbortReason::Interrupted` as a first-class runtime event. The
51+
goal runtime accounts progress, clears the continuation turn id, and pauses the
52+
active goal. That makes Esc/user interrupt mean "stop this goal loop for now",
53+
not "immediately schedule another continuation".
54+
55+
### 2. Continuation is scheduled from `agent_end`, not from a runtime idle gate
56+
57+
`pi-goal` schedules the next turn from an extension `agent_end` handler. That
58+
handler is downstream of the agent loop and does not own the runtime's active
59+
turn reservation. Depending on exact event ordering, queued follow-ups,
60+
compaction, retry, or other extension work can race with the continuation.
61+
62+
Codex separates "turn finished" from "maybe continue if idle". After a turn is
63+
fully cleared, it calls a runtime-owned `MaybeContinueIfIdle` path. That path
64+
first starts any pending non-goal work, then tries goal continuation only if the
65+
session is still idle.
66+
67+
### 3. Pending user input is not prioritized
68+
69+
`pi-goal` does not check `ctx.hasPendingMessages()` before scheduling an
70+
automatic continuation. A user follow-up, steering message, or another extension
71+
message can be queued at the same boundary as the goal continuation.
72+
73+
Codex explicitly skips active-goal continuation when queued response items or
74+
trigger-turn mailbox items are pending. User or inter-agent work wins over the
75+
automatic goal loop.
76+
77+
### 4. No continuation lock or reserved continuation turn id
78+
79+
`pi-goal` guards against stale goals by comparing the goal id before sending, but
80+
it does not keep a continuation lock or a reserved continuation turn id. Multiple
81+
`agent_end`-like boundaries, reload/resume timing, or queued messages can still
82+
produce duplicate or stale continuation pressure.
83+
84+
Codex has both a `continuation_lock` and a `continuation_turn_id`. It reserves an
85+
active turn before injecting continuation input, re-reads the goal from state,
86+
and clears the reservation if the goal changed or another turn appeared.
87+
88+
### 5. The continuation prompt is a visible user message
89+
90+
`pi-goal` continuation uses normal `sendUserMessage()`. This makes continuation
91+
look like user input and subjects it to the same queue semantics as real user
92+
messages.
93+
94+
Codex injects hidden user-context fragments wrapped in `<goal_context>`. The
95+
model sees runtime-owned steering, but the UI and input queue can still treat
96+
real user input separately.
97+
98+
### 6. Goal objective text is not escaped inside prompt delimiters
99+
100+
Codex escapes objective text before placing it inside XML-like tags in goal
101+
prompts. `pi-goal` currently interpolates the objective directly into plain text
102+
prompts. That is simpler, but a goal containing delimiter-like or instruction-like
103+
text can make the continuation prompt less robust.
104+
105+
### 7. Budget handling happens only at turn end
106+
107+
`pi-goal` checks token budget in `agent_end`. Long turns with many tools can
108+
exceed the budget substantially before the extension can react.
109+
110+
Codex accounts goal usage after tool completion and at turn finish. When the
111+
budget is reached, it can inject a budget-limit steering item during the current
112+
turn and mark the goal `budget_limited` once.
113+
114+
## Codex design points worth copying
115+
116+
### Runtime event dispatcher
117+
118+
Codex centralizes lifecycle policy behind `GoalRuntimeEvent`:
119+
120+
- `TurnStarted`
121+
- `ToolCompleted`
122+
- `ToolCompletedGoal`
123+
- `TurnFinished`
124+
- `MaybeContinueIfIdle`
125+
- `TaskAborted`
126+
- `ExternalMutationStarting`
127+
- `ExternalSet`
128+
- `ExternalClear`
129+
- `ThreadResumed`
130+
131+
This keeps accounting, continuation, abort, resume, and external goal mutation
132+
rules in one place.
133+
134+
### Interrupt means pause
135+
136+
On user interrupt, Codex accounts current progress and pauses the active goal.
137+
It also records model-visible interrupted-turn context so future turns do not
138+
assume commands or tools cleanly completed.
139+
140+
### Continuation is idle-only and lock-protected
141+
142+
Codex continuation is not just "send a follow-up". It:
143+
144+
1. checks that goals are enabled and the current mode allows goals;
145+
2. checks no active turn exists;
146+
3. checks no queued user or trigger-turn mailbox input exists;
147+
4. reads the current persisted goal and requires `status == active`;
148+
5. acquires a continuation lock;
149+
6. reserves an active turn;
150+
7. re-reads the goal before launch;
151+
8. injects hidden goal context;
152+
9. marks the generated turn id as the continuation turn.
153+
154+
### Hidden goal context
155+
156+
Codex continuation, budget-limit, and objective-update prompts are runtime-owned
157+
hidden context fragments, not ordinary user messages. This preserves the user
158+
queue as user intent while still giving the model persistent goal guidance.
159+
160+
### Completion audit prompt
161+
162+
The Codex continuation template strongly warns the model not to redefine the
163+
goal into a smaller task, and requires evidence-based completion before calling
164+
`update_goal` with status `complete`. `pi-goal` has similar rules, but Codex's
165+
template is more explicit about requirement-by-requirement verification and
166+
using the current worktree/external state as authoritative.
167+
168+
## Recommendations
169+
170+
### Extension-only improvements
171+
172+
These can be implemented inside `pi-goal` without Pi core changes:
173+
174+
1. **Detect aborted/error agent endings.** If the final assistant message has
175+
`stopReason: "aborted"`, pause the goal and do not auto-continue. If it has
176+
`stopReason: "error"`, consider pausing or requiring explicit `/goal resume`.
177+
2. **Respect pending user work.** Before sending a continuation, check
178+
`ctx.hasPendingMessages()` and skip or delay goal continuation when user or
179+
extension messages are already queued.
180+
3. **Add a continuation-pending guard.** Track goal id + iteration for a queued
181+
continuation so repeated end events cannot enqueue duplicates.
182+
4. **Escape objective text in XML-like prompts.** If prompts use delimiters,
183+
escape `&`, `<`, and `>` like Codex does.
184+
5. **Strengthen continuation prompts.** Borrow Codex's "current state is
185+
authoritative" and "completion audit" language.
186+
6. **Document interruption semantics.** Make `/goal pause` and user interrupt
187+
behavior explicit in the README once implemented.
188+
189+
### Pi core improvements needed for Codex parity
190+
191+
Some Codex behavior cannot be faithfully reproduced by an extension alone:
192+
193+
1. **Abort lifecycle event.** Extensions need a first-class event carrying abort
194+
reason so goal-like extensions can distinguish user interrupt from normal
195+
completion.
196+
2. **Runtime-owned continuation scheduling.** Pi needs an idle-only scheduling
197+
API with pending-input checks, a lock, and a turn reservation, rather than
198+
requiring extensions to call `sendUserMessage()` from `agent_end`.
199+
3. **Hidden contextual input.** A hidden context-message API would let runtime
200+
steering reach the model without appearing as user-submitted text or fighting
201+
the normal user queue.
202+
4. **Pending-input categories.** Codex's UI distinguishes pending steers,
203+
rejected steers, and queued follow-ups. Pi goal continuation would be safer if
204+
automatic continuation could be lower priority than all user-visible queues.
205+
5. **Tool-boundary accounting hooks.** Budget-aware goals need accounting after
206+
tool completion, not only at agent end.
207+
208+
## Suggested next step
209+
210+
Implement the extension-only safety fixes first, especially abort/error detection
211+
and pending-message checks. That should reduce the most visible "goal keeps going
212+
after I interrupted it" and "goal races my next message" failures.
213+
214+
If goal mode is expected to match Codex long-term, move the continuation policy
215+
into Pi core or add core APIs that let `pi-goal` request a locked, idle-only,
216+
hidden-context continuation.

extensions/pi-goal/README.md

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
`@narumitw/pi-goal` is a native [Pi coding agent](https://pi.dev) extension that adds session-scoped `/goal` commands and a `goal_complete` tool for autonomous, verifiable task completion.
66

7-
Goal mode uses Codex-like persistence instructions and keeps sending guarded continuation messages until the agent calls `goal_complete`, the user pauses or clears the goal, or an optional token budget is reached.
7+
Goal mode uses Codex-like persistence instructions and keeps sending guarded continuation messages until the agent calls `goal_complete`, the user pauses or clears the goal, an interrupt/error pauses the goal, or an optional token budget is reached.
88

99
## ✨ Features
1010

@@ -16,9 +16,10 @@ Goal mode uses Codex-like persistence instructions and keeps sending guarded con
1616
- Tracks `active`, `paused`, `budget_limited`, and `complete` states.
1717
- Stores goal state in the current Pi session, following Codex's thread-owned goal model instead of using a global per-directory goal.
1818
- Registers a `goal_complete` tool for explicit completion.
19-
- Automatically prompts the agent to continue if an active turn ends early, directly triggering the next turn when Pi is idle.
20-
- Guards auto-follow-ups so replaced, paused, cleared, completed, or budget-limited goals are not continued.
21-
- Encourages verification before the goal is marked complete.
19+
- Automatically prompts the agent to continue if an active turn ends early, directly triggering the next turn when Pi is idle and no pending messages are queued.
20+
- Pauses instead of auto-continuing when Pi reports an aborted or errored assistant turn.
21+
- Guards auto-follow-ups so duplicate, replaced, paused, cleared, completed, or budget-limited goals are not continued.
22+
- Encourages requirement-by-requirement verification before the goal is marked complete.
2223

2324
## 📦 Install
2425

@@ -55,7 +56,7 @@ pi -e ./extensions/pi-goal
5556
- `/goal --tokens 100k <goal_to_complete>` starts or replaces goal mode with a token budget. `k` and `m` suffixes are accepted, for example `100k` or `1.5m`.
5657
- `/goal edit <goal_to_complete>` updates the existing goal objective without resetting usage counters. Active goals stay active, paused goals stay paused, and budget-limited goals remain budget-limited if their budget is still exhausted.
5758
- `/goal pause` stops prompt injection and auto-continuation without forgetting the goal.
58-
- `/goal resume` resumes a paused or budget-limited goal when the token budget allows it.
59+
- `/goal resume` resumes a paused or budget-limited goal when the token budget allows it, then queues a resume prompt so work continues.
5960
- `/goal clear` cancels the current goal and also removes any legacy persisted state for the current working directory.
6061

6162
Goal objectives are limited to 4,000 characters. Put longer instructions in a file and reference the file path from `/goal`.
@@ -78,9 +79,17 @@ Older versions wrote unfinished goals to `~/.pi/agent/pi-goal-state.json` keyed
7879

7980
## ✅ How completion works
8081

81-
The extension registers a `goal_complete` tool. While a goal is active, the system prompt uses Codex-like persistence rules: keep going until the goal is resolved end-to-end, do not stop at analysis, a plan, partial fixes, or suggested next steps, use available tools for implementation and verification, and call `goal_complete` only when the goal is fully done.
82+
The extension registers a `goal_complete` tool. While a goal is active, the system prompt uses Codex-like persistence rules: keep going until the goal is resolved end-to-end, treat current files, command output, tests, and external state as authoritative, avoid redefining the goal into a smaller task, and call `goal_complete` only after requirement-by-requirement verification.
8283

83-
If an agent turn ends before `goal_complete` is called, the extension records elapsed time and token usage, checks the budget, verifies that the same goal id is still active, then sends a continuation prompt for the same goal. When Pi is already idle, this directly triggers the next turn; otherwise it is queued as a follow-up until the agent finishes current work.
84+
If an agent turn ends before `goal_complete` is called, the extension records elapsed time and token usage, checks the budget, verifies that the same goal id is still active, and sends a continuation prompt only when no user or extension messages are already pending. When Pi is already idle, this directly triggers the next turn; otherwise it is queued as a follow-up until the agent finishes current work. A continuation-pending guard prevents repeated end events from enqueueing duplicate continuations.
85+
86+
## 🛑 Interruption and queued-input behavior
87+
88+
When Pi reports the final assistant message with `stopReason: "aborted"` or `stopReason: "error"`, `pi-goal` records usage, changes the goal to `paused`, and does not auto-continue. Run `/goal resume` to continue after reviewing the interruption or error.
89+
90+
If user or extension messages are already queued at the end of a goal turn, those messages take priority and `pi-goal` skips that automatic continuation. Active goal instructions are still injected into the next agent turn, and normal continuation can resume after pending work finishes.
91+
92+
Queued automatic continuation prompts are tracked by goal id and iteration. If a queued continuation is invalidated by `/goal pause`, `/goal clear`, `/goal edit`, replacement, or completion before delivery, the extension handles that stale prompt instead of letting it restart old goal work.
8493

8594
## 🧠 Use cases
8695

0 commit comments

Comments
 (0)