Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 25 additions & 9 deletions loopx/pi_goal_mode/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,31 @@ into a LoopX-governed visible goal loop.
and fences an in-flight quota probe. For persistent sessions, the loop stays
paused across Pi restarts until `/loopx resume` or a fresh goal activation
explicitly re-arms it.
- **Pause reasons** — every pause records why it happened in the binding:
`pauseReason: "user_prompt"` when a `before_agent_start` prompt was not the
loop's own injected task body, `"aborted"` after Escape, and `""` again once
`/loopx resume` re-arms the loop. Both pause paths report the pause in the UI
and the recorded reason is diagnostic only: it never decides continuation, and
the canonical `autoResume` flag stays the single authority. Prompt origin is not
inferred from another event: Pi compacts before it emits `before_agent_start`
for a submitted prompt, so a preceding `session_compact` does not prove that
the next prompt came from the host.
- **Input boundary** — observed Pi `input` events with source `interactive` or
`rpc` pause automatic continuation, including queued `steer` / `followUp`,
image-only input and text identical to the last injected task. Pi still
processes the input and can compact/recover within that run; LoopX stops
submitting subsequent tasks. Already-submitted messages and running tools
are not cancelled. Input arriving during a pending resume or activation
revokes dispatch immediately, then waits for already-started re-arm writes
before saving the pause, so restarting Pi cannot restore that older re-arm.
Use `/loopx resume` to re-arm with fresh quota admission.
- **Pause reasons** — a saved input pause records `pauseReason: "user_prompt"`,
Escape records `"aborted"`, and explicit resume clears the reason. Input and
run-start events for the same pause notify once and preserve an existing pause
reason. The reason is diagnostic; durable `autoResume` and LoopX quota still
authorize continuation. While saving an input pause, a session-local fence
also denies dispatch. If saving fails, that instance stays stopped and reports
that the pause may not survive restart; restore storage before explicit resume.
- **Origin limits** — `extension` does not identify which extension sent input.
Unknown-source user starts retain the injected-text fallback; custom starts
pause because LoopX only injects user messages. The fallback compares the
injected text before waiting for a pending re-arm and persists a non-own
start's pause after older writes; a later explicit resume still takes priority.
Earlier Pi input handlers can consume an event before this extension sees it.
This is not complete prompt
provenance: compaction never exempts a prompt or re-arms a pause, and its
occurrence does not establish the next prompt's producer.

## Install / uninstall

Expand Down
22 changes: 19 additions & 3 deletions loopx/pi_goal_mode/loopx-goal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -614,14 +614,30 @@ export default function (pi: ExtensionAPI) {
await loop.interrupt(key);
});

// Every prompt that is not this loop's own injected task body is the owner
// taking the wheel and pauses the loop; the pause itself carries the reason.
// Queued steer/followUp input does not start a new run. Observe its explicit
// host source here, even when its text equals our task body or is image-only.
// Return continue so Pi still processes the owner's message normally.
pi.on("input", async (event, ctx) => {
if (event.source === "interactive" || event.source === "rpc") {
const { key } = bindContext(ctx);
await loop.ownerInput(key);
}
return { action: "continue" };
});

// Starts without observed external input retain the conservative fallback.
// "extension" is not proof that a message came from this LoopX instance.
// Pi compacts before it emits before_agent_start for a submitted prompt, so a
// preceding compaction says nothing about who submitted that prompt and is not
// used to exempt one.
pi.on("before_agent_start", async (event, ctx) => {
const { key } = bindContext(ctx);
await loop.userPrompt(key, String(event.prompt || ""));
if (event.messageType === "custom") {
// LoopX only injects user messages; a custom start has no prompt string.
await loop.ownerInput(key);
} else {
await loop.userPrompt(key, String(event.prompt || ""));
}
});

// Session shutdown / session replacement: atomically dispose the whole
Expand Down
Loading
Loading