@omercnet/paseo-omp 5.1.1-next.335.1 → 5.1.1-next.336.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/client/provider-launch-settings.tsx +46 -2
- package/index.server.ts +9 -0
- package/package.json +1 -1
- package/server/mcp-browser.ts +3 -1
- package/server/package-version.ts +1 -1
- package/server/provider/connection.ts +25 -5
- package/server/provider/registration.ts +6 -0
- package/server/provider/session-limit.ts +32 -0
- package/shared/provider-launch-settings.ts +4 -0
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
9
9
|
import { Text, View } from "react-native";
|
|
10
10
|
import {
|
|
11
|
+
MAX_SESSIONS_UPPER_BOUND,
|
|
11
12
|
providerLaunchSettings,
|
|
12
13
|
providerLaunchSettingsSchema,
|
|
13
14
|
} from "../shared/provider-launch-settings";
|
|
@@ -23,6 +24,11 @@ function ProviderLaunchSettingsControls({ settings }: { settings: ReadySettings
|
|
|
23
24
|
useEffect(() => {
|
|
24
25
|
setRawNames(savedNames);
|
|
25
26
|
}, [savedNames]);
|
|
27
|
+
const savedMaxSessions = String(settings.values.maxSessions);
|
|
28
|
+
const [rawMaxSessions, setRawMaxSessions] = useState(() => savedMaxSessions);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
setRawMaxSessions(savedMaxSessions);
|
|
31
|
+
}, [savedMaxSessions]);
|
|
26
32
|
const inheritEnv = [
|
|
27
33
|
...new Set(
|
|
28
34
|
rawNames
|
|
@@ -31,7 +37,7 @@ function ProviderLaunchSettingsControls({ settings }: { settings: ReadySettings
|
|
|
31
37
|
.filter(Boolean),
|
|
32
38
|
),
|
|
33
39
|
];
|
|
34
|
-
const next = providerLaunchSettingsSchema.safeParse({ inheritEnv });
|
|
40
|
+
const next = providerLaunchSettingsSchema.safeParse({ ...settings.values, inheritEnv });
|
|
35
41
|
const error = next.success
|
|
36
42
|
? null
|
|
37
43
|
: (next.error.issues[0]?.message ?? "Enter valid variable names.");
|
|
@@ -39,6 +45,14 @@ function ProviderLaunchSettingsControls({ settings }: { settings: ReadySettings
|
|
|
39
45
|
if (!next.success) return;
|
|
40
46
|
await settings.save(next.data, settings.revision);
|
|
41
47
|
}, [next, settings]);
|
|
48
|
+
const nextMaxSessions = providerLaunchSettingsSchema.safeParse({
|
|
49
|
+
...settings.values,
|
|
50
|
+
maxSessions: Number(rawMaxSessions.trim()),
|
|
51
|
+
});
|
|
52
|
+
const saveMaxSessions = useCallback(async () => {
|
|
53
|
+
if (!nextMaxSessions.success) return;
|
|
54
|
+
await settings.save(nextMaxSessions.data, settings.revision);
|
|
55
|
+
}, [nextMaxSessions, settings]);
|
|
42
56
|
|
|
43
57
|
return (
|
|
44
58
|
<View style={{ gap: 12 }}>
|
|
@@ -69,7 +83,9 @@ function ProviderLaunchSettingsControls({ settings }: { settings: ReadySettings
|
|
|
69
83
|
actionLabel="Clear"
|
|
70
84
|
disabled={settings.saving || settings.values.inheritEnv.length === 0}
|
|
71
85
|
onPress={async () => {
|
|
72
|
-
if (await settings.save({ inheritEnv: [] }, settings.revision))
|
|
86
|
+
if (await settings.save({ ...settings.values, inheritEnv: [] }, settings.revision)) {
|
|
87
|
+
setRawNames("");
|
|
88
|
+
}
|
|
73
89
|
}}
|
|
74
90
|
/>
|
|
75
91
|
{settings.saveError ? (
|
|
@@ -83,6 +99,34 @@ function ProviderLaunchSettingsControls({ settings }: { settings: ReadySettings
|
|
|
83
99
|
) : null}
|
|
84
100
|
</SettingsCard>
|
|
85
101
|
</SettingsSection>
|
|
102
|
+
<SettingsSection
|
|
103
|
+
title="Session limit"
|
|
104
|
+
info="Applies to all OMP Plugin agents on this host together, including idle ones."
|
|
105
|
+
>
|
|
106
|
+
<SettingsCard>
|
|
107
|
+
<SettingsInput
|
|
108
|
+
key={savedMaxSessions}
|
|
109
|
+
label="Open sessions"
|
|
110
|
+
hint="New OMP Plugin agents are refused while this many sessions are open. Archive idle agents to free sessions."
|
|
111
|
+
initialValue={savedMaxSessions}
|
|
112
|
+
onChangeText={setRawMaxSessions}
|
|
113
|
+
error={
|
|
114
|
+
nextMaxSessions.success
|
|
115
|
+
? undefined
|
|
116
|
+
: `Enter a whole number from 1 to ${MAX_SESSIONS_UPPER_BOUND}.`
|
|
117
|
+
}
|
|
118
|
+
disabled={settings.saving}
|
|
119
|
+
/>
|
|
120
|
+
<SettingsAction
|
|
121
|
+
label="Save session limit"
|
|
122
|
+
actionLabel="Save"
|
|
123
|
+
disabled={
|
|
124
|
+
settings.saving || rawMaxSessions === savedMaxSessions || !nextMaxSessions.success
|
|
125
|
+
}
|
|
126
|
+
onPress={() => void saveMaxSessions()}
|
|
127
|
+
/>
|
|
128
|
+
</SettingsCard>
|
|
129
|
+
</SettingsSection>
|
|
86
130
|
</View>
|
|
87
131
|
);
|
|
88
132
|
}
|
package/index.server.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
} from "./server/provider/profile-providers";
|
|
26
26
|
import { createOmpProvider } from "./server/provider/registration";
|
|
27
27
|
import { OmpPublicError } from "./server/provider/security";
|
|
28
|
+
import { OmpSessionLimit } from "./server/provider/session-limit";
|
|
28
29
|
import { resolveGetOmpProviderHealth } from "./server/provider-diagnostics";
|
|
29
30
|
import { resolveListOmpQuotas } from "./server/quota";
|
|
30
31
|
import { resolveListOmpSessions } from "./server/sessions";
|
|
@@ -62,6 +63,11 @@ export default function contribute(server: PluginServerContext) {
|
|
|
62
63
|
throw new OmpPublicError("OMP host launch settings are unavailable");
|
|
63
64
|
};
|
|
64
65
|
const browserAuthorizationRegistry = new OmpBrowserAuthorizationRegistry();
|
|
66
|
+
const sessionLimit = new OmpSessionLimit(async () => {
|
|
67
|
+
const current = await launchSettings.read();
|
|
68
|
+
return current.status === "ready" ? current.values.maxSessions : undefined;
|
|
69
|
+
});
|
|
70
|
+
const removeSessionLimitListener = launchSettings.subscribe(() => sessionLimit.refresh());
|
|
65
71
|
const protocolViolations = new OmpProtocolViolationCollector();
|
|
66
72
|
const operationalFailures = new OmpOperationalFailureCollector();
|
|
67
73
|
const profiles = discoverOmpProfilesSync();
|
|
@@ -70,6 +76,7 @@ export default function contribute(server: PluginServerContext) {
|
|
|
70
76
|
server.registerProvider(
|
|
71
77
|
createProfileOmpProvider(profile, {
|
|
72
78
|
browserAuthorizationRegistry,
|
|
79
|
+
sessionLimit,
|
|
73
80
|
reportProtocolViolation: protocolViolations.report,
|
|
74
81
|
reportOperationalFailure: operationalFailures.report,
|
|
75
82
|
resolveHostInheritEnv,
|
|
@@ -104,6 +111,7 @@ export default function contribute(server: PluginServerContext) {
|
|
|
104
111
|
server.registerProvider(
|
|
105
112
|
createOmpProvider({
|
|
106
113
|
browserAuthorizationRegistry,
|
|
114
|
+
sessionLimit,
|
|
107
115
|
reportProtocolViolation: protocolViolations.report,
|
|
108
116
|
reportOperationalFailure: operationalFailures.report,
|
|
109
117
|
resolveHostInheritEnv,
|
|
@@ -111,6 +119,7 @@ export default function contribute(server: PluginServerContext) {
|
|
|
111
119
|
);
|
|
112
120
|
return () => {
|
|
113
121
|
browserAuthorizationRegistry.clear();
|
|
122
|
+
removeSessionLimitListener();
|
|
114
123
|
removeSessionOpenHook();
|
|
115
124
|
};
|
|
116
125
|
}
|
package/package.json
CHANGED
package/server/mcp-browser.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import type { RpcInput } from "@getpaseo/plugin";
|
|
3
3
|
import type { openOmpMcpAuthorizationInPaseoBrowser } from "../shared/mcp";
|
|
4
|
+
import { MAX_SESSIONS_UPPER_BOUND } from "../shared/provider-launch-settings";
|
|
4
5
|
import { OmpPublicError } from "./provider/security";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
// One registration per live session. The shared session limit refuses sessions first.
|
|
8
|
+
const MAX_REGISTERED_SESSIONS = MAX_SESSIONS_UPPER_BOUND;
|
|
7
9
|
const MAX_AUTHORIZATIONS_PER_SESSION = 16;
|
|
8
10
|
|
|
9
11
|
type BrowserAuthorizationOpener = (url: string) => Promise<void>;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Release Please keeps this bundle-safe constant aligned with package.json. */
|
|
2
|
-
export const PASEO_OMP_PACKAGE_VERSION = "5.1.1-next.
|
|
2
|
+
export const PASEO_OMP_PACKAGE_VERSION = "5.1.1-next.336.1"; // x-release-please-version
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
} from "./security";
|
|
35
35
|
import { type OmpOpenedSession, OmpProviderSession, ompPersistenceSessionId } from "./session";
|
|
36
36
|
import { OmpHistorySession } from "./session-history";
|
|
37
|
+
import { OmpSessionLimit } from "./session-limit";
|
|
37
38
|
import { isOmpHistoryOnlyOpen } from "./session-purpose";
|
|
38
39
|
|
|
39
40
|
type ProviderConfigurationCompat = {
|
|
@@ -69,7 +70,6 @@ const SUPPORTED_INPUTS: Readonly<Record<string, true>> = {
|
|
|
69
70
|
"session.can_stop_runtime": true,
|
|
70
71
|
"session.close": true,
|
|
71
72
|
};
|
|
72
|
-
const MAX_CONNECTION_SESSIONS = 32;
|
|
73
73
|
const MAX_ACTIVE_OPERATIONS = 128;
|
|
74
74
|
const MAX_PROVIDER_INPUT_BYTES = 2 * 1024 * 1024;
|
|
75
75
|
const MAX_NESTED_OPTION_BYTES = 256 * 1024;
|
|
@@ -723,6 +723,7 @@ export function createOmpConnection(
|
|
|
723
723
|
console.error("OMP provider failure", diagnostic),
|
|
724
724
|
reportOperationalFailure: OmpOperationalFailureReporter = () => {},
|
|
725
725
|
resolveHostInheritEnv?: () => Promise<readonly string[]>,
|
|
726
|
+
sessionLimit = new OmpSessionLimit(),
|
|
726
727
|
): ProviderConnection {
|
|
727
728
|
const errorDetails = (error: unknown, fallback: string): { message: string } => {
|
|
728
729
|
if (isOmpPublicError(error)) return { message: error.message };
|
|
@@ -767,6 +768,13 @@ export function createOmpConnection(
|
|
|
767
768
|
string,
|
|
768
769
|
{ token: symbol; nativeSessionId?: string; cleanup?: Promise<void> }
|
|
769
770
|
>();
|
|
771
|
+
// One session id is held by `sessions`, `opening` and `failedCleanup` at different moments, and
|
|
772
|
+
// by two of them at once while it opens or fails. After a failed close only unresolved cleanup
|
|
773
|
+
// stays counted, until it drains.
|
|
774
|
+
const untrackSessions = sessionLimit.track(() => {
|
|
775
|
+
if (closed) return failedCleanup.size;
|
|
776
|
+
return new Set([...sessions.keys(), ...opening.keys(), ...failedCleanup.keys()]).size;
|
|
777
|
+
});
|
|
770
778
|
const shutdown = new AbortController();
|
|
771
779
|
let catalogCleanup: Promise<void> | null = null;
|
|
772
780
|
const activeOperations = new Set<Promise<void>>();
|
|
@@ -809,7 +817,9 @@ export function createOmpConnection(
|
|
|
809
817
|
...(quarantinedNativeSessionId ? { nativeSessionId: quarantinedNativeSessionId } : {}),
|
|
810
818
|
});
|
|
811
819
|
nativeReservations.quarantine(quarantinedNativeSessionId, token, cleanup, () => {
|
|
812
|
-
if (failedCleanup.get(sessionId)?.token
|
|
820
|
+
if (failedCleanup.get(sessionId)?.token !== token) return;
|
|
821
|
+
failedCleanup.delete(sessionId);
|
|
822
|
+
if (closed && failedCleanup.size === 0) untrackSessions();
|
|
813
823
|
});
|
|
814
824
|
};
|
|
815
825
|
|
|
@@ -906,8 +916,13 @@ export function createOmpConnection(
|
|
|
906
916
|
requestFailure(input.requestId, new OmpPublicError("OMP session already exists"));
|
|
907
917
|
return;
|
|
908
918
|
}
|
|
909
|
-
if (
|
|
910
|
-
requestFailure(
|
|
919
|
+
if (sessionLimit.openSessions >= sessionLimit.maxSessions) {
|
|
920
|
+
requestFailure(
|
|
921
|
+
input.requestId,
|
|
922
|
+
new OmpPublicError(
|
|
923
|
+
`OMP session limit reached (${sessionLimit.maxSessions} open sessions). Archive idle agents or raise the session limit in OMP → Plugin.`,
|
|
924
|
+
),
|
|
925
|
+
);
|
|
911
926
|
return;
|
|
912
927
|
}
|
|
913
928
|
const token = Symbol(input.sessionId);
|
|
@@ -1313,7 +1328,12 @@ export function createOmpConnection(
|
|
|
1313
1328
|
return () => listeners.delete(listener);
|
|
1314
1329
|
},
|
|
1315
1330
|
close() {
|
|
1316
|
-
closePromise ??= Promise.resolve()
|
|
1331
|
+
closePromise ??= Promise.resolve()
|
|
1332
|
+
.then(disposeConnection)
|
|
1333
|
+
.then(untrackSessions, (error: unknown) => {
|
|
1334
|
+
if (failedCleanup.size === 0) untrackSessions();
|
|
1335
|
+
throw error;
|
|
1336
|
+
});
|
|
1317
1337
|
return closePromise;
|
|
1318
1338
|
},
|
|
1319
1339
|
};
|
|
@@ -12,6 +12,7 @@ import { OmpRpcRuntime, type OmpRuntime } from "./omp-rpc";
|
|
|
12
12
|
import type { OmpProtocolViolationDiagnostic } from "./omp-rpc-protocol";
|
|
13
13
|
import { parseOmpProviderOptions } from "./provider-options";
|
|
14
14
|
import { boundedJsonBytes } from "./security";
|
|
15
|
+
import { OmpSessionLimit } from "./session-limit";
|
|
15
16
|
import { OmpProviderOptionsSchema } from "./settings";
|
|
16
17
|
import type { OmpTimelineScheduler } from "./timeline-projector";
|
|
17
18
|
|
|
@@ -121,6 +122,8 @@ export interface OmpProviderOptions {
|
|
|
121
122
|
mcpInitializationTimeoutMs?: number;
|
|
122
123
|
mcpConnector?: OmpMcpConnector;
|
|
123
124
|
browserAuthorizationRegistry?: OmpBrowserAuthorizationRegistry;
|
|
125
|
+
/** Shared by every provider of one plugin instance; each provider has its own otherwise. */
|
|
126
|
+
sessionLimit?: OmpSessionLimit;
|
|
124
127
|
reportProtocolViolation?: (diagnostic: OmpProtocolViolationDiagnostic) => void | Promise<void>;
|
|
125
128
|
reportOperationalFailure?: OmpOperationalFailureReporter;
|
|
126
129
|
availabilityProbe?: (
|
|
@@ -150,6 +153,7 @@ export function createOmpProvider(options: OmpProviderOptions = {}): ProviderReg
|
|
|
150
153
|
reportProtocolViolation: options.reportProtocolViolation,
|
|
151
154
|
});
|
|
152
155
|
const nativeReservations = new OmpNativeSessionReservations();
|
|
156
|
+
const sessionLimit = options.sessionLimit ?? new OmpSessionLimit();
|
|
153
157
|
const checkAvailability: NonNullable<ProviderRegistrationCompat["checkAvailability"]> = async (
|
|
154
158
|
catalogOptions,
|
|
155
159
|
context,
|
|
@@ -216,6 +220,7 @@ export function createOmpProvider(options: OmpProviderOptions = {}): ProviderReg
|
|
|
216
220
|
const capabilities = CAPABILITIES.filter((capability) =>
|
|
217
221
|
requestedCapabilities.has(capability),
|
|
218
222
|
);
|
|
223
|
+
await sessionLimit.refresh();
|
|
219
224
|
return createOmpConnection(
|
|
220
225
|
runtime,
|
|
221
226
|
capabilities,
|
|
@@ -229,6 +234,7 @@ export function createOmpProvider(options: OmpProviderOptions = {}): ProviderReg
|
|
|
229
234
|
undefined,
|
|
230
235
|
options.reportOperationalFailure,
|
|
231
236
|
options.resolveHostInheritEnv,
|
|
237
|
+
sessionLimit,
|
|
232
238
|
);
|
|
233
239
|
},
|
|
234
240
|
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { DEFAULT_MAX_SESSIONS } from "../../shared/provider-launch-settings";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Counts open sessions across every provider connection that shares this instance. The plugin
|
|
5
|
+
* shares one instance between all of its OMP providers, so the limit applies to the whole host.
|
|
6
|
+
*/
|
|
7
|
+
export class OmpSessionLimit {
|
|
8
|
+
private readonly counts = new Set<() => number>();
|
|
9
|
+
maxSessions = DEFAULT_MAX_SESSIONS;
|
|
10
|
+
|
|
11
|
+
/** `readConfigured` returns the configured limit, or `undefined` to keep the current one. */
|
|
12
|
+
constructor(private readonly readConfigured?: () => Promise<number | undefined>) {}
|
|
13
|
+
|
|
14
|
+
async refresh(): Promise<void> {
|
|
15
|
+
const configured = await this.readConfigured?.();
|
|
16
|
+
if (configured !== undefined) this.maxSessions = configured;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Adds a connection's open sessions to the total until the returned function runs. */
|
|
20
|
+
track(count: () => number): () => void {
|
|
21
|
+
this.counts.add(count);
|
|
22
|
+
return () => {
|
|
23
|
+
this.counts.delete(count);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get openSessions(): number {
|
|
28
|
+
let total = 0;
|
|
29
|
+
for (const count of this.counts) total += count();
|
|
30
|
+
return total;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -3,9 +3,13 @@ import { z } from "zod";
|
|
|
3
3
|
|
|
4
4
|
export const MAX_INHERITED_ENVIRONMENT_NAMES = 256;
|
|
5
5
|
export const environmentNameSchema = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]{0,127}$/u);
|
|
6
|
+
export const DEFAULT_MAX_SESSIONS = 32;
|
|
7
|
+
// Every allowed session can open at once without reaching the 128 in-flight operations bound.
|
|
8
|
+
export const MAX_SESSIONS_UPPER_BOUND = 64;
|
|
6
9
|
|
|
7
10
|
export const providerLaunchSettingsSchema = z.object({
|
|
8
11
|
inheritEnv: z.array(environmentNameSchema).max(MAX_INHERITED_ENVIRONMENT_NAMES).default([]),
|
|
12
|
+
maxSessions: z.number().int().min(1).max(MAX_SESSIONS_UPPER_BOUND).default(DEFAULT_MAX_SESSIONS),
|
|
9
13
|
});
|
|
10
14
|
|
|
11
15
|
export type ProviderLaunchSettings = z.infer<typeof providerLaunchSettingsSchema>;
|