@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.
@@ -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)) setRawNames("");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omercnet/paseo-omp",
3
- "version": "5.1.1-next.335.1",
3
+ "version": "5.1.1-next.336.1",
4
4
  "type": "module",
5
5
  "description": "Paseo integration for OMP, including its direct provider and workspace tooling.",
6
6
  "license": "MIT",
@@ -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
- const MAX_REGISTERED_SESSIONS = 32;
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.335.1"; // x-release-please-version
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 === token) failedCleanup.delete(sessionId);
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 (sessions.size + opening.size + failedCleanup.size >= MAX_CONNECTION_SESSIONS) {
910
- requestFailure(input.requestId, new OmpPublicError("OMP session limit reached"));
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().then(disposeConnection);
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>;