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
4 changes: 2 additions & 2 deletions paseo-omp/docs/core-provider-issue-audit.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Core OMP provider issue audit

This audit compares reports in `getpaseo/paseo` with the community `omp-plugin` provider. It was refreshed on 2026-09-18 from GitHub issue titles and bodies containing `OMP`, `oh-my-pi`, or `rpc-ui`, OMP-related pull requests, and materially equivalent Pi/RPC reports. GitHub Discussions were also inspected; the repository had no OMP-related discussion.
This audit compares reports in `getpaseo/paseo` with the community `omp-plugin` provider. It was refreshed on 2026-09-23 from GitHub issue titles and bodies containing `OMP`, `oh-my-pi`, or `rpc-ui`, OMP-related pull requests, and materially equivalent Pi/RPC reports. GitHub Discussions were also inspected; detached-child Stop semantics are tracked in [Discussion #5256](https://github.com/getpaseo/paseo/discussions/5256).

Issue and pull-request pairs are consolidated by root cause. A closed upstream issue does not prove the plugin implements the behavior, and an open upstream issue does not imply the plugin is affected.

Expand All @@ -22,7 +22,7 @@ Status meanings:
| [#3654](https://github.com/getpaseo/paseo/issues/3654), [#3998](https://github.com/getpaseo/paseo/issues/3998), [PR #3667](https://github.com/getpaseo/paseo/pull/3667) | **Verified** | Post-`agent_end` state reconciliation is bounded. Stale or unavailable `get_state` cannot leave a turn running forever. |
| Upstream OMP [PR #12331](https://github.com/can1357/oh-my-pi/pull/12331) and released OMP 18.2.x unkeyed `agent_end` frames | **Mitigated** | Request-keyed terminals use exact matching. For released binaries that omit the key, the plugin accepts only ordered current-prompt evidence: a fresh branch-correlated user entry followed by assistant activity, confirmed native idle/non-compacting state, and no active permission, tool, steer, or child work. Active or pre-evidence stale candidates are ignored. Confirmed-idle ambiguity fails only the Paseo turn, not the OMP runtime. The remaining same-agent stale-event misattribution risk is explicit and bounded; waiting for the upstream protocol fix would make every later turn unusable on published releases. Client reconnects, including mobile reconnect grace, are host transport behavior and do not own the daemon-managed provider session. |
| [#3999](https://github.com/getpaseo/paseo/issues/3999), [#4000](https://github.com/getpaseo/paseo/issues/4000), [#4039](https://github.com/getpaseo/paseo/issues/4039), [PR #3772](https://github.com/getpaseo/paseo/pull/3772), [PR #4217](https://github.com/getpaseo/paseo/pull/4217) | **Verified** | `prompt.steer` is negotiated and implemented with expected-turn checks, acknowledgement ordering, duplicate correlation, and interrupt/terminal race coverage. |
| Shared RPC cancellation [#3540](https://github.com/getpaseo/paseo/issues/3540), Pi [#3749](https://github.com/getpaseo/paseo/issues/3749) | **Verified in provider** | Native abort, exactly-one terminal event, permission cleanup, descendant cleanup, and uncertain-cleanup quarantine are tested. UI keyboard delivery remains host-owned. |
| Shared RPC cancellation [#3540](https://github.com/getpaseo/paseo/issues/3540), Pi [#3749](https://github.com/getpaseo/paseo/issues/3749), OMP [#4921](https://github.com/getpaseo/paseo/pull/4921), [#5101](https://github.com/getpaseo/paseo/issues/5101), [PR #5243](https://github.com/getpaseo/paseo/pull/5243) (`90737e1`) | **Verified** | The plugin independently maps OMP `aborted`, `canceled`, and `cancelled` stop reasons to canceled in `session-terminal.ts`; focused provider regressions prove an intentional native abort with error text emits exactly one canceled turn, a genuine native error remains failed, and a later turn completes. Core now maps `aborted` to canceled and fences stale provider-idle completion. Parent cancellation still terminalizes all subsessions in this plugin. Whether foreground Stop preserves detached children is an unresolved product decision, not a plugin gap to patch: preserving only the child descriptor is unsafe while shared host-tool cleanup can abort its in-flight work. [Discussion #5256](https://github.com/getpaseo/paseo/discussions/5256) requests the contract. |
| [#3218](https://github.com/getpaseo/paseo/issues/3218) | **Verified at provider boundary** | Session close owns and awaits OMP process cleanup. Whether every archive UI route invokes provider close is a host concern. |

## Persistence, import, and subagents
Expand Down
79 changes: 72 additions & 7 deletions paseo-omp/tests/provider-interrupt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { OmpCleanupFailure } from "../server/provider/security";
import {
createHarness,
EventLog,
establishTerminalOwnership,
FakeOmpRuntime,
finishTurn,
MODEL_PUBLIC_ID,
Expand All @@ -17,11 +18,10 @@ import {
} from "./helpers/provider-harness";

describe("OMP direct provider", () => {
test("interrupts and emits one terminal turn", async () => {
test("maps an intentional native abort to one cancellation and keeps the next turn healthy", async () => {
const { connection, events, runtime } = await createHarness();
await openSession(connection, events);
const promptResult = await startPrompt(connection, events);
const turnId = turnIdFrom(promptResult);
const firstTurnId = turnIdFrom(await startPrompt(connection, events));
const session = sessionAt(runtime);

await connection.send({
Expand All @@ -32,17 +32,82 @@ describe("OMP direct provider", () => {
await events.waitFor(
(event) => event.type === "request.completed" && event.requestId === "interrupt-1",
);
const terminal = await finishTurn(events, session, turnId);
session.emit({ type: "agent_end", messages: [], isTerminal: true });
establishTerminalOwnership(session);
session.emit({
type: "agent_end",
requestId: "rpc-prompt-1",
messages: [
{
role: "assistant",
content: "Interrupted by user",
stopReason: "aborted",
errorMessage: "Interrupted by user",
},
],
isTerminal: true,
});
const terminal = await events.waitFor(
(event) =>
event.type === "session.turn" && event.turnId === firstTurnId && event.state !== "started",
);

expect(session.aborts).toBe(1);
expect(terminal).toEqual(expect.objectContaining({ state: "canceled" }));
expect(
events.filter(
(event) =>
event.type === "session.turn" && event.turnId === turnId && event.state !== "started",
event.type === "session.turn" &&
event.turnId === firstTurnId &&
event.state !== "started",
),
).toHaveLength(1);

const laterTurnId = turnIdFrom(
await startPrompt(connection, events, "after-interrupt", "continue"),
);
await expect(finishTurn(events, session, laterTurnId)).resolves.toEqual(
expect.objectContaining({ state: "completed" }),
);
await connection.close();
});
test("keeps a genuine native failure failed and allows a later turn", async () => {
const { connection, events, runtime } = await createHarness();
await openSession(connection, events);
const failedTurnId = turnIdFrom(
await startPrompt(connection, events, "native-failure", "work"),
);
const session = sessionAt(runtime);

establishTerminalOwnership(session);
session.emit({
type: "agent_end",
requestId: "rpc-prompt-1",
messages: [
{
role: "assistant",
content: "rate limited",
stopReason: "error",
errorMessage: "rate limited",
},
],
isTerminal: true,
});
await expect(
events.waitFor(
(event) =>
event.type === "session.turn" &&
event.turnId === failedTurnId &&
event.state !== "started",
),
).resolves.toEqual(
expect.objectContaining({ state: "failed", error: { message: "OMP assistant turn failed" } }),
);

const laterTurnId = turnIdFrom(
await startPrompt(connection, events, "after-native-failure", "continue"),
);
await expect(finishTurn(events, session, laterTurnId)).resolves.toEqual(
expect.objectContaining({ state: "completed" }),
);
await connection.close();
});
test("does not start a later turn while an earlier abort is unsettled", async () => {
Expand Down
Loading