@omercnet/paseo-omp 0.2.1-next.88.1 → 0.2.1-next.90.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -5
- package/SUPPORT.md +5 -3
- package/TESTING.md +6 -6
- package/client/external-url.ts +15 -0
- package/client/mcp-authorization.tsx +3 -2
- package/client/omp-config-surface.tsx +3 -2
- package/client/paseo-types.ts +9 -0
- package/client/provider-diagnostics-state.ts +1 -2
- package/docs/alpha-release-checklist.md +1 -1
- package/docs/core-provider-issue-audit.md +2 -1
- package/docs/installation.md +31 -9
- package/index.client.tsx +1 -1
- package/package.json +5 -6
- package/paseo-plugin.json +1 -1
- package/scripts/prepare-dependencies.mjs +8 -4
- package/server/provider/session.ts +279 -169
- package/shared/external-url.ts +21 -0
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Community OMP integration for Paseo. The plugin registers the distinct `omp-plug
|
|
|
6
6
|
|
|
7
7
|
## Quick start
|
|
8
8
|
|
|
9
|
-
Requirements: Paseo
|
|
9
|
+
Requirements: Paseo `>=0.8.0 <0.10.0`, OMP `18.1.15` or newer, and OMP RPC protocol v2. Paseo `0.9.0-beta.1` is recommended for npm-managed installation and the complete client integration.
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
paseo plugin install npm:@omercnet/paseo-omp@<version>
|
|
@@ -22,7 +22,7 @@ Open the **OMP** sidebar to review the provider-profile contract, choose which c
|
|
|
22
22
|
- [Core-provider issue and parity audit](docs/core-provider-issue-audit.md)
|
|
23
23
|
- [Alpha release checklist](docs/alpha-release-checklist.md)
|
|
24
24
|
|
|
25
|
-
The plugin uses
|
|
25
|
+
The plugin uses public Paseo 0.8 and 0.9 beta provider contracts and registers distinct `omp-plugin` and `omp-plugin-<profile>` identities; it does not modify the bundled `omp` provider.
|
|
26
26
|
|
|
27
27
|
## Named OMP profiles
|
|
28
28
|
|
|
@@ -36,7 +36,7 @@ Profile selection is request-local: concurrent clients cannot change each other'
|
|
|
36
36
|
|
|
37
37
|
## Paseo provider SDK coverage
|
|
38
38
|
|
|
39
|
-
This matrix inventories the complete capability set exported by the pinned `@getpaseo/plugin` 0.
|
|
39
|
+
This matrix inventories the complete capability set exported by the pinned `@getpaseo/plugin` 0.9.0-beta.1 provider SDK. It measures strict SDK surface coverage, not general product quality.
|
|
40
40
|
|
|
41
41
|
**Current capability completeness: 61.8%.** The provider advertises 11 of 17 capabilities (64.7%); ten are complete and `session.configure` is partial.
|
|
42
42
|
|
|
@@ -49,7 +49,7 @@ Scoring is deliberately mechanical so releases remain comparable:
|
|
|
49
49
|
|
|
50
50
|
| SDK capability | Completeness | Support and gap |
|
|
51
51
|
| --- | ---: | --- |
|
|
52
|
-
| `prompt.message` | **100%** | Text, structured attachments, optimistic correlation, streaming, and terminal outcomes map to native OMP prompts. |
|
|
52
|
+
| `prompt.message` | **100%** | Text, structured attachments, optimistic correlation, streaming, and terminal outcomes map to native OMP prompts. Matching terminal request IDs remain authoritative; released OMP builds without them use the ordered compatibility policy described below. |
|
|
53
53
|
| `prompt.command` | **100%** | Native command discovery plus structured `compact`, `autocompact`, `handoff`, `steer`, and `follow-up` dispatch. Individual commands can still fail when their documented OMP prerequisites are absent. |
|
|
54
54
|
| `prompt.image` | **100%** | Native image blocks for capable models; private bounded temporary files with cleanup for text-only models. |
|
|
55
55
|
| `prompt.output_schema` | **0%** | Not advertised. OMP has no equivalent structured-output contract; `outputSchema` fails visibly. |
|
|
@@ -97,7 +97,9 @@ The plugin always remains a separate provider. It registers `omp-plugin` and `om
|
|
|
97
97
|
|
|
98
98
|
Existing agents whose provider is `omp` remain owned by the bundled provider. New plugin agents persist under their selected `omp-plugin` or `omp-plugin-<profile>` identity with the plugin's versioned opaque handle. The plugin can list and import OMP-native sessions through its own provider flow, but it performs no implicit conversion of bundled-provider records.
|
|
99
99
|
|
|
100
|
-
OMP `18.1.15` is the oldest version tested end to end. The direct provider's hard compatibility gate is `rpc-ui` protocol v2: metadata-free legacy ready frames and v1-only runtimes are rejected before a provider session opens because they cannot support the advertised persistence and conversation-rewind capabilities. Typed tool approvals remain capability-gated and fall back as described above.
|
|
100
|
+
OMP `18.1.15` is the oldest version tested end to end. The direct provider's hard compatibility gate is `rpc-ui` protocol v2: metadata-free legacy ready frames and v1-only runtimes are rejected before a provider session opens because they cannot support the advertised persistence and conversation-rewind capabilities. Typed tool approvals remain capability-gated and fall back as described above. For terminal completion, a matching `agent_end.requestId` is authoritative and mismatches are ignored before state changes. Released OMP 18.2.x builds that omit that field use an ordered fallback requiring a fresh correlated native user entry followed by current-turn assistant activity, an idle non-compacting runtime, and no conflicting permission, tool, steer, or child-session work. Ambiguous events while OMP is active are ignored; unresolved ambiguity after confirmed idle fails only the Paseo turn. This trades a bounded residual same-agent stale-event risk for compatibility with released OMP instead of terminating and lazily restarting the runtime after every later prompt. Paseo clients, including mobile clients inside reconnect grace, do not own or terminate the daemon-managed OMP session.
|
|
101
|
+
|
|
102
|
+
Paseo 0.9.0-beta.1 is the canary target and provides npm-managed plugin sources, platform-owned external URL opening, and whole-item timeline transforms before Overview grouping. Paseo 0.8.x remains supported for provider operation and Git or directory installation; its clients do not provide the external URL opener and retain their older timeline presentation semantics.
|
|
101
103
|
|
|
102
104
|
Install, update, disable, or remove `paseo-omp` independently of the bundled provider. Verify the provider snapshot contains `omp-plugin` after installation; a bundled `omp` entry may remain present and is not modified by this plugin.
|
|
103
105
|
|
package/SUPPORT.md
CHANGED
|
@@ -16,7 +16,7 @@ Do not put credentials, private repository paths, session transcripts, or unreda
|
|
|
16
16
|
|
|
17
17
|
## Supported versions
|
|
18
18
|
|
|
19
|
-
- Paseo:
|
|
19
|
+
- Paseo: `>=0.8.0 <0.10.0` on both the daemon and every app loading the client entry. The pinned SDK and controlled canary use `0.9.0-beta.1`; 0.8.x remains the compatibility line.
|
|
20
20
|
- OMP: `18.1.15` is the oldest release in the required real-binary regression job. The hard runtime contract is `rpc-ui` protocol v2, not the version string alone.
|
|
21
21
|
- Plugin release channel: alpha. Backward compatibility is best effort until stable `0.1.0`; every known migration requirement must be stated in the release notes.
|
|
22
22
|
- Typed approvals: optional. When both peers negotiate `typedToolApprovals: 1`, the plugin uses typed tool permissions. Otherwise it retains the bounded generic extension-question flow.
|
|
@@ -26,15 +26,17 @@ Do not put credentials, private repository paths, session transcripts, or unreda
|
|
|
26
26
|
- `omp` and `omp-plugin` are independent provider identities. Agents, provider settings, and persisted handles do not migrate automatically between them.
|
|
27
27
|
- Do not open the same underlying OMP session concurrently through both providers. Reservation tracking is provider-local and cannot coordinate ownership with Paseo's bundled adapter.
|
|
28
28
|
- OMP 18.1.15 does not advertise typed tool approvals. The plugin uses its bounded generic permission fallback until both peers negotiate `typedToolApprovals: 1`.
|
|
29
|
-
- OMP releases
|
|
30
|
-
- Timeline correlation rebuilds an invalid watermark from a complete pre-prompt `get_branch_messages` snapshot, not replayed model context or an evicting identity cache. Snapshots are limited to 1,024 entries and 4 MiB; duplicate IDs, surplus exact-text matches, unavailable history, and exceeded bounds leave users uncorrelated rather than claiming an old entry. Repeated accepted prompts within a turn consume matching branch occurrences in order.
|
|
29
|
+
- OMP releases through 18.2.x may omit the originating prompt `requestId` from `agent_end`. A matching ID is authoritative when present, and a mismatch is discarded before changing turn state. For an unkeyed later-turn terminal, the plugin uses a bounded ordered fallback: the accepted prompt must not be known local-only, a fresh native user entry must correlate to it, current-turn assistant activity must follow that entry, OMP must report idle and non-compacting, and no permission, tool, steer, or child-session work may remain. Ambiguous candidates are ignored while OMP is active; once OMP is confirmed idle, unresolved ambiguity fails only the Paseo turn and keeps the OMP process available. This deliberately accepts a residual same-agent risk: a sufficiently delayed unkeyed event with indistinguishable ordered evidence can still be misattributed. It is safer than accepting any idle `agent_end`, while remaining usable with released OMP builds that cannot provide request identity.
|
|
30
|
+
- Timeline correlation rebuilds an invalid watermark from a complete pre-prompt `get_branch_messages` snapshot, not replayed model context or an evicting identity cache. Snapshots are limited to 1,024 entries and 4 MiB; duplicate IDs, surplus exact-text matches, unavailable history, and exceeded bounds leave users uncorrelated rather than claiming an old entry. Repeated accepted prompts within a turn consume matching branch occurrences in order.
|
|
31
31
|
- Configured MCP servers are supported and bridged into OMP. Paseo's own orchestration tools appear under their native names when the daemon's **Enable Paseo tools** / `daemon.mcp.injectIntoAgents` setting is enabled; other MCP servers remain namespaced. Exact Paseo `toolPolicy` preapproval cannot be represented by OMP `set_host_tools` and therefore fails session startup closed. `disallowedTools` applies only to recognized native OMP built-ins; unknown names are rejected and MCP tools are not silently filtered through it.
|
|
32
32
|
- `qwen2.5:0.5b` is provided only for free exploratory inference. It may ignore exact-output instructions and is not a deterministic protocol or tool-use oracle; use `canary-mock/Deterministic Canary` for assertions.
|
|
33
33
|
- The deterministic mock does not implement OMP's compaction-summary contract, so `/compact` reports `OMP compaction failed` in the canary; compaction remains covered by protocol fixtures.
|
|
34
34
|
- `/handoff` reports `OMP command failed` in the controlled canary even with deterministic role models configured; treat handoff as unavailable there until its native prerequisite is isolated.
|
|
35
35
|
- On official Paseo 0.8.0, requesting a live approval-mode change that the plugin rejects can trigger the daemon's unhandled-rejection restart path. Create a new `full`, `write`, or `ask` session instead of changing mode in place.
|
|
36
|
+
- Paseo 0.8 clients do not expose the platform-owned plugin URL opener and transform timeline items after Overview grouping. Use a 0.9.0-beta.1 app for external documentation/device-authorization actions and for reliable image-card transformation of every source tool call.
|
|
36
37
|
- Native Fast mode and a first-class plan mode are not exposed. `/handoff` is implemented but is not reproducible in the controlled canary without its native OMP workflow prerequisites.
|
|
37
38
|
- Terminal-started OMP sessions are discoverable and importable but are not registered automatically through a terminal hook.
|
|
39
|
+
- Paseo clients do not own OMP sessions. Mobile or desktop disconnects remain subject to Paseo's reconnect grace while the daemon-owned provider and OMP child continue independently; reconnecting can reveal an existing turn but does not transfer or sever runtime ownership.
|
|
38
40
|
- OMP tool output reaching RPC stdout is strictly parsed and bounded, but channel purity ultimately depends on OMP keeping non-protocol output off stdout.
|
|
39
41
|
|
|
40
42
|
The deduplicated comparison with every known OMP report in `getpaseo/paseo` is maintained in [docs/core-provider-issue-audit.md](docs/core-provider-issue-audit.md).
|
package/TESTING.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# OMP provider parity audit
|
|
2
2
|
|
|
3
|
-
Validated against the Paseo plugin SDK versions pinned in `package.json
|
|
3
|
+
Validated against the Paseo plugin SDK versions pinned in `package.json` and the real OMP compatibility matrix: high-use historical releases 17.2.15, 17.3.4, 18.0.11, and 18.1.10; minimum supported release 18.1.15; latest published 18.1 patch 18.1.22; and current release 18.2.0. Validation against official Paseo Docker image `0.9.0-beta.1@sha256:f75a0eb3547ad3cc6bbdeaa7277d2d50eb4d06a9dd669d480371d7adf1c911b5` is partial as recorded below.
|
|
4
4
|
|
|
5
5
|
Classifications:
|
|
6
6
|
|
|
@@ -32,7 +32,7 @@ Classifications:
|
|
|
32
32
|
| Image prompts | **Equivalent** | Valid native image models receive image blocks; text-only models receive private content-addressed local files with an aggregate cap and turn/session/failure cleanup. The file path is valid because the direct provider and OMP child share the daemon host. |
|
|
33
33
|
| Structured attachments | **Equivalent** | Forge change requests/issues, legacy GitHub forms, text, reviews, and uploaded files render to bounded OMP prompt text; regression in `tests/provider.test.ts`. |
|
|
34
34
|
| Optimistic message correlation | **Equivalent** | Native entry lookup, repeated-text occurrence correlation, steering correlation, replay-boundary dedupe, and exactly-one `session.prompt_result` regressions. Bounded branch snapshots rebuild incomplete or evicted replay watermarks; duplicate IDs, surplus matching entries, unavailable snapshots, and count/byte overflow retain local user-message fallback instead of claiming an old entry. |
|
|
35
|
-
| Terminal
|
|
35
|
+
| Terminal correlation on later turns | **Equivalent with legacy fallback** | A matching `agent_end.requestId` is authoritative and a mismatch is discarded before turn state changes. Released OMP builds that omit the field use ordered evidence from the accepted prompt: a fresh branch-correlated native user entry, later current-turn assistant activity, an idle non-compacting runtime, and no conflicting permission, tool, steer, or child-session work. `prompt_result.agentInvoked: false` remains local-only. Active ambiguous terminals are ignored; confirmed-idle ambiguity fails only the Paseo turn without killing the OMP process. Tests cover three sequential legacy prompts, stale terminals before evidence and while active, repeated text, local-only results, keyed mismatch/match, and degraded state/history. The remaining risk is same-agent misattribution when a delayed unkeyed event is indistinguishable from this ordered evidence. |
|
|
36
36
|
| Streaming assistant text | **Equivalent** | `OmpTimelineProjector` publishes stable complete snapshots with frame coalescing and bounded retained bytes. |
|
|
37
37
|
| Streaming reasoning | **Equivalent** | Indexed thinking blocks map to stable `reasoning` items and share stream bounds. |
|
|
38
38
|
| `contentIndex` ordering | **Equivalent** | Stable 0→1→0 updates, sparse-index rejection, and 64-block bounds are tested in `tests/provider.test.ts`. |
|
|
@@ -103,9 +103,9 @@ npm run test:integration:canary
|
|
|
103
103
|
|
|
104
104
|
Set `PASEO_CANARY_OMP_VERSION` to exercise another pinned release. The local runner knows the SHA-256 values for every version in the compatibility matrix; an unlisted version requires explicit `PASEO_CANARY_OMP_SHA256_AMD64` and `PASEO_CANARY_OMP_SHA256_ARM64` values. It allocates isolated loopback ports, builds a version-specific image, waits for both services to become healthy, runs the deterministic provider scenarios, prints container logs on failure, and always removes its containers, volumes, and image namespace.
|
|
105
105
|
|
|
106
|
-
The Linux real-OMP CI matrix downloads checksummed `omp-linux-x64` assets for OMP 17.2.15, 17.3.4, 18.0.11, 18.1.10, 18.1.15, 18.1.22, and 18.2.0, verifies each binary's pinned GitHub release SHA-256, and runs `PASEO_OMP_REAL_E2E=1 PASEO_OMP_VERSION=<version> npm test -- tests/provider.real.e2e.test.ts`. The historical entries are the four pre-floor releases with more than 4,000 downloads shown by npm for the seven days ending 2026-09-15; they are compatibility regression probes, not a support commitment. The remaining entries cover the supported floor, newest patch in that minor line, and current release. The test uses the real OMP binary and a local deterministic OpenAI-compatible model endpoint,
|
|
106
|
+
The Linux real-OMP CI matrix downloads checksummed `omp-linux-x64` assets for OMP 17.2.15, 17.3.4, 18.0.11, 18.1.10, 18.1.15, 18.1.22, and 18.2.0, verifies each binary's pinned GitHub release SHA-256, and runs `PASEO_OMP_REAL_E2E=1 PASEO_OMP_VERSION=<version> npm test -- tests/provider.real.e2e.test.ts`. The historical entries are the four pre-floor releases with more than 4,000 downloads shown by npm for the seven days ending 2026-09-15; they are compatibility regression probes, not a support commitment. The remaining entries cover the supported floor, newest patch in that minor line, and current release. The test uses the real OMP binary and a local deterministic OpenAI-compatible model endpoint, including three sequential prompts on one native session, a Bash tool, and oversized-image transport.
|
|
107
107
|
|
|
108
|
-
The controlled canary in `canary/compose.yml`
|
|
108
|
+
The controlled canary in `canary/compose.yml` successfully built the plugin into that official Paseo image, started the daemon, loaded `paseo-omp`, and completed runtime health, catalog/mode discovery, initial text, model switching, `autocompact`, `follow-up`, and the documented compact/handoff error collection. It then failed on the first Bash scenario because the later turn ended without request-correlated terminal ownership: `OMP terminal ownership could not be confirmed`. The subsequent image, MCP, permission, steering, interruption, persistence, subagent, Hub, usage, rewind, browser, and optional Ollama scenarios were not reached and remain unverified on Paseo 0.9.0-beta.1.
|
|
109
109
|
|
|
110
110
|
## OMP RPC compatibility intake
|
|
111
111
|
|
|
@@ -142,9 +142,9 @@ The go/no-go criteria, manual acceptance boundary, and alpha limitation list are
|
|
|
142
142
|
- `npm test -- tests/provider-conformance.test.ts`: host-boundary conformance coverage includes `prompt.command`, `session.configure`, typed and fallback permission allow/deny/cancel paths, registry-driven reload/removal, verified stubborn-descendant cleanup, and sequential turns plus interrupt races with post-turn barriers.
|
|
143
143
|
- `PASEO_OMP_REAL_E2E=1 PASEO_OMP_VERSION=<version> npm test -- tests/provider.real.e2e.test.ts`: the installed matrix version's catalog and hermetic real-binary text/Bash and oversized-image scenarios run against a local deterministic model.
|
|
144
144
|
- `npm run test:coverage`: Vitest enforces aggregate 85% function and 89% line coverage over loaded source modules. Generated `dist/**` trees are excluded.
|
|
145
|
-
- `npm run test:integration:install`:
|
|
145
|
+
- `npm run test:integration:install`: npm package acquisition retains required production dependencies, and a fresh Git-style checkout runs frozen production-only preparation with lifecycle scripts disabled. Both installed trees resolve runtime dependencies, compile the client and server entries with Paseo's host compiler, and load the server contribution.
|
|
146
146
|
- `npm run test:integration:docker`: verifies the host/container ownership boundary.
|
|
147
147
|
- `npm run test:integration:wsl`: locally skips when `wsl.exe` is unavailable; Windows CI sets `PASEO_OMP_REQUIRE_WSL=1`, so this boundary remains required there.
|
|
148
|
-
- `docker compose -f canary/compose.yml`: official Paseo 0.
|
|
148
|
+
- `docker compose -f canary/compose.yml`: official Paseo 0.9.0-beta.1 image build, daemon startup, plugin load, and early deterministic scenarios passed; the smoke stopped at the first later-turn Bash scenario with `OMP terminal ownership could not be confirmed`, so the remaining scenarios are unverified.
|
|
149
149
|
- `zizmor .github/workflows`: no findings (offline audit; six repository-wide suppressions remain).
|
|
150
150
|
- Release Please 17.1.2 `config.json` and `manifest.json` schema validation: passed for `release-please-config.json` and `.release-please-manifest.json`.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as paseoClient from "@getpaseo/plugin/client";
|
|
2
|
+
import { Linking } from "react-native";
|
|
3
|
+
import { selectExternalUrlOpener, validatedHttpUrl } from "../shared/external-url";
|
|
4
|
+
|
|
5
|
+
const paseoOpenExternalUrl =
|
|
6
|
+
"openExternalUrl" in paseoClient && typeof paseoClient.openExternalUrl === "function"
|
|
7
|
+
? paseoClient.openExternalUrl
|
|
8
|
+
: undefined;
|
|
9
|
+
const openExternalUrl = selectExternalUrlOpener(paseoOpenExternalUrl, (url) =>
|
|
10
|
+
Linking.openURL(url),
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
export function openOmpExternalUrl(url: string): Promise<void> {
|
|
14
|
+
return openExternalUrl(validatedHttpUrl(url));
|
|
15
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { type PluginTimelineItemProps, useRpc } from "@getpaseo/plugin/client";
|
|
2
2
|
import { Icon } from "@getpaseo/plugin/client/react-native";
|
|
3
3
|
import { useMemo, useState } from "react";
|
|
4
|
-
import {
|
|
4
|
+
import { Pressable, Text, View } from "react-native";
|
|
5
5
|
import {
|
|
6
6
|
type OmpMcpAuthorizationTimeline,
|
|
7
7
|
openOmpMcpAuthorizationInPaseoBrowser,
|
|
8
8
|
} from "../shared/mcp";
|
|
9
|
+
import { openOmpExternalUrl } from "./external-url";
|
|
9
10
|
|
|
10
11
|
function browserAuthorizationError(error: unknown): string {
|
|
11
12
|
const detail = error instanceof Error ? error.message : String(error);
|
|
@@ -98,7 +99,7 @@ export function OmpMcpAuthorizationCard({
|
|
|
98
99
|
setError(null);
|
|
99
100
|
setMessage(null);
|
|
100
101
|
try {
|
|
101
|
-
await
|
|
102
|
+
await openOmpExternalUrl(item.data.url);
|
|
102
103
|
} catch {
|
|
103
104
|
setError("Could not open the authorization URL. Copy the URL below into a browser.");
|
|
104
105
|
} finally {
|
|
@@ -10,7 +10,7 @@ import { useIsMutating, useMutation, useQuery, useQueryClient } from "@tanstack/
|
|
|
10
10
|
import type { ReactNode } from "react";
|
|
11
11
|
import { useCallback, useMemo, useState } from "react";
|
|
12
12
|
import type { TextStyle, ViewStyle } from "react-native";
|
|
13
|
-
import {
|
|
13
|
+
import { Pressable, ScrollView, Switch, Text, View } from "react-native";
|
|
14
14
|
import type { ComposerPillSettings } from "../shared/composer-pill-settings";
|
|
15
15
|
import { listOmpConfig, type OmpConfig } from "../shared/omp-config";
|
|
16
16
|
import {
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
import { type OmpStore, storeLabel } from "../shared/omp-store";
|
|
27
27
|
import { getOmpProviderHealth, type OmpProviderHealth } from "../shared/provider-diagnostics";
|
|
28
28
|
import { ComposerPillSettingsSection } from "./composer-pill-settings";
|
|
29
|
+
import { openOmpExternalUrl } from "./external-url";
|
|
29
30
|
import {
|
|
30
31
|
documentationForSettingCategory,
|
|
31
32
|
documentationForSettingPath,
|
|
@@ -1072,7 +1073,7 @@ function OmpConfigContent({
|
|
|
1072
1073
|
const openDocumentation = useCallback(async (link: OmpDocumentationLink) => {
|
|
1073
1074
|
setDocumentationError(null);
|
|
1074
1075
|
try {
|
|
1075
|
-
await
|
|
1076
|
+
await openOmpExternalUrl(link.url);
|
|
1076
1077
|
} catch {
|
|
1077
1078
|
setDocumentationError(`Could not open ${link.label.toLocaleLowerCase()}.`);
|
|
1078
1079
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PluginClientContext } from "@getpaseo/plugin/client";
|
|
2
|
+
|
|
3
|
+
type AsyncResult<Callable> = Callable extends (...args: never[]) => Promise<infer Result>
|
|
4
|
+
? Result
|
|
5
|
+
: never;
|
|
6
|
+
|
|
7
|
+
export type PaseoApi = PluginClientContext["paseo"];
|
|
8
|
+
export type PaseoAgentListResult = AsyncResult<PaseoApi["agents"]["list"]>;
|
|
9
|
+
export type PaseoProviderSnapshotResult = AsyncResult<PaseoApi["providers"]["snapshot"]>;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { PaseoApi, PaseoProviderSnapshotResult } from "@getpaseo/client";
|
|
2
1
|
import type {
|
|
3
2
|
OmpLspDiagnostics,
|
|
4
3
|
OmpMcpDiagnostics,
|
|
@@ -8,8 +7,8 @@ import type {
|
|
|
8
7
|
OmpVersionStatus,
|
|
9
8
|
PathState,
|
|
10
9
|
} from "../shared/provider-diagnostics";
|
|
11
|
-
|
|
12
10
|
import { isOmpProvider } from "./omp-store-state";
|
|
11
|
+
import type { PaseoApi, PaseoProviderSnapshotResult } from "./paseo-types";
|
|
13
12
|
|
|
14
13
|
export type ProviderHealthTone = "ok" | "warning" | "danger" | "muted";
|
|
15
14
|
export const OMP_PROVIDER_IDS = ["omp", "omp-plugin"] as const;
|
|
@@ -7,7 +7,7 @@ This checklist prepares `paseo-omp-v0.1.0-alpha.1`. It does not authorize public
|
|
|
7
7
|
- [ ] Release Please proposes `0.1.0-alpha.1` from manifest version `0.0.0`.
|
|
8
8
|
- [ ] Package and tag names are `@omercnet/paseo-omp` and `paseo-omp-v0.1.0-alpha.1`.
|
|
9
9
|
- [ ] Provider identity remains `omp-plugin`; bundled `omp` remains independent and enabled or disabled by the user.
|
|
10
|
-
- [ ] Paseo requirement remains
|
|
10
|
+
- [ ] Paseo requirement remains the reviewed dual-version range, currently `>=0.8.0 <0.10.0`.
|
|
11
11
|
- [ ] Minimum tested OMP version, checksum, CI job, README, SUPPORT, and TESTING agree.
|
|
12
12
|
|
|
13
13
|
## Required gates
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Core OMP provider issue audit
|
|
2
2
|
|
|
3
|
-
This audit compares reports in `getpaseo/paseo` with the community `omp-plugin` provider. It was refreshed on 2026-09-
|
|
3
|
+
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.
|
|
4
4
|
|
|
5
5
|
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.
|
|
6
6
|
|
|
@@ -20,6 +20,7 @@ Status meanings:
|
|
|
20
20
|
| [#2260](https://github.com/getpaseo/paseo/issues/2260), [PR #2261](https://github.com/getpaseo/paseo/pull/2261) | **Verified** | Incomplete or compacted `agent_end` frames use complete streamed `message_end` evidence or bounded history correlated by entry ID, with idle state confirmed before and after retrieval. Success, failure, native cancellation, and concurrent interrupts are preserved; missing, unavailable, non-correlatable, or conflicting terminal evidence fails closed with content-free count diagnostics. |
|
|
21
21
|
| [#2281](https://github.com/getpaseo/paseo/issues/2281), [PR #2282](https://github.com/getpaseo/paseo/pull/2282) | **Verified** | Local-only prompt results and structured commands have explicit terminal ownership. |
|
|
22
22
|
| [#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. |
|
|
23
|
+
| 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. |
|
|
23
24
|
| [#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. |
|
|
24
25
|
| 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. |
|
|
25
26
|
| [#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. |
|
package/docs/installation.md
CHANGED
|
@@ -4,29 +4,53 @@ Paseo plugins are trusted, unsandboxed code. Review this plugin and its producti
|
|
|
4
4
|
|
|
5
5
|
## Requirements
|
|
6
6
|
|
|
7
|
-
- Paseo daemon and apps:
|
|
7
|
+
- Paseo daemon and apps: `>=0.8.0 <0.10.0`; `0.9.0-beta.1` is recommended
|
|
8
8
|
- OMP: `18.1.15` or newer is the supported floor
|
|
9
9
|
- OMP RPC: protocol v2 must negotiate successfully
|
|
10
10
|
|
|
11
11
|
The first public build is an alpha. Alpha releases are compatibility previews and may require deleting and re-importing agents created by an earlier preview.
|
|
12
12
|
|
|
13
|
-
## Install
|
|
13
|
+
## Install or update from npm on Paseo 0.9
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Paseo 0.9.0-beta.1 can acquire the published package and its production dependencies directly from npm on the daemon host:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
18
|
paseo plugin install npm:@omercnet/paseo-omp@<version>
|
|
19
19
|
paseo plugin ls paseo-omp
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
Check for the registry's current `latest` version and approve the proposed update, or select an exact version explicitly:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
paseo plugin update paseo-omp
|
|
26
|
+
paseo plugin update paseo-omp --version <new-version>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The daemon uses its own npm registry and authentication configuration. npm acquisition installs the artifact's production dependencies before preparation; because npm artifacts omit `package-lock.json`, the preparation helper leaves that tree unchanged. A Git checkout with the committed lockfile runs frozen `npm ci --omit=dev --ignore-scripts`. A failed download, preparation, compatibility check, or activation keeps the installed revision active. Paseo 0.8 does not support npm plugin sources; use a Git tag or local directory instead.
|
|
30
|
+
|
|
31
|
+
## Install a Git release on Paseo 0.8 or 0.9
|
|
32
|
+
|
|
33
|
+
Install the matching reviewed Git tag:
|
|
23
34
|
|
|
24
35
|
```bash
|
|
25
36
|
paseo plugin add omercnet/paseo-plugins:paseo-omp --ref paseo-omp-v<version>
|
|
26
37
|
paseo plugin ls paseo-omp
|
|
27
38
|
```
|
|
28
39
|
|
|
29
|
-
|
|
40
|
+
### Update a Git installation on Paseo 0.9
|
|
41
|
+
|
|
42
|
+
An ordinary update reviews the remote repository's current default HEAD. To move directly to another reviewed tag or commit, select it explicitly:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
paseo plugin update paseo-omp
|
|
46
|
+
paseo plugin update paseo-omp --ref paseo-omp-v<new-version>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The install-time `--ref` does not constrain later updates. Paseo 0.9 stages and validates the selected revision before replacing the active installation, so plugin settings remain intact and a failed update leaves the previous revision running.
|
|
50
|
+
|
|
51
|
+
### Update a tag-pinned Git installation on Paseo 0.8
|
|
52
|
+
|
|
53
|
+
Paseo 0.8 does not provide the 0.9 non-destructive explicit-ref update flow. Record the current installation, then replace it with the new tag in one maintenance window:
|
|
30
54
|
|
|
31
55
|
```bash
|
|
32
56
|
paseo plugin ls paseo-omp --json > paseo-omp-before-update.json
|
|
@@ -56,12 +80,10 @@ paseo plugin ls paseo-omp
|
|
|
56
80
|
|
|
57
81
|
## Track a branch
|
|
58
82
|
|
|
59
|
-
Tracking `main` executes future dependency and plugin updates with the daemon user's privileges
|
|
83
|
+
Tracking `main` executes future dependency and plugin updates with the daemon user's privileges:
|
|
60
84
|
|
|
61
85
|
```bash
|
|
62
86
|
paseo plugin add omercnet/paseo-plugins:paseo-omp --ref main
|
|
63
|
-
paseo plugin ls paseo-omp --json > paseo-omp-before-update.json
|
|
64
|
-
paseo plugin update paseo-omp
|
|
65
87
|
```
|
|
66
88
|
|
|
67
|
-
|
|
89
|
+
On Paseo 0.9, preview and approve the remote default HEAD with `paseo plugin update paseo-omp`, or select `main` explicitly with `paseo plugin update paseo-omp --ref main`. On Paseo 0.8, record the installed commit before its existing branch-update workflow. A failed build or compatibility check leaves the previous revision active.
|
package/index.client.tsx
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { PaseoAgentListResult, PaseoApi } from "@getpaseo/client";
|
|
2
1
|
import { settingsRpc } from "@getpaseo/plugin";
|
|
3
2
|
import type {
|
|
4
3
|
PluginButtonRegistration,
|
|
@@ -19,6 +18,7 @@ import {
|
|
|
19
18
|
isOmpProvider,
|
|
20
19
|
ompStoreKey,
|
|
21
20
|
} from "./client/omp-store-state";
|
|
21
|
+
import type { PaseoAgentListResult, PaseoApi } from "./client/paseo-types";
|
|
22
22
|
import { quotaProviderIcon } from "./client/provider-icon";
|
|
23
23
|
import { OmpImageTimeline } from "./client/provider-image";
|
|
24
24
|
import { QuotaPopover } from "./client/quota-popover";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omercnet/paseo-omp",
|
|
3
|
-
"version": "0.2.1-next.
|
|
3
|
+
"version": "0.2.1-next.90.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Paseo integration for OMP, including its direct provider and workspace tooling.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -57,20 +57,19 @@
|
|
|
57
57
|
"typecheck": "tsc --noEmit"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
+
"@getpaseo/protocol": "0.9.0-beta.1",
|
|
60
61
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
61
62
|
"yaml": "^2.9.0"
|
|
62
63
|
},
|
|
63
64
|
"devDependencies": {
|
|
64
65
|
"@biomejs/biome": "^2.5.10",
|
|
65
|
-
"@getpaseo/cli": "0.
|
|
66
|
-
"@getpaseo/client": "0.
|
|
67
|
-
"@getpaseo/plugin": "0.
|
|
68
|
-
"@getpaseo/protocol": "0.8.0",
|
|
66
|
+
"@getpaseo/cli": "0.9.0-beta.1",
|
|
67
|
+
"@getpaseo/client": "0.9.0-beta.1",
|
|
68
|
+
"@getpaseo/plugin": "0.9.0-beta.1",
|
|
69
69
|
"@tanstack/react-query": "^5.102.3",
|
|
70
70
|
"@types/node": "^24.10.1",
|
|
71
71
|
"@types/react": "^19.2.18",
|
|
72
72
|
"@vitest/coverage-v8": "^5.0.0",
|
|
73
|
-
"esbuild": "^0.28.0",
|
|
74
73
|
"react": "19.1.0",
|
|
75
74
|
"react-native": "0.81.5",
|
|
76
75
|
"tsx": "^4.20.6",
|
package/paseo-plugin.json
CHANGED
|
@@ -8,10 +8,14 @@ const projectRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
|
8
8
|
export function prepareDependencies(root = projectRoot, execute = execFileSync) {
|
|
9
9
|
if (!existsSync(join(root, "package-lock.json"))) return false;
|
|
10
10
|
|
|
11
|
-
execute(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
execute(
|
|
12
|
+
process.platform === "win32" ? "npm.cmd" : "npm",
|
|
13
|
+
["ci", "--omit=dev", "--ignore-scripts"],
|
|
14
|
+
{
|
|
15
|
+
cwd: root,
|
|
16
|
+
stdio: "inherit",
|
|
17
|
+
},
|
|
18
|
+
);
|
|
15
19
|
return true;
|
|
16
20
|
}
|
|
17
21
|
|
|
@@ -228,6 +228,24 @@ type PendingUser = {
|
|
|
228
228
|
fallbackOnFinish: boolean;
|
|
229
229
|
bufferedEchoes: OmpMessage[];
|
|
230
230
|
};
|
|
231
|
+
type TerminalCorrelationEvidence = "fresh-native-user" | "current-assistant";
|
|
232
|
+
|
|
233
|
+
type TerminalCorrelation = {
|
|
234
|
+
policy: "initial-turn" | "ordered-legacy" | "native-command";
|
|
235
|
+
evidence: Map<TerminalCorrelationEvidence, number>;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
type TerminalCandidate = {
|
|
239
|
+
event: Extract<OmpRpcEvent, { type: "agent_end" }>;
|
|
240
|
+
arrivalSequence: number;
|
|
241
|
+
confidence:
|
|
242
|
+
| "keyed"
|
|
243
|
+
| "initial-turn"
|
|
244
|
+
| "ordered-legacy"
|
|
245
|
+
| "native-command"
|
|
246
|
+
| "interrupted"
|
|
247
|
+
| "ambiguous";
|
|
248
|
+
};
|
|
231
249
|
|
|
232
250
|
type ActiveTurn = {
|
|
233
251
|
turnId: string;
|
|
@@ -245,10 +263,8 @@ type ActiveTurn = {
|
|
|
245
263
|
awaitingPermissionEvidence: boolean;
|
|
246
264
|
activitySequence: number;
|
|
247
265
|
acknowledged: boolean;
|
|
248
|
-
|
|
249
|
-
terminalOwnershipRequired: boolean;
|
|
266
|
+
terminalCorrelation: TerminalCorrelation;
|
|
250
267
|
replayingBufferedEvents: boolean;
|
|
251
|
-
bufferedTerminalOwnershipEvidence: boolean;
|
|
252
268
|
agentInvoked?: boolean;
|
|
253
269
|
nativeRequestId?: string;
|
|
254
270
|
promptAcceptedEventIndex?: number;
|
|
@@ -263,14 +279,15 @@ type ActiveTurn = {
|
|
|
263
279
|
agentEndRetryTimer?: unknown;
|
|
264
280
|
agentEndDeadlineTimer?: unknown;
|
|
265
281
|
agentEndCheck?: Promise<void>;
|
|
266
|
-
|
|
282
|
+
ambiguousTerminalTimer?: unknown;
|
|
267
283
|
terminalizing: boolean;
|
|
268
284
|
terminalization?: Promise<void>;
|
|
269
285
|
terminalOutcome?: TurnOutcome;
|
|
270
286
|
terminalWake?: VoidDeferred;
|
|
271
287
|
steerReady: VoidDeferred;
|
|
272
288
|
steersInFlight: number;
|
|
273
|
-
deferredAgentEnd?:
|
|
289
|
+
deferredAgentEnd?: TerminalCandidate;
|
|
290
|
+
activeToolCallIds: Set<string>;
|
|
274
291
|
bufferedEvents: OmpRpcEvent[];
|
|
275
292
|
pendingUsers: PendingUser[];
|
|
276
293
|
userEchoes: OmpMessage[];
|
|
@@ -784,7 +801,7 @@ function createActiveTurn(
|
|
|
784
801
|
clientMessageId: string,
|
|
785
802
|
text: string,
|
|
786
803
|
generation: number,
|
|
787
|
-
|
|
804
|
+
terminalPolicy: TerminalCorrelation["policy"],
|
|
788
805
|
manualCompaction = false,
|
|
789
806
|
): ActiveTurn {
|
|
790
807
|
return {
|
|
@@ -809,11 +826,10 @@ function createActiveTurn(
|
|
|
809
826
|
manualCompaction,
|
|
810
827
|
activitySequence: 0,
|
|
811
828
|
acknowledged: false,
|
|
812
|
-
|
|
829
|
+
terminalCorrelation: { policy: terminalPolicy, evidence: new Map() },
|
|
813
830
|
replayingBufferedEvents: false,
|
|
814
|
-
bufferedTerminalOwnershipEvidence: false,
|
|
815
|
-
terminalOwnershipRequired,
|
|
816
831
|
steersInFlight: 0,
|
|
832
|
+
activeToolCallIds: new Set(),
|
|
817
833
|
steerReady: Promise.withResolvers<void>(),
|
|
818
834
|
userCorrelationActive: false,
|
|
819
835
|
userLookups: new Set(),
|
|
@@ -833,6 +849,35 @@ function createActiveTurn(
|
|
|
833
849
|
],
|
|
834
850
|
};
|
|
835
851
|
}
|
|
852
|
+
function classifyTerminalCandidate(
|
|
853
|
+
turn: ActiveTurn,
|
|
854
|
+
event: Extract<OmpRpcEvent, { type: "agent_end" }>,
|
|
855
|
+
arrivalSequence = turn.activitySequence,
|
|
856
|
+
): TerminalCandidate {
|
|
857
|
+
if (event.requestId !== undefined) return { event, confidence: "keyed", arrivalSequence };
|
|
858
|
+
if (turn.agentInvoked === false) return { event, confidence: "ambiguous", arrivalSequence };
|
|
859
|
+
if (turn.terminalCorrelation.policy === "initial-turn") {
|
|
860
|
+
return { event, confidence: "initial-turn", arrivalSequence };
|
|
861
|
+
}
|
|
862
|
+
const assistantSequence = turn.terminalCorrelation.evidence.get("current-assistant");
|
|
863
|
+
if (
|
|
864
|
+
turn.terminalCorrelation.policy === "native-command" &&
|
|
865
|
+
assistantSequence !== undefined &&
|
|
866
|
+
assistantSequence <= arrivalSequence
|
|
867
|
+
) {
|
|
868
|
+
return { event, confidence: "native-command", arrivalSequence };
|
|
869
|
+
}
|
|
870
|
+
const userSequence = turn.terminalCorrelation.evidence.get("fresh-native-user");
|
|
871
|
+
if (
|
|
872
|
+
userSequence !== undefined &&
|
|
873
|
+
assistantSequence !== undefined &&
|
|
874
|
+
userSequence < assistantSequence &&
|
|
875
|
+
assistantSequence <= arrivalSequence
|
|
876
|
+
) {
|
|
877
|
+
return { event, confidence: "ordered-legacy", arrivalSequence };
|
|
878
|
+
}
|
|
879
|
+
return { event, confidence: "ambiguous", arrivalSequence };
|
|
880
|
+
}
|
|
836
881
|
|
|
837
882
|
export class OmpProviderSession {
|
|
838
883
|
readonly id: string;
|
|
@@ -1864,7 +1909,7 @@ export class OmpProviderSession {
|
|
|
1864
1909
|
input.prompt.clientMessageId,
|
|
1865
1910
|
payload.text,
|
|
1866
1911
|
this.generation,
|
|
1867
|
-
this.runtimeTurnCompleted,
|
|
1912
|
+
this.runtimeTurnCompleted ? "ordered-legacy" : "initial-turn",
|
|
1868
1913
|
slashCommandName(payload.text) === "compact",
|
|
1869
1914
|
);
|
|
1870
1915
|
this.activeTurn = turn;
|
|
@@ -1901,7 +1946,11 @@ export class OmpProviderSession {
|
|
|
1901
1946
|
return;
|
|
1902
1947
|
}
|
|
1903
1948
|
const ownershipPending = turn.pendingUsers[0];
|
|
1904
|
-
if (
|
|
1949
|
+
if (
|
|
1950
|
+
turn.terminalCorrelation.policy === "ordered-legacy" &&
|
|
1951
|
+
!this.branchWatermarkValid &&
|
|
1952
|
+
ownershipPending
|
|
1953
|
+
) {
|
|
1905
1954
|
await this.refreshBranchEntries(turn, ownershipPending);
|
|
1906
1955
|
if (
|
|
1907
1956
|
this.closed ||
|
|
@@ -1939,13 +1988,10 @@ export class OmpProviderSession {
|
|
|
1939
1988
|
0,
|
|
1940
1989
|
turn.promptAcceptedEventIndex ?? bufferedEvents.length,
|
|
1941
1990
|
);
|
|
1942
|
-
for (const event of preAcceptanceEvents) this.handleTurnEvent(turn, event);
|
|
1991
|
+
for (const event of preAcceptanceEvents) this.handleTurnEvent(turn, event, false);
|
|
1943
1992
|
this.projector.acceptLiveTurn(turn.turnId);
|
|
1944
1993
|
for (const event of bufferedEvents) this.handleTurnEvent(turn, event);
|
|
1945
1994
|
turn.replayingBufferedEvents = false;
|
|
1946
|
-
if (acknowledgement.agentInvoked === true && turn.bufferedTerminalOwnershipEvidence) {
|
|
1947
|
-
this.markTerminalOwnershipEvidence(turn);
|
|
1948
|
-
}
|
|
1949
1995
|
if (
|
|
1950
1996
|
acknowledgement.agentInvoked === false &&
|
|
1951
1997
|
turn.agentInvoked !== true &&
|
|
@@ -2096,7 +2142,7 @@ export class OmpProviderSession {
|
|
|
2096
2142
|
if (this.activeTurn) {
|
|
2097
2143
|
throw new OmpPublicError("OMP already has an active turn; send this message as a steer");
|
|
2098
2144
|
}
|
|
2099
|
-
const turn = createActiveTurn(clientMessageId, text, this.generation,
|
|
2145
|
+
const turn = createActiveTurn(clientMessageId, text, this.generation, "native-command");
|
|
2100
2146
|
this.activeTurn = turn;
|
|
2101
2147
|
try {
|
|
2102
2148
|
await invoke();
|
|
@@ -2994,6 +3040,7 @@ export class OmpProviderSession {
|
|
|
2994
3040
|
return;
|
|
2995
3041
|
}
|
|
2996
3042
|
turn.localOnlyDisabled = true;
|
|
3043
|
+
this.cancelAmbiguousTerminal(turn);
|
|
2997
3044
|
turn.deferredAgentEnd = undefined;
|
|
2998
3045
|
this.acceptPendingUser(turn, pending);
|
|
2999
3046
|
this.emit({
|
|
@@ -3145,7 +3192,11 @@ export class OmpProviderSession {
|
|
|
3145
3192
|
this.handleTurnEvent(turn, event);
|
|
3146
3193
|
}
|
|
3147
3194
|
|
|
3148
|
-
private handleTurnEvent(
|
|
3195
|
+
private handleTurnEvent(
|
|
3196
|
+
turn: ActiveTurn,
|
|
3197
|
+
event: OmpRpcEvent,
|
|
3198
|
+
correlatesToAcceptedPrompt = true,
|
|
3199
|
+
): void {
|
|
3149
3200
|
if (
|
|
3150
3201
|
turn.generation !== this.generation ||
|
|
3151
3202
|
turn.terminal ||
|
|
@@ -3161,7 +3212,7 @@ export class OmpProviderSession {
|
|
|
3161
3212
|
) {
|
|
3162
3213
|
return;
|
|
3163
3214
|
}
|
|
3164
|
-
if (event.type === "message_end") {
|
|
3215
|
+
if (event.type === "message_end" && correlatesToAcceptedPrompt) {
|
|
3165
3216
|
const entryId = nativeEntryId(event.message);
|
|
3166
3217
|
if (!entryId || turn.streamedMessageEntryIds.length >= MAX_AGENT_END_CORRELATION_MESSAGES) {
|
|
3167
3218
|
turn.streamedMessageIdentityComplete = false;
|
|
@@ -3187,15 +3238,19 @@ export class OmpProviderSession {
|
|
|
3187
3238
|
if (event.type === "prompt_result") {
|
|
3188
3239
|
if (!event.id || event.id !== turn.nativeRequestId) return;
|
|
3189
3240
|
if (event.agentInvoked) {
|
|
3241
|
+
// A request-keyed prompt result proves dispatch, but an unkeyed terminal still needs
|
|
3242
|
+
// ordered native user and assistant evidence from this accepted prompt.
|
|
3190
3243
|
this.markAgentEvidence(turn);
|
|
3191
|
-
if (turn.replayingBufferedEvents) turn.bufferedTerminalOwnershipEvidence = true;
|
|
3192
|
-
else this.markTerminalOwnershipEvidence(turn);
|
|
3193
3244
|
return;
|
|
3194
3245
|
}
|
|
3195
3246
|
if (turn.localOnlyDisabled || turn.steersInFlight > 0) return;
|
|
3196
3247
|
if (!turn.nativeActivity && !turn.awaitingPermissionEvidence) {
|
|
3197
3248
|
turn.agentInvoked = false;
|
|
3198
3249
|
turn.localOnlyEligible = true;
|
|
3250
|
+
if (turn.deferredAgentEnd?.confidence === "ambiguous") {
|
|
3251
|
+
turn.deferredAgentEnd = undefined;
|
|
3252
|
+
this.cancelAmbiguousTerminal(turn);
|
|
3253
|
+
}
|
|
3199
3254
|
this.scheduleLocalOnlyCompletion(turn);
|
|
3200
3255
|
}
|
|
3201
3256
|
return;
|
|
@@ -3243,8 +3298,10 @@ export class OmpProviderSession {
|
|
|
3243
3298
|
return;
|
|
3244
3299
|
}
|
|
3245
3300
|
if (event.type === "agent_end" && turn.manualCompactionPending) return;
|
|
3246
|
-
if (event.type === "
|
|
3247
|
-
|
|
3301
|
+
if (event.type === "tool_execution_start") turn.activeToolCallIds.add(event.toolCallId);
|
|
3302
|
+
if (event.type === "tool_execution_end") {
|
|
3303
|
+
turn.activeToolCallIds.delete(event.toolCallId);
|
|
3304
|
+
if (event.toolName === "ask_user") this.pendingFreeformSelection = null;
|
|
3248
3305
|
}
|
|
3249
3306
|
if (event.type === "tool_execution_start" || event.type === "tool_execution_end") {
|
|
3250
3307
|
try {
|
|
@@ -3254,23 +3311,6 @@ export class OmpProviderSession {
|
|
|
3254
3311
|
return;
|
|
3255
3312
|
}
|
|
3256
3313
|
}
|
|
3257
|
-
if (isNativeTurnActivity(event)) {
|
|
3258
|
-
turn.nativeActivity = true;
|
|
3259
|
-
this.cancelLocalOnlyCompletion(turn);
|
|
3260
|
-
}
|
|
3261
|
-
if (event.type === "message_end" && event.message.role === "user") {
|
|
3262
|
-
this.markAgentEvidence(turn);
|
|
3263
|
-
this.projectUserEcho(turn, event.message);
|
|
3264
|
-
return;
|
|
3265
|
-
}
|
|
3266
|
-
if (
|
|
3267
|
-
(event.type === "message_start" ||
|
|
3268
|
-
event.type === "message_update" ||
|
|
3269
|
-
event.type === "message_end") &&
|
|
3270
|
-
event.message.role === "assistant"
|
|
3271
|
-
) {
|
|
3272
|
-
turn.awaitingPermissionEvidence = false;
|
|
3273
|
-
}
|
|
3274
3314
|
if (event.type === "agent_end") {
|
|
3275
3315
|
if (event.isTerminal === false) {
|
|
3276
3316
|
turn.completedMessageCount = 0;
|
|
@@ -3280,41 +3320,51 @@ export class OmpProviderSession {
|
|
|
3280
3320
|
turn.lastCompletedAssistantEntryId = undefined;
|
|
3281
3321
|
return;
|
|
3282
3322
|
}
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
!(turn.replayingBufferedEvents && turn.bufferedTerminalOwnershipEvidence)
|
|
3289
|
-
) {
|
|
3290
|
-
this.scheduleTerminalOwnershipTimeout(turn);
|
|
3323
|
+
const candidate: TerminalCandidate = turn.interrupted
|
|
3324
|
+
? { event, confidence: "interrupted", arrivalSequence: turn.activitySequence }
|
|
3325
|
+
: classifyTerminalCandidate(turn, event);
|
|
3326
|
+
if (candidate.confidence === "ambiguous") {
|
|
3327
|
+
this.deferAmbiguousTerminal(turn, candidate);
|
|
3291
3328
|
return;
|
|
3292
3329
|
}
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
if (
|
|
3296
|
-
turn.
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
}
|
|
3304
|
-
if (turn.steersInFlight > 0) {
|
|
3305
|
-
turn.deferredAgentEnd = event;
|
|
3330
|
+
turn.nativeActivity = true;
|
|
3331
|
+
this.cancelLocalOnlyCompletion(turn);
|
|
3332
|
+
if (
|
|
3333
|
+
(!turn.interrupted &&
|
|
3334
|
+
candidate.confidence !== "keyed" &&
|
|
3335
|
+
this.hasTerminalConflict(turn, false)) ||
|
|
3336
|
+
turn.steersInFlight > 0 ||
|
|
3337
|
+
turn.terminalizing
|
|
3338
|
+
) {
|
|
3339
|
+
turn.deferredAgentEnd = candidate;
|
|
3306
3340
|
return;
|
|
3307
3341
|
}
|
|
3308
|
-
this.beginTerminalization(turn,
|
|
3342
|
+
this.beginTerminalization(turn, candidate);
|
|
3309
3343
|
return;
|
|
3310
3344
|
}
|
|
3311
|
-
if (isNativeTurnActivity(event))
|
|
3345
|
+
if (isNativeTurnActivity(event)) {
|
|
3346
|
+
turn.nativeActivity = true;
|
|
3347
|
+
this.cancelLocalOnlyCompletion(turn);
|
|
3348
|
+
}
|
|
3312
3349
|
if (event.type === "message_end" && event.message.role === "user") {
|
|
3313
3350
|
this.markAgentEvidence(turn);
|
|
3314
3351
|
this.projectUserEcho(turn, event.message);
|
|
3315
3352
|
return;
|
|
3316
3353
|
}
|
|
3354
|
+
if (
|
|
3355
|
+
(event.type === "message_start" ||
|
|
3356
|
+
event.type === "message_update" ||
|
|
3357
|
+
event.type === "message_end") &&
|
|
3358
|
+
event.message.role === "assistant"
|
|
3359
|
+
) {
|
|
3360
|
+
turn.awaitingPermissionEvidence = false;
|
|
3361
|
+
if (correlatesToAcceptedPrompt) {
|
|
3362
|
+
turn.terminalCorrelation.evidence.set("current-assistant", turn.activitySequence + 1);
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
if (isNativeTurnActivity(event)) this.markAgentEvidence(turn);
|
|
3317
3366
|
this.projector.project(event, turn.turnId);
|
|
3367
|
+
if (event.type === "tool_execution_end") this.resumeDeferredAgentEnd();
|
|
3318
3368
|
}
|
|
3319
3369
|
|
|
3320
3370
|
private projectUserEcho(turn: ActiveTurn, message: OmpMessage): void {
|
|
@@ -3385,6 +3435,7 @@ export class OmpProviderSession {
|
|
|
3385
3435
|
pending.bufferedEchoes.push(...turn.userEchoes.splice(0));
|
|
3386
3436
|
return;
|
|
3387
3437
|
}
|
|
3438
|
+
const observedSequence = turn.activitySequence;
|
|
3388
3439
|
let resolvedId = entryId ?? this.claimUnclaimedBranchEntry(turn, pending.text);
|
|
3389
3440
|
if (!resolvedId && (await this.refreshBranchEntries(turn, pending))) {
|
|
3390
3441
|
resolvedId = this.claimUnclaimedBranchEntry(turn, pending.text);
|
|
@@ -3400,7 +3451,7 @@ export class OmpProviderSession {
|
|
|
3400
3451
|
turn.userEchoes.shift();
|
|
3401
3452
|
if (!resolvedId) return;
|
|
3402
3453
|
turn.pendingUsers.shift();
|
|
3403
|
-
this.publishCorrelatedUser(turn, pending, resolvedId);
|
|
3454
|
+
this.publishCorrelatedUser(turn, pending, resolvedId, observedSequence);
|
|
3404
3455
|
}
|
|
3405
3456
|
}
|
|
3406
3457
|
private async refreshBranchEntries(turn: ActiveTurn, pending: PendingUser): Promise<boolean> {
|
|
@@ -4234,23 +4285,27 @@ export class OmpProviderSession {
|
|
|
4234
4285
|
return this.activeTurn?.turnId === pending.turnId && !this.activeTurn.terminal;
|
|
4235
4286
|
}
|
|
4236
4287
|
|
|
4237
|
-
private
|
|
4238
|
-
const turn = this.activeTurn;
|
|
4239
|
-
if (!turn?.deferredAgentEnd || turn.terminal || turn.terminalizing) return;
|
|
4288
|
+
private hasTerminalConflict(turn: ActiveTurn, includeChildren = true): boolean {
|
|
4240
4289
|
const ownsTurn = (pending: PendingPermission | PendingToolPermission) =>
|
|
4241
4290
|
pending.turnId === turn.turnId;
|
|
4242
|
-
|
|
4291
|
+
return (
|
|
4292
|
+
turn.awaitingPermissionEvidence ||
|
|
4243
4293
|
[...this.pendingPermissions.values()].some(ownsTurn) ||
|
|
4244
4294
|
[...this.inFlightPermissions.values()].some(ownsTurn) ||
|
|
4245
4295
|
[...this.pendingToolPermissions.values()].some(ownsTurn) ||
|
|
4246
|
-
[...this.inFlightToolPermissions.values()].some(ownsTurn)
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4296
|
+
[...this.inFlightToolPermissions.values()].some(ownsTurn) ||
|
|
4297
|
+
turn.activeToolCallIds.size > 0 ||
|
|
4298
|
+
turn.steersInFlight > 0 ||
|
|
4299
|
+
(includeChildren && Boolean(this.subsessions?.hasActiveChildren()))
|
|
4300
|
+
);
|
|
4301
|
+
}
|
|
4302
|
+
|
|
4303
|
+
private reevaluateDeferredPermissionTerminal(): void {
|
|
4304
|
+
const turn = this.activeTurn;
|
|
4305
|
+
if (!turn?.deferredAgentEnd || turn.terminal || turn.terminalizing) return;
|
|
4306
|
+
if (this.hasTerminalConflict(turn)) return;
|
|
4253
4307
|
const deferred = turn.deferredAgentEnd;
|
|
4308
|
+
if (deferred.confidence === "ambiguous") return;
|
|
4254
4309
|
turn.deferredAgentEnd = undefined;
|
|
4255
4310
|
this.beginTerminalization(turn, deferred);
|
|
4256
4311
|
}
|
|
@@ -4266,12 +4321,18 @@ export class OmpProviderSession {
|
|
|
4266
4321
|
return this.slashCommands.has(commandName);
|
|
4267
4322
|
}
|
|
4268
4323
|
|
|
4269
|
-
private publishCorrelatedUser(
|
|
4324
|
+
private publishCorrelatedUser(
|
|
4325
|
+
turn: ActiveTurn,
|
|
4326
|
+
pending: PendingUser,
|
|
4327
|
+
entryId: string | undefined,
|
|
4328
|
+
observedSequence: number,
|
|
4329
|
+
): void {
|
|
4270
4330
|
if (entryId) {
|
|
4271
4331
|
if (this.emittedEntryIds.has(entryId)) return;
|
|
4272
4332
|
this.seenEntryIds.add(entryId);
|
|
4273
4333
|
this.emittedEntryIds.add(entryId);
|
|
4274
|
-
|
|
4334
|
+
turn.terminalCorrelation.evidence.set("fresh-native-user", observedSequence);
|
|
4335
|
+
this.refreshAmbiguousTerminal(turn);
|
|
4275
4336
|
if (this.branchWatermarkValid && !this.branchEntryIds.has(entryId)) {
|
|
4276
4337
|
if (this.branchEntryIds.size >= MAX_UNCLAIMED_BRANCH_ENTRIES)
|
|
4277
4338
|
this.quarantineBranchEntries();
|
|
@@ -4291,12 +4352,7 @@ export class OmpProviderSession {
|
|
|
4291
4352
|
turn.activitySequence += 1;
|
|
4292
4353
|
turn.localOnlyEligible = false;
|
|
4293
4354
|
this.cancelLocalOnlyCompletion(turn);
|
|
4294
|
-
|
|
4295
|
-
}
|
|
4296
|
-
|
|
4297
|
-
private markTerminalOwnershipEvidence(turn: ActiveTurn): void {
|
|
4298
|
-
turn.terminalOwnershipEvidence = true;
|
|
4299
|
-
this.cancelTerminalOwnershipTimeout(turn);
|
|
4355
|
+
this.refreshAmbiguousTerminal(turn);
|
|
4300
4356
|
}
|
|
4301
4357
|
|
|
4302
4358
|
private scheduleLocalOnlyCompletion(turn: ActiveTurn): void {
|
|
@@ -4322,32 +4378,68 @@ export class OmpProviderSession {
|
|
|
4322
4378
|
turn.localOnlyTimer = undefined;
|
|
4323
4379
|
}
|
|
4324
4380
|
|
|
4325
|
-
private
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4381
|
+
private refreshAmbiguousTerminal(turn: ActiveTurn): void {
|
|
4382
|
+
const candidate = turn.deferredAgentEnd;
|
|
4383
|
+
if (candidate?.confidence !== "ambiguous") return;
|
|
4384
|
+
const resolved = classifyTerminalCandidate(turn, candidate.event, candidate.arrivalSequence);
|
|
4385
|
+
if (resolved.confidence !== "ambiguous") {
|
|
4386
|
+
this.cancelAmbiguousTerminal(turn);
|
|
4387
|
+
turn.deferredAgentEnd = undefined;
|
|
4388
|
+
if (turn.terminalizing || this.hasTerminalConflict(turn, false)) {
|
|
4389
|
+
turn.deferredAgentEnd = resolved;
|
|
4390
|
+
} else {
|
|
4391
|
+
this.beginTerminalization(turn, resolved);
|
|
4392
|
+
}
|
|
4331
4393
|
return;
|
|
4332
4394
|
}
|
|
4333
|
-
turn.
|
|
4334
|
-
turn.
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4395
|
+
if (turn.activitySequence > candidate.arrivalSequence) {
|
|
4396
|
+
turn.deferredAgentEnd = undefined;
|
|
4397
|
+
this.cancelAmbiguousTerminal(turn);
|
|
4398
|
+
}
|
|
4399
|
+
}
|
|
4400
|
+
|
|
4401
|
+
private deferAmbiguousTerminal(turn: ActiveTurn, candidate: TerminalCandidate): void {
|
|
4402
|
+
if (turn.agentInvoked === false && turn.localOnlyEligible) return;
|
|
4403
|
+
this.cancelAmbiguousTerminal(turn);
|
|
4404
|
+
turn.deferredAgentEnd = candidate;
|
|
4405
|
+
turn.ambiguousTerminalTimer = this.scheduler.set(() => {
|
|
4406
|
+
turn.ambiguousTerminalTimer = undefined;
|
|
4407
|
+
void this.settleAmbiguousTerminal(turn, candidate);
|
|
4344
4408
|
}, AGENT_END_STATE_TIMEOUT_MS);
|
|
4345
4409
|
}
|
|
4346
4410
|
|
|
4347
|
-
private
|
|
4348
|
-
if (turn.
|
|
4349
|
-
this.scheduler.clear(turn.
|
|
4350
|
-
turn.
|
|
4411
|
+
private cancelAmbiguousTerminal(turn: ActiveTurn): void {
|
|
4412
|
+
if (turn.ambiguousTerminalTimer === undefined) return;
|
|
4413
|
+
this.scheduler.clear(turn.ambiguousTerminalTimer);
|
|
4414
|
+
turn.ambiguousTerminalTimer = undefined;
|
|
4415
|
+
}
|
|
4416
|
+
|
|
4417
|
+
private async settleAmbiguousTerminal(
|
|
4418
|
+
turn: ActiveTurn,
|
|
4419
|
+
candidate: TerminalCandidate,
|
|
4420
|
+
): Promise<void> {
|
|
4421
|
+
if (
|
|
4422
|
+
this.closed ||
|
|
4423
|
+
turn.terminal ||
|
|
4424
|
+
this.activeTurn !== turn ||
|
|
4425
|
+
turn.deferredAgentEnd !== candidate
|
|
4426
|
+
) {
|
|
4427
|
+
return;
|
|
4428
|
+
}
|
|
4429
|
+
await Promise.allSettled(turn.userLookups);
|
|
4430
|
+
this.refreshAmbiguousTerminal(turn);
|
|
4431
|
+
if (turn.terminal || this.activeTurn !== turn || turn.deferredAgentEnd !== candidate) return;
|
|
4432
|
+
const state = await this.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
|
|
4433
|
+
if (turn.terminal || this.activeTurn !== turn || turn.deferredAgentEnd !== candidate) return;
|
|
4434
|
+
if (!state) {
|
|
4435
|
+
this.handleRuntimeFailure("OMP agent_end state could not be confirmed");
|
|
4436
|
+
return;
|
|
4437
|
+
}
|
|
4438
|
+
turn.deferredAgentEnd = undefined;
|
|
4439
|
+
if (state.isStreaming || state.isCompacting) return;
|
|
4440
|
+
await this.finishTurn(turn, "failed", {
|
|
4441
|
+
message: "OMP unkeyed agent_end could not be correlated to the current prompt",
|
|
4442
|
+
});
|
|
4351
4443
|
}
|
|
4352
4444
|
|
|
4353
4445
|
private async completeLocalOnlyTurn(turn: ActiveTurn): Promise<void> {
|
|
@@ -4373,15 +4465,44 @@ export class OmpProviderSession {
|
|
|
4373
4465
|
await this.finishTurn(turn, "completed", undefined, false, false, true);
|
|
4374
4466
|
}
|
|
4375
4467
|
|
|
4376
|
-
private
|
|
4377
|
-
turn
|
|
4378
|
-
|
|
4379
|
-
|
|
4468
|
+
private resetAgentEndProbe(turn: ActiveTurn): void {
|
|
4469
|
+
turn.agentEndPending = false;
|
|
4470
|
+
turn.terminalizing = false;
|
|
4471
|
+
if (turn.agentEndRetryTimer !== undefined) {
|
|
4472
|
+
this.scheduler.clear(turn.agentEndRetryTimer);
|
|
4473
|
+
turn.agentEndRetryTimer = undefined;
|
|
4474
|
+
}
|
|
4475
|
+
if (turn.agentEndDeadlineTimer !== undefined) {
|
|
4476
|
+
this.scheduler.clear(turn.agentEndDeadlineTimer);
|
|
4477
|
+
turn.agentEndDeadlineTimer = undefined;
|
|
4478
|
+
}
|
|
4479
|
+
}
|
|
4480
|
+
private ignoreActiveTerminalCandidate(turn: ActiveTurn, candidate: TerminalCandidate): void {
|
|
4481
|
+
const replacement =
|
|
4482
|
+
turn.deferredAgentEnd && turn.deferredAgentEnd !== candidate
|
|
4483
|
+
? turn.deferredAgentEnd
|
|
4484
|
+
: undefined;
|
|
4485
|
+
this.resetAgentEndProbe(turn);
|
|
4486
|
+
turn.deferredAgentEnd = replacement;
|
|
4487
|
+
if (replacement && replacement.confidence !== "ambiguous") {
|
|
4488
|
+
turn.deferredAgentEnd = undefined;
|
|
4489
|
+
this.beginTerminalization(turn, replacement);
|
|
4490
|
+
return;
|
|
4491
|
+
}
|
|
4492
|
+
this.pollUsage(turn);
|
|
4493
|
+
}
|
|
4494
|
+
|
|
4495
|
+
private beginTerminalization(turn: ActiveTurn, candidate: TerminalCandidate): void {
|
|
4380
4496
|
if (turn.terminal || turn.terminalizing || turn.agentEndPending || this.activeTurn !== turn) {
|
|
4381
4497
|
return;
|
|
4382
4498
|
}
|
|
4383
|
-
if (!turn.interrupted && this.deferAgentEndForSubsessions(turn,
|
|
4499
|
+
if (!turn.interrupted && this.deferAgentEndForSubsessions(turn, candidate)) return;
|
|
4384
4500
|
turn.agentEndPending = true;
|
|
4501
|
+
if (turn.deferredAgentEnd?.confidence === "ambiguous") {
|
|
4502
|
+
turn.deferredAgentEnd = undefined;
|
|
4503
|
+
}
|
|
4504
|
+
turn.terminalizing = true;
|
|
4505
|
+
this.cancelAmbiguousTerminal(turn);
|
|
4385
4506
|
turn.usageSampleFloor = this.usageSequence + 1;
|
|
4386
4507
|
if (this.usageSample?.turn === turn && this.usageSample.sequence < turn.usageSampleFloor) {
|
|
4387
4508
|
this.usageSample = null;
|
|
@@ -4391,21 +4512,21 @@ export class OmpProviderSession {
|
|
|
4391
4512
|
turn.agentEndDeadlineTimer = undefined;
|
|
4392
4513
|
if (!turn.agentEndPending || turn.terminal || this.activeTurn !== turn) return;
|
|
4393
4514
|
if (
|
|
4394
|
-
|
|
4395
|
-
(
|
|
4515
|
+
candidate.confidence === "keyed" ||
|
|
4516
|
+
(candidate.confidence === "initial-turn" && !turn.userEchoObserved)
|
|
4396
4517
|
) {
|
|
4397
|
-
this.
|
|
4518
|
+
void this.completeAgentEnd(turn, candidate.event);
|
|
4398
4519
|
} else {
|
|
4399
|
-
|
|
4520
|
+
this.handleRuntimeFailure("OMP agent_end state could not be confirmed");
|
|
4400
4521
|
}
|
|
4401
4522
|
}, AGENT_END_SETTLE_MS);
|
|
4402
|
-
this.finishFromAgentEnd(turn,
|
|
4523
|
+
this.finishFromAgentEnd(turn, candidate);
|
|
4403
4524
|
}
|
|
4404
4525
|
|
|
4405
4526
|
private resumeAfterFailedSteer(turn: ActiveTurn): void {
|
|
4406
4527
|
if (turn.terminal || this.activeTurn !== turn || turn.steersInFlight > 0) return;
|
|
4407
4528
|
const deferred = turn.deferredAgentEnd;
|
|
4408
|
-
if (deferred) {
|
|
4529
|
+
if (deferred && deferred.confidence !== "ambiguous" && !this.hasTerminalConflict(turn)) {
|
|
4409
4530
|
turn.deferredAgentEnd = undefined;
|
|
4410
4531
|
this.beginTerminalization(turn, deferred);
|
|
4411
4532
|
return;
|
|
@@ -4414,13 +4535,11 @@ export class OmpProviderSession {
|
|
|
4414
4535
|
this.scheduleLocalOnlyCompletion(turn);
|
|
4415
4536
|
}
|
|
4416
4537
|
}
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
event: Extract<OmpRpcEvent, { type: "agent_end" }>,
|
|
4420
|
-
): boolean {
|
|
4538
|
+
|
|
4539
|
+
private deferAgentEndForSubsessions(turn: ActiveTurn, candidate: TerminalCandidate): boolean {
|
|
4421
4540
|
if (!this.subsessions?.hasActiveChildren()) return false;
|
|
4422
4541
|
turn.terminalizing = false;
|
|
4423
|
-
turn.deferredAgentEnd =
|
|
4542
|
+
turn.deferredAgentEnd = candidate;
|
|
4424
4543
|
void this.subsessions.reconcile(this.runtime).catch(() => {
|
|
4425
4544
|
if (!turn.terminal && this.activeTurn === turn) {
|
|
4426
4545
|
this.handleRuntimeFailure("OMP subagent reconciliation failed");
|
|
@@ -4431,31 +4550,22 @@ export class OmpProviderSession {
|
|
|
4431
4550
|
|
|
4432
4551
|
private resumeDeferredAgentEnd(): void {
|
|
4433
4552
|
const turn = this.activeTurn;
|
|
4434
|
-
if (
|
|
4435
|
-
!turn ||
|
|
4436
|
-
turn.terminal ||
|
|
4437
|
-
turn.terminalizing ||
|
|
4438
|
-
turn.steersInFlight > 0 ||
|
|
4439
|
-
this.subsessions?.hasActiveChildren()
|
|
4440
|
-
) {
|
|
4553
|
+
if (!turn || turn.terminal || turn.terminalizing || this.hasTerminalConflict(turn)) {
|
|
4441
4554
|
return;
|
|
4442
4555
|
}
|
|
4443
|
-
const
|
|
4444
|
-
if (!
|
|
4556
|
+
const candidate = turn.deferredAgentEnd;
|
|
4557
|
+
if (!candidate || candidate.confidence === "ambiguous") return;
|
|
4445
4558
|
turn.deferredAgentEnd = undefined;
|
|
4446
|
-
this.beginTerminalization(turn,
|
|
4559
|
+
this.beginTerminalization(turn, candidate);
|
|
4447
4560
|
}
|
|
4448
4561
|
|
|
4449
|
-
private finishFromAgentEnd(
|
|
4450
|
-
turn: ActiveTurn,
|
|
4451
|
-
event: Extract<OmpRpcEvent, { type: "agent_end" }>,
|
|
4452
|
-
): void {
|
|
4562
|
+
private finishFromAgentEnd(turn: ActiveTurn, candidate: TerminalCandidate): void {
|
|
4453
4563
|
if (!turn.agentEndPending || turn.terminal) return;
|
|
4454
4564
|
if (turn.agentEndCheck) {
|
|
4455
|
-
turn.deferredAgentEnd =
|
|
4565
|
+
turn.deferredAgentEnd = candidate;
|
|
4456
4566
|
return;
|
|
4457
4567
|
}
|
|
4458
|
-
const check = this.checkAgentEndState(turn,
|
|
4568
|
+
const check = this.checkAgentEndState(turn, candidate);
|
|
4459
4569
|
turn.agentEndCheck = check;
|
|
4460
4570
|
void check.finally(() => {
|
|
4461
4571
|
if (turn.agentEndCheck !== check) return;
|
|
@@ -4467,13 +4577,7 @@ export class OmpProviderSession {
|
|
|
4467
4577
|
});
|
|
4468
4578
|
}
|
|
4469
4579
|
|
|
4470
|
-
private async checkAgentEndState(
|
|
4471
|
-
turn: ActiveTurn,
|
|
4472
|
-
event: Extract<OmpRpcEvent, { type: "agent_end" }>,
|
|
4473
|
-
): Promise<void> {
|
|
4474
|
-
// Evidence arriving during this check cannot authorize an older terminal frame.
|
|
4475
|
-
const ownershipObserved =
|
|
4476
|
-
turn.terminalOwnershipEvidence || turn.bufferedTerminalOwnershipEvidence;
|
|
4580
|
+
private async checkAgentEndState(turn: ActiveTurn, candidate: TerminalCandidate): Promise<void> {
|
|
4477
4581
|
await Promise.allSettled(turn.userLookups);
|
|
4478
4582
|
if (!turn.agentEndPending || turn.terminal || this.activeTurn !== turn) return;
|
|
4479
4583
|
while (turn.userEchoes.length > 0) {
|
|
@@ -4482,40 +4586,42 @@ export class OmpProviderSession {
|
|
|
4482
4586
|
await Promise.allSettled(turn.userLookups);
|
|
4483
4587
|
if (!turn.agentEndPending || turn.terminal || this.activeTurn !== turn) return;
|
|
4484
4588
|
}
|
|
4589
|
+
if (
|
|
4590
|
+
!turn.interrupted &&
|
|
4591
|
+
candidate.confidence !== "keyed" &&
|
|
4592
|
+
this.hasTerminalConflict(turn, false)
|
|
4593
|
+
) {
|
|
4594
|
+
this.resetAgentEndProbe(turn);
|
|
4595
|
+
turn.deferredAgentEnd = candidate;
|
|
4596
|
+
this.pollUsage(turn);
|
|
4597
|
+
return;
|
|
4598
|
+
}
|
|
4485
4599
|
if (turn.interrupted) {
|
|
4486
|
-
await this.completeAgentEnd(turn, event);
|
|
4600
|
+
await this.completeAgentEnd(turn, candidate.event);
|
|
4487
4601
|
return;
|
|
4488
4602
|
}
|
|
4489
4603
|
const state = await this.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
|
|
4490
4604
|
if (!turn.agentEndPending || turn.terminal || this.activeTurn !== turn) return;
|
|
4491
4605
|
if (!turn.interrupted && this.subsessions?.hasActiveChildren()) {
|
|
4492
|
-
turn
|
|
4493
|
-
if (this.deferAgentEndForSubsessions(turn,
|
|
4606
|
+
this.resetAgentEndProbe(turn);
|
|
4607
|
+
if (this.deferAgentEndForSubsessions(turn, candidate)) return;
|
|
4608
|
+
}
|
|
4609
|
+
if (
|
|
4610
|
+
!turn.interrupted &&
|
|
4611
|
+
candidate.confidence !== "keyed" &&
|
|
4612
|
+
this.hasTerminalConflict(turn, false)
|
|
4613
|
+
) {
|
|
4614
|
+
this.resetAgentEndProbe(turn);
|
|
4615
|
+
turn.deferredAgentEnd = candidate;
|
|
4616
|
+
this.pollUsage(turn);
|
|
4617
|
+
return;
|
|
4494
4618
|
}
|
|
4495
4619
|
if (state) {
|
|
4496
4620
|
if (state.isStreaming || state.isCompacting) {
|
|
4497
|
-
|
|
4498
|
-
const message = "OMP agent_end arrived while the native runtime remained active";
|
|
4499
|
-
this.invalidateRuntime(message);
|
|
4500
|
-
await this.finishTurn(turn, "failed", { message }, true, true);
|
|
4501
|
-
return;
|
|
4502
|
-
}
|
|
4503
|
-
turn.agentEndPending = false;
|
|
4504
|
-
turn.terminalizing = false;
|
|
4505
|
-
turn.deferredAgentEnd = undefined;
|
|
4506
|
-
return;
|
|
4507
|
-
}
|
|
4508
|
-
if (
|
|
4509
|
-
turn.terminalOwnershipRequired &&
|
|
4510
|
-
(!ownershipObserved || !turn.terminalOwnershipEvidence)
|
|
4511
|
-
) {
|
|
4512
|
-
turn.agentEndPending = false;
|
|
4513
|
-
turn.terminalizing = false;
|
|
4514
|
-
turn.deferredAgentEnd = undefined;
|
|
4515
|
-
this.scheduleTerminalOwnershipTimeout(turn);
|
|
4621
|
+
this.ignoreActiveTerminalCandidate(turn, candidate);
|
|
4516
4622
|
return;
|
|
4517
4623
|
}
|
|
4518
|
-
await this.completeAgentEnd(turn, event, true);
|
|
4624
|
+
await this.completeAgentEnd(turn, candidate.event, true);
|
|
4519
4625
|
return;
|
|
4520
4626
|
}
|
|
4521
4627
|
if (turn.userEchoObserved) {
|
|
@@ -4525,7 +4631,7 @@ export class OmpProviderSession {
|
|
|
4525
4631
|
if (turn.agentEndRetryTimer === undefined) {
|
|
4526
4632
|
turn.agentEndRetryTimer = this.scheduler.set(() => {
|
|
4527
4633
|
turn.agentEndRetryTimer = undefined;
|
|
4528
|
-
this.finishFromAgentEnd(turn,
|
|
4634
|
+
this.finishFromAgentEnd(turn, candidate);
|
|
4529
4635
|
}, USAGE_POLL_MS);
|
|
4530
4636
|
}
|
|
4531
4637
|
}
|
|
@@ -4556,8 +4662,11 @@ export class OmpProviderSession {
|
|
|
4556
4662
|
)
|
|
4557
4663
|
return;
|
|
4558
4664
|
if (!turn.interrupted && state && (state.isStreaming || state.isCompacting)) {
|
|
4559
|
-
turn
|
|
4560
|
-
|
|
4665
|
+
this.ignoreActiveTerminalCandidate(turn, {
|
|
4666
|
+
event,
|
|
4667
|
+
arrivalSequence: turn.activitySequence,
|
|
4668
|
+
confidence: event.requestId === undefined ? "ordered-legacy" : "keyed",
|
|
4669
|
+
});
|
|
4561
4670
|
return;
|
|
4562
4671
|
}
|
|
4563
4672
|
if (state && !state.isStreaming && !state.isCompacting) {
|
|
@@ -4623,6 +4732,7 @@ export class OmpProviderSession {
|
|
|
4623
4732
|
turn.starting = false;
|
|
4624
4733
|
turn.terminal = true;
|
|
4625
4734
|
this.stopUsagePoll(turn);
|
|
4735
|
+
this.cancelAmbiguousTerminal(turn);
|
|
4626
4736
|
this.resolveTurnPermissions(turn.turnId);
|
|
4627
4737
|
this.projector.finishTurn(turn.turnId);
|
|
4628
4738
|
if (this.activeTurn === turn) this.activeTurn = null;
|
|
@@ -4677,7 +4787,7 @@ export class OmpProviderSession {
|
|
|
4677
4787
|
turn.manualCompactionPending = false;
|
|
4678
4788
|
turn.agentEndPending = false;
|
|
4679
4789
|
this.cancelLocalOnlyCompletion(turn);
|
|
4680
|
-
this.
|
|
4790
|
+
this.cancelAmbiguousTerminal(turn);
|
|
4681
4791
|
this.stopUsagePoll(turn);
|
|
4682
4792
|
if (turn.manualCompactionDeadlineTimer !== undefined) {
|
|
4683
4793
|
this.scheduler.clear(turn.manualCompactionDeadlineTimer);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type ExternalUrlOpener = (url: string) => Promise<void>;
|
|
2
|
+
|
|
3
|
+
export function selectExternalUrlOpener(
|
|
4
|
+
paseoOpener: ExternalUrlOpener | undefined,
|
|
5
|
+
fallback: ExternalUrlOpener,
|
|
6
|
+
): ExternalUrlOpener {
|
|
7
|
+
return paseoOpener ?? fallback;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function validatedHttpUrl(value: string): string {
|
|
11
|
+
let url: URL;
|
|
12
|
+
try {
|
|
13
|
+
url = new URL(value);
|
|
14
|
+
} catch {
|
|
15
|
+
throw new Error("Only absolute HTTP(S) URLs are supported.");
|
|
16
|
+
}
|
|
17
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
18
|
+
throw new Error("Only absolute HTTP(S) URLs are supported.");
|
|
19
|
+
}
|
|
20
|
+
return url.href;
|
|
21
|
+
}
|