@granular-software/sdk 0.4.30 → 0.4.32

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/dist/index.js CHANGED
@@ -4005,7 +4005,11 @@ var WSClient = class {
4005
4005
  return null;
4006
4006
  }
4007
4007
  try {
4008
- const payloadRaw = this.decodeBase64Url(parts[1]);
4008
+ const payloadSegment = parts[1];
4009
+ if (!payloadSegment) {
4010
+ return null;
4011
+ }
4012
+ const payloadRaw = this.decodeBase64Url(payloadSegment);
4009
4013
  const payload = JSON.parse(payloadRaw);
4010
4014
  if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp)) {
4011
4015
  return null;
@@ -4669,27 +4673,27 @@ var Session = class {
4669
4673
  }
4670
4674
  async publishTools(tools, revision = "1.0.0") {
4671
4675
  throw new Error(
4672
- "Environment-scoped effect publication was removed. Declare effects in the manifest and register live handlers with granular.registerEffects(environment.sandboxId, effects)."
4676
+ "Environment-scoped effect publication was removed. Declare effects in the manifest and register live handlers with granular.ontology(environment.sandboxId).effects.registerMany(effects)."
4673
4677
  );
4674
4678
  }
4675
4679
  async publishEffect(effect) {
4676
4680
  throw new Error(
4677
- "Environment-scoped effect publication was removed. Use granular.registerEffect(environment.sandboxId, effect)."
4681
+ "Environment-scoped effect publication was removed. Use granular.ontology(environment.sandboxId).effects.register(effect)."
4678
4682
  );
4679
4683
  }
4680
4684
  async publishEffects(effects) {
4681
4685
  throw new Error(
4682
- "Environment-scoped effect publication was removed. Use granular.registerEffects(environment.sandboxId, effects)."
4686
+ "Environment-scoped effect publication was removed. Use granular.ontology(environment.sandboxId).effects.registerMany(effects)."
4683
4687
  );
4684
4688
  }
4685
4689
  async unpublishEffect(name) {
4686
4690
  throw new Error(
4687
- "Environment-scoped effect publication was removed. Use granular.unregisterEffect(environment.sandboxId, effectName)."
4691
+ "Environment-scoped effect publication was removed. Use granular.ontology(environment.sandboxId).effects.unregister(effectName)."
4688
4692
  );
4689
4693
  }
4690
4694
  async unpublishAllEffects() {
4691
4695
  throw new Error(
4692
- "Environment-scoped effect publication was removed. Use granular.unregisterAllEffects(environment.sandboxId)."
4696
+ "Environment-scoped effect publication was removed. Use granular.ontology(environment.sandboxId).effects.clear()."
4693
4697
  );
4694
4698
  }
4695
4699
  /**
@@ -5082,6 +5086,7 @@ import { ${allImports} } from "./sandbox-tools";
5082
5086
  this.eventListeners.set(event, []);
5083
5087
  }
5084
5088
  this.eventListeners.get(event).push(handler);
5089
+ return () => this.off(event, handler);
5085
5090
  }
5086
5091
  /**
5087
5092
  * Unsubscribe from session events
@@ -5538,6 +5543,16 @@ var JobImplementation = class {
5538
5543
  handler(message);
5539
5544
  }
5540
5545
  }
5546
+ return () => {
5547
+ const handlers = this.eventListeners.get(event);
5548
+ if (!handlers) {
5549
+ return;
5550
+ }
5551
+ this.eventListeners.set(
5552
+ event,
5553
+ handlers.filter((current) => current !== handler)
5554
+ );
5555
+ };
5541
5556
  }
5542
5557
  replayAgentMessage(message) {
5543
5558
  this.captureAgentMessage(message);
@@ -11713,6 +11728,22 @@ function normalizeHeapSnapshot(raw) {
11713
11728
  updatedAt: typeof heap.updatedAt === "number" ? heap.updatedAt : Date.now()
11714
11729
  };
11715
11730
  }
11731
+ function deriveRuntimeBaseUrl(apiEndpoint) {
11732
+ try {
11733
+ const endpoint = new URL(apiEndpoint);
11734
+ const graphqlSuffix = "/orchestrator/graphql";
11735
+ if (endpoint.pathname.endsWith(graphqlSuffix)) {
11736
+ endpoint.pathname = endpoint.pathname.slice(0, -graphqlSuffix.length);
11737
+ } else if (endpoint.pathname.endsWith("/graphql")) {
11738
+ endpoint.pathname = endpoint.pathname.slice(0, -"/graphql".length);
11739
+ }
11740
+ endpoint.search = "";
11741
+ endpoint.hash = "";
11742
+ return endpoint.toString().replace(/\/$/, "");
11743
+ } catch {
11744
+ return apiEndpoint.replace(/\/orchestrator\/graphql$/, "").replace(/\/$/, "");
11745
+ }
11746
+ }
11716
11747
  function normalizeSubject(subject) {
11717
11748
  const granularId = subject.granularId || subject.subjectId;
11718
11749
  const userId = subject.userId || subject.identityId || granularId;
@@ -11752,12 +11783,13 @@ function normalizeEnvironmentData(environment) {
11752
11783
  tracking: environment.tracking || buildPolicy
11753
11784
  };
11754
11785
  }
11755
- var Environment = class extends Session {
11786
+ var Environment = class {
11787
+ granular;
11756
11788
  envData;
11757
11789
  _apiKey;
11758
11790
  _apiEndpoint;
11759
- constructor(client, envData, clientId, apiKey, apiEndpoint) {
11760
- super(client, clientId);
11791
+ constructor(granular, envData, apiKey, apiEndpoint) {
11792
+ this.granular = granular;
11761
11793
  this.envData = envData;
11762
11794
  this._apiKey = apiKey;
11763
11795
  this._apiEndpoint = apiEndpoint;
@@ -11798,35 +11830,126 @@ var Environment = class extends Session {
11798
11830
  get permissionProfileId() {
11799
11831
  return this.envData.permissionProfileId;
11800
11832
  }
11833
+ /** The current build policy backing this environment */
11834
+ get buildPolicy() {
11835
+ return this.envData.buildPolicy;
11836
+ }
11837
+ /** The current update state relative to the followed tag */
11838
+ get updateState() {
11839
+ return this.envData.updateState;
11840
+ }
11841
+ /** Convenience flag for whether this environment trails the current tag target */
11842
+ get isOutdated() {
11843
+ return this.envData.updateState === "update_available";
11844
+ }
11845
+ /** The followed tag name when this environment is tag-tracked */
11846
+ get tag() {
11847
+ return this.envData.tag?.name || this.envData.buildPolicy.tagName || null;
11848
+ }
11801
11849
  /** The GraphQL API endpoint URL */
11802
11850
  get apiEndpoint() {
11803
11851
  return this._apiEndpoint;
11804
11852
  }
11853
+ /** Internal auth token used for control-plane and runtime fallback requests */
11854
+ get authToken() {
11855
+ return this._apiKey;
11856
+ }
11857
+ /** Base runtime URL derived from the GraphQL endpoint */
11858
+ get runtimeBaseUrl() {
11859
+ return this.getRuntimeBaseUrl();
11860
+ }
11861
+ get sessions() {
11862
+ return {
11863
+ list: async (options) => this.listSessions(options?.status || "active"),
11864
+ create: async (options) => this.createSession(options),
11865
+ connect: async (sessionId, options) => this.connectSession(sessionId, options),
11866
+ reopen: async (sessionId, options) => this.reopenSession(sessionId, options),
11867
+ close: async (sessionId, session) => this.closeSession(sessionId, session)
11868
+ };
11869
+ }
11870
+ get data() {
11871
+ return {
11872
+ record: async (record) => this.recordObject(record),
11873
+ recordMany: async (records, options) => this.recordObjects(records, options),
11874
+ import: async (records, options) => this.enqueueRecordImport(records, options),
11875
+ listImports: async (status) => this.listRecordImports(status),
11876
+ getImport: async (importId) => this.getRecordImport(importId),
11877
+ getImportSummary: async () => this.getRecordImportSummary(),
11878
+ cancelImport: async (importId) => this.cancelRecordImport(importId),
11879
+ getAwaitingCount: async () => this.getAwaitingRecordCount()
11880
+ };
11881
+ }
11882
+ get feedback() {
11883
+ return {
11884
+ list: async () => this.listFeedback()
11885
+ };
11886
+ }
11805
11887
  /**
11806
- * Return a plain JS snapshot of the synced session heap.
11807
- *
11808
- * The heap lives in the Automerge document, so this method does not perform
11809
- * any extra network roundtrip.
11888
+ * Sessionless environments do not own a live transport, so disconnecting the
11889
+ * environment handle itself is a no-op. This keeps the public surface
11890
+ * symmetric with `EnvironmentSession.disconnect()` and lets callers always
11891
+ * clean up safely without tracking whether they currently hold an environment
11892
+ * or a session.
11810
11893
  */
11811
- getHeap() {
11812
- const doc = this.document;
11813
- return normalizeHeapSnapshot(doc?.heap);
11894
+ async disconnect() {
11814
11895
  }
11815
- getRuntimeBaseUrl() {
11816
- try {
11817
- const endpoint = new URL(this._apiEndpoint);
11818
- const graphqlSuffix = "/orchestrator/graphql";
11819
- if (endpoint.pathname.endsWith(graphqlSuffix)) {
11820
- endpoint.pathname = endpoint.pathname.slice(0, -graphqlSuffix.length);
11821
- } else if (endpoint.pathname.endsWith("/graphql")) {
11822
- endpoint.pathname = endpoint.pathname.slice(0, -"/graphql".length);
11823
- }
11824
- endpoint.search = "";
11825
- endpoint.hash = "";
11826
- return endpoint.toString().replace(/\/$/, "");
11827
- } catch {
11828
- return this._apiEndpoint.replace(/\/orchestrator\/graphql$/, "").replace(/\/$/, "");
11896
+ async listSessions(status = "active") {
11897
+ if (status === "all") {
11898
+ const [active, closed] = await Promise.all([
11899
+ this.granular.listOpenSessions({ environmentId: this.environmentId }),
11900
+ this.granular.listClosedSessions({ environmentId: this.environmentId })
11901
+ ]);
11902
+ return [...active, ...closed].sort(
11903
+ (left, right) => Date.parse(right.lastSeenAt) - Date.parse(left.lastSeenAt)
11904
+ );
11905
+ }
11906
+ return status === "closed" ? this.granular.listClosedSessions({ environmentId: this.environmentId }) : this.granular.listOpenSessions({ environmentId: this.environmentId });
11907
+ }
11908
+ async createSession(options) {
11909
+ return this.granular.createSession({
11910
+ environmentId: this.environmentId,
11911
+ clientId: options?.clientId,
11912
+ initialHeap: options?.initialHeap
11913
+ });
11914
+ }
11915
+ async connectSession(sessionId, options) {
11916
+ const session = await this.granular["connectSession"]({
11917
+ sessionId,
11918
+ clientId: options?.clientId
11919
+ });
11920
+ if (session.environmentId !== this.environmentId) {
11921
+ await session.disconnect().catch(() => {
11922
+ session.disconnectTransport();
11923
+ });
11924
+ throw new Error(
11925
+ `Session ${sessionId} belongs to environment ${session.environmentId}, not ${this.environmentId}.`
11926
+ );
11927
+ }
11928
+ return session;
11929
+ }
11930
+ async reopenSession(sessionId, options) {
11931
+ const session = await this.granular.reopenSession(sessionId, {
11932
+ clientId: options?.clientId
11933
+ });
11934
+ if (session.environmentId !== this.environmentId) {
11935
+ await session.disconnect().catch(() => {
11936
+ session.disconnectTransport();
11937
+ });
11938
+ throw new Error(
11939
+ `Session ${sessionId} belongs to environment ${session.environmentId}, not ${this.environmentId}.`
11940
+ );
11829
11941
  }
11942
+ return session;
11943
+ }
11944
+ async closeSession(sessionId, session) {
11945
+ await this.granular.closeSession(sessionId, session);
11946
+ }
11947
+ async listFeedback() {
11948
+ const response = await this.controlPlaneRequest(`/control/environments/${this.environmentId}/feedback`);
11949
+ return Array.isArray(response.items) ? response.items : [];
11950
+ }
11951
+ getRuntimeBaseUrl() {
11952
+ return deriveRuntimeBaseUrl(this._apiEndpoint);
11830
11953
  }
11831
11954
  async controlPlaneRequest(path, options = {}) {
11832
11955
  const runtimeBase = this.getRuntimeBaseUrl();
@@ -11847,95 +11970,6 @@ var Environment = class extends Session {
11847
11970
  }
11848
11971
  return response.json();
11849
11972
  }
11850
- /**
11851
- * Close the session and disconnect from the sandbox.
11852
- *
11853
- * Sends `client.goodbye` over WebSocket first, then issues an HTTP fallback
11854
- * to the runtime goodbye endpoint if no definitive WS-side runtime notify
11855
- * acknowledgement was observed.
11856
- */
11857
- async disconnect() {
11858
- let wsNotifiedRuntime = false;
11859
- try {
11860
- const goodbye = await this.rpc(
11861
- "client.goodbye",
11862
- {
11863
- timestamp: Date.now()
11864
- }
11865
- );
11866
- wsNotifiedRuntime = Boolean(goodbye?.ok && goodbye?.via);
11867
- } catch {
11868
- wsNotifiedRuntime = false;
11869
- }
11870
- if (!wsNotifiedRuntime) {
11871
- try {
11872
- const runtimeBase = this.getRuntimeBaseUrl();
11873
- await fetch(
11874
- `${runtimeBase}/orchestrator/runtime/environments/${this.environmentId}/session-goodbye`,
11875
- {
11876
- method: "POST",
11877
- headers: {
11878
- "Content-Type": "application/json",
11879
- Authorization: `Bearer ${this._apiKey}`,
11880
- Connection: "close"
11881
- },
11882
- body: JSON.stringify({
11883
- reason: "sdk_disconnect_http_fallback",
11884
- sessionId: this.client.currentSessionId
11885
- })
11886
- }
11887
- );
11888
- } catch {
11889
- }
11890
- }
11891
- this.client.disconnect();
11892
- }
11893
- /**
11894
- * Close only the socket transport without sending `client.goodbye`.
11895
- *
11896
- * Use this when the caller intends to immediately reattach to the same
11897
- * session after an unexpected disconnect.
11898
- */
11899
- disconnectTransport() {
11900
- this.client.disconnect();
11901
- }
11902
- // ==================== GRAPH CONTAINER READINESS ====================
11903
- /** The last known graph container status, updated by checkReadiness() or on heartbeat */
11904
- graphContainerStatus = null;
11905
- /**
11906
- * Check if the graph container is ready and warm.
11907
- *
11908
- * Sends a lightweight heartbeat RPC to the Session DO which internally
11909
- * pings the FalkorDB container. The response includes `graphContainerStatus`,
11910
- * which is stored locally and emitted as a `readiness` event.
11911
- *
11912
- * Use this method to proactively warm the graph container before any
11913
- * GraphQL query that requires it, or to poll the container's state in
11914
- * the background.
11915
- *
11916
- * @returns The current graph container status object
11917
- *
11918
- * @example
11919
- * ```typescript
11920
- * const status = await env.checkReadiness();
11921
- * console.log(status.status); // 'hot' | 'warming' | 'unknown'
11922
- *
11923
- * // Or listen for live updates
11924
- * env.on('readiness', (status) => {
11925
- * console.log('Graph is now:', status.status);
11926
- * });
11927
- * ```
11928
- */
11929
- async checkReadiness() {
11930
- const result = await this.client.call("client.heartbeat", {});
11931
- const containerStatus = result?.graphContainerStatus ?? {
11932
- lastKeepAliveAt: Date.now(),
11933
- status: "unknown"
11934
- };
11935
- this.graphContainerStatus = containerStatus;
11936
- this.emit("readiness", containerStatus);
11937
- return containerStatus;
11938
- }
11939
11973
  // ==================== ID ↔ GRAPH PATH MAPPING ====================
11940
11974
  /**
11941
11975
  * Convert a class name + real-world ID into a unique graph path.
@@ -12065,7 +12099,9 @@ var Environment = class extends Session {
12065
12099
  }
12066
12100
  );
12067
12101
  if (result.errors?.length) {
12068
- throw new Error(`defineRelationship failed: ${result.errors[0].message}`);
12102
+ throw new Error(
12103
+ `defineRelationship failed: ${result.errors[0]?.message ?? "Unknown GraphQL error"}`
12104
+ );
12069
12105
  }
12070
12106
  return result.data.at.define_relationship;
12071
12107
  }
@@ -12101,7 +12137,9 @@ var Environment = class extends Session {
12101
12137
  { path: modelPath }
12102
12138
  );
12103
12139
  if (result.errors?.length) {
12104
- throw new Error(`getRelationships failed: ${result.errors[0].message}`);
12140
+ throw new Error(
12141
+ `getRelationships failed: ${result.errors[0]?.message ?? "Unknown GraphQL error"}`
12142
+ );
12105
12143
  }
12106
12144
  return result.data?.model?.relationships || [];
12107
12145
  }
@@ -12140,7 +12178,7 @@ var Environment = class extends Session {
12140
12178
  { target: targetPath }
12141
12179
  );
12142
12180
  if (result.errors?.length) {
12143
- throw new Error(`attach failed: ${result.errors[0].message}`);
12181
+ throw new Error(`attach failed: ${result.errors[0]?.message ?? "Unknown GraphQL error"}`);
12144
12182
  }
12145
12183
  }
12146
12184
  /**
@@ -12175,7 +12213,7 @@ var Environment = class extends Session {
12175
12213
  }`
12176
12214
  );
12177
12215
  if (result.errors?.length) {
12178
- throw new Error(`detach failed: ${result.errors[0].message}`);
12216
+ throw new Error(`detach failed: ${result.errors[0]?.message ?? "Unknown GraphQL error"}`);
12179
12217
  }
12180
12218
  }
12181
12219
  /**
@@ -12202,7 +12240,9 @@ var Environment = class extends Session {
12202
12240
  }`
12203
12241
  );
12204
12242
  if (result.errors?.length) {
12205
- throw new Error(`listRelated failed: ${result.errors[0].message}`);
12243
+ throw new Error(
12244
+ `listRelated failed: ${result.errors[0]?.message ?? "Unknown GraphQL error"}`
12245
+ );
12206
12246
  }
12207
12247
  return result.data?.at?.at?.list_related || [];
12208
12248
  }
@@ -12295,7 +12335,7 @@ var Environment = class extends Session {
12295
12335
  async _runGraphql(query, label) {
12296
12336
  const result = await this.graphql(query);
12297
12337
  if (result.errors?.length) {
12298
- throw new Error(`${label}: ${result.errors[0].message}`);
12338
+ throw new Error(`${label}: ${result.errors[0]?.message ?? "Unknown GraphQL error"}`);
12299
12339
  }
12300
12340
  return result.data;
12301
12341
  }
@@ -12640,7 +12680,11 @@ var Environment = class extends Session {
12640
12680
  */
12641
12681
  async recordObject(options) {
12642
12682
  const results = await this.recordObjects([options]);
12643
- return results[0];
12683
+ const result = results[0];
12684
+ if (!result) {
12685
+ throw new Error("recordObject: no result returned for record");
12686
+ }
12687
+ return result;
12644
12688
  }
12645
12689
  /**
12646
12690
  * Batch version of `recordObject()`.
@@ -12692,7 +12736,13 @@ var Environment = class extends Session {
12692
12736
  );
12693
12737
  }
12694
12738
  for (let index = 0; index < items.length; index += 1) {
12695
- results[plan.offset + index] = items[index];
12739
+ const item = items[index];
12740
+ if (!item) {
12741
+ throw new Error(
12742
+ `recordObjects: chunk ${plan.chunkIndex + 1} returned an empty result at index ${index}`
12743
+ );
12744
+ }
12745
+ results[plan.offset + index] = item;
12696
12746
  }
12697
12747
  if (onChunk) {
12698
12748
  const info = {
@@ -12796,36 +12846,186 @@ var Environment = class extends Session {
12796
12846
  }
12797
12847
  );
12798
12848
  }
12799
- // ==================== PUBLISH TOOLS ====================
12849
+ };
12850
+ var EnvironmentSession = class extends Session {
12851
+ environment;
12852
+ /** The last known graph container status, updated by checkReadiness() or on heartbeat */
12853
+ graphContainerStatus = null;
12854
+ constructor(client, environment, clientId) {
12855
+ super(client, clientId);
12856
+ this.environment = environment;
12857
+ }
12858
+ get environmentId() {
12859
+ return this.environment.environmentId;
12860
+ }
12861
+ get sandboxId() {
12862
+ return this.environment.sandboxId;
12863
+ }
12864
+ get ontologyId() {
12865
+ return this.environment.ontologyId;
12866
+ }
12867
+ get subjectId() {
12868
+ return this.environment.subjectId;
12869
+ }
12870
+ get envName() {
12871
+ return this.environment.envName;
12872
+ }
12873
+ get versionId() {
12874
+ return this.environment.versionId;
12875
+ }
12876
+ get granularId() {
12877
+ return this.environment.granularId;
12878
+ }
12879
+ get permissionProfileId() {
12880
+ return this.environment.permissionProfileId;
12881
+ }
12882
+ get apiEndpoint() {
12883
+ return this.environment.apiEndpoint;
12884
+ }
12885
+ get data() {
12886
+ return this.environment.data;
12887
+ }
12888
+ get feedback() {
12889
+ return this.environment.feedback;
12890
+ }
12800
12891
  /**
12801
- * Removed: environment-scoped effect publication is no longer supported.
12892
+ * Return a plain JS snapshot of the synced session heap.
12802
12893
  */
12803
- async publishTools(tools, revision = "1.0.0") {
12804
- return super.publishTools(tools, revision);
12894
+ getHeap() {
12895
+ const doc = this.document;
12896
+ return normalizeHeapSnapshot(doc?.heap);
12897
+ }
12898
+ async graphql(query, variables) {
12899
+ return this.environment.graphql(query, variables);
12900
+ }
12901
+ async defineRelationship(options) {
12902
+ return this.environment.defineRelationship(options);
12903
+ }
12904
+ async getRelationships(modelPath) {
12905
+ return this.environment.getRelationships(modelPath);
12906
+ }
12907
+ async attach(modelPath, submodelPath, targetPath) {
12908
+ return this.environment.attach(modelPath, submodelPath, targetPath);
12909
+ }
12910
+ async detach(modelPath, submodelPath, targetPath) {
12911
+ return this.environment.detach(modelPath, submodelPath, targetPath);
12912
+ }
12913
+ async listRelated(modelPath, submodelPath) {
12914
+ return this.environment.listRelated(modelPath, submodelPath);
12915
+ }
12916
+ async applyManifest(manifest) {
12917
+ return this.environment.applyManifest(manifest);
12918
+ }
12919
+ async recordObject(options) {
12920
+ return this.environment.recordObject(options);
12921
+ }
12922
+ async recordObjects(records, options) {
12923
+ return this.environment.recordObjects(records, options);
12924
+ }
12925
+ async enqueueRecordImport(records, options = {}) {
12926
+ return this.environment.enqueueRecordImport(records, options);
12927
+ }
12928
+ async listRecordImports(status) {
12929
+ return this.environment.listRecordImports(status);
12930
+ }
12931
+ async getRecordImportSummary() {
12932
+ return this.environment.getRecordImportSummary();
12933
+ }
12934
+ async getAwaitingRecordCount() {
12935
+ return this.environment.getAwaitingRecordCount();
12936
+ }
12937
+ async getRecordImport(importId) {
12938
+ return this.environment.getRecordImport(importId);
12939
+ }
12940
+ async cancelRecordImport(importId) {
12941
+ return this.environment.cancelRecordImport(importId);
12942
+ }
12943
+ async listFeedback() {
12944
+ return this.environment.listFeedback();
12805
12945
  }
12806
12946
  /**
12807
- * Removed: environment-scoped effect publication is no longer supported.
12947
+ * Close the session and disconnect from the sandbox.
12948
+ *
12949
+ * Sends `client.goodbye` over WebSocket first, then issues an HTTP fallback
12950
+ * to the runtime goodbye endpoint if no definitive WS-side runtime notify
12951
+ * acknowledgement was observed.
12808
12952
  */
12809
- async publishEffect(effect) {
12810
- return super.publishEffect(effect);
12953
+ async disconnect() {
12954
+ let wsNotifiedRuntime = false;
12955
+ try {
12956
+ const goodbye = await this.rpc(
12957
+ "client.goodbye",
12958
+ {
12959
+ timestamp: Date.now()
12960
+ }
12961
+ );
12962
+ wsNotifiedRuntime = Boolean(goodbye?.ok && goodbye?.via);
12963
+ } catch {
12964
+ wsNotifiedRuntime = false;
12965
+ }
12966
+ if (!wsNotifiedRuntime) {
12967
+ try {
12968
+ await fetch(
12969
+ `${this.environment.runtimeBaseUrl}/orchestrator/runtime/environments/${this.environmentId}/session-goodbye`,
12970
+ {
12971
+ method: "POST",
12972
+ headers: {
12973
+ "Content-Type": "application/json",
12974
+ Authorization: `Bearer ${this.environment.authToken}`,
12975
+ Connection: "close"
12976
+ },
12977
+ body: JSON.stringify({
12978
+ reason: "sdk_disconnect_http_fallback",
12979
+ sessionId: this.client.currentSessionId
12980
+ })
12981
+ }
12982
+ );
12983
+ } catch {
12984
+ }
12985
+ }
12986
+ this.client.disconnect();
12811
12987
  }
12812
12988
  /**
12813
- * Removed: environment-scoped effect publication is no longer supported.
12989
+ * Close only the socket transport without sending `client.goodbye`.
12814
12990
  */
12815
- async publishEffects(effects) {
12816
- return super.publishEffects(effects);
12991
+ disconnectTransport() {
12992
+ this.client.disconnect();
12817
12993
  }
12818
12994
  /**
12819
- * Removed: environment-scoped effect publication is no longer supported.
12995
+ * Backwards-compatible alias for `disconnect()`.
12820
12996
  */
12821
- async unpublishEffect(name) {
12822
- return super.unpublishEffect(name);
12997
+ async close() {
12998
+ await this.disconnect();
12823
12999
  }
12824
13000
  /**
12825
- * Removed: environment-scoped effect publication is no longer supported.
13001
+ * Check if the graph container is ready and warm.
12826
13002
  */
12827
- async unpublishAllEffects() {
12828
- return super.unpublishAllEffects();
13003
+ async checkReadiness() {
13004
+ const result = await this.client.call("client.heartbeat", {});
13005
+ const containerStatus = result?.graphContainerStatus ?? {
13006
+ lastKeepAliveAt: Date.now(),
13007
+ status: "unknown"
13008
+ };
13009
+ this.graphContainerStatus = containerStatus;
13010
+ this.emit("readiness", containerStatus);
13011
+ return containerStatus;
13012
+ }
13013
+ };
13014
+ var OntologyHandle = class {
13015
+ granular;
13016
+ ontologyNameOrId;
13017
+ constructor(granular, ontologyNameOrId) {
13018
+ this.granular = granular;
13019
+ this.ontologyNameOrId = ontologyNameOrId;
13020
+ }
13021
+ get effects() {
13022
+ return {
13023
+ register: async (effect) => this.granular.registerEffect(this.ontologyNameOrId, effect),
13024
+ registerMany: async (effects) => this.granular.registerEffects(this.ontologyNameOrId, effects),
13025
+ unregister: async (name) => this.granular.unregisterEffect(this.ontologyNameOrId, name),
13026
+ clear: async () => this.granular.unregisterAllEffects(this.ontologyNameOrId),
13027
+ disconnect: async () => this.granular.disconnectEffects(this.ontologyNameOrId)
13028
+ };
12829
13029
  }
12830
13030
  };
12831
13031
  var Granular = class _Granular {
@@ -12862,6 +13062,12 @@ var Granular = class _Granular {
12862
13062
  this.onReconnectError = options.onReconnectError;
12863
13063
  this.httpUrl = this.apiUrl.replace(/^wss:\/\//, "https://").replace(/^ws:\/\//, "http://").replace(/\/ws$/, "");
12864
13064
  }
13065
+ /**
13066
+ * Return an ontology-scoped handle for effects and other ontology-level APIs.
13067
+ */
13068
+ ontology(ontologyNameOrId) {
13069
+ return new OntologyHandle(this, ontologyNameOrId);
13070
+ }
12865
13071
  /**
12866
13072
  * Records/upserts a user and prepares them for sandbox connections
12867
13073
  *
@@ -12898,7 +13104,23 @@ var Granular = class _Granular {
12898
13104
  permissions: options.permissions || []
12899
13105
  });
12900
13106
  }
13107
+ /**
13108
+ * Alias for `recordUser()` with user-facing naming that matches upsert semantics.
13109
+ */
13110
+ async upsertUser(options) {
13111
+ return this.recordUser(options);
13112
+ }
12901
13113
  async resolveConnectUser(options) {
13114
+ const providedIdentityCount = [
13115
+ Boolean(options.user),
13116
+ Boolean(options.userId),
13117
+ Boolean(options.granularId)
13118
+ ].filter(Boolean).length;
13119
+ if (providedIdentityCount !== 1) {
13120
+ throw new Error(
13121
+ "openEnvironment() requires exactly one of userId, granularId, or a user object returned by recordUser()."
13122
+ );
13123
+ }
12902
13124
  if (options.user) {
12903
13125
  const user = normalizeUser(options.user);
12904
13126
  return {
@@ -12935,56 +13157,85 @@ var Granular = class _Granular {
12935
13157
  };
12936
13158
  }
12937
13159
  throw new Error(
12938
- "connect() requires either userId, granularId, or a user object returned by recordUser()."
13160
+ "openEnvironment() requires either userId, granularId, or a user object returned by recordUser()."
12939
13161
  );
12940
13162
  }
12941
13163
  /**
12942
- * Connect to an ontology environment and establish a real-time session.
12943
- *
12944
- * Effects are registered at the sandbox level via `granular.registerEffect()`
12945
- * or `granular.registerEffects()`. Sessions pick up live availability from
12946
- * the sandbox registry automatically.
12947
- *
12948
- * @param options - Connection options
12949
- * @returns An active environment session
13164
+ * Open or resolve an ontology environment for one user without opening a session.
12950
13165
  *
12951
13166
  * @example
12952
13167
  * ```typescript
12953
- * const environment = await granular.connect({
13168
+ * const environment = await granular.openEnvironment({
12954
13169
  * ontology: 'my-ontology',
12955
- * environment: 'dev',
13170
+ * tag: 'dev',
12956
13171
  * userId: 'user_123',
12957
13172
  * permissions: ['agent'],
12958
13173
  * });
12959
13174
  *
12960
- * await granular.registerEffect('my-sandbox', {
12961
- * name: 'greet',
12962
- * description: 'Say hello',
12963
- * inputSchema: { type: 'object', properties: {} },
12964
- * handler: async () => 'Hello!',
13175
+ * await environment.data.record({
13176
+ * className: 'customer',
13177
+ * id: 'acme',
13178
+ * fields: { name: 'Acme' },
12965
13179
  * });
12966
13180
  *
12967
- * // Submit job
12968
- * const job = await environment.submitJob(`
12969
- * import { tools } from './sandbox-tools';
12970
- * return await tools.greet({});
12971
- * `);
12972
- *
12973
- * console.log(await job.result); // 'Hello!'
13181
+ * const session = await environment.sessions.create();
13182
+ * const job = await session.submitJob(`return "hello";`);
13183
+ * console.log(await job.result);
12974
13184
  * ```
12975
13185
  */
13186
+ async openEnvironment(options) {
13187
+ const envData = await this.resolveOpenEnvironmentData(
13188
+ options,
13189
+ "openEnvironment"
13190
+ );
13191
+ return this.bindEnvironmentHandle(envData);
13192
+ }
13193
+ /**
13194
+ * Deprecated compatibility alias for `openEnvironment()`.
13195
+ *
13196
+ * `connect()` no longer opens a runtime session automatically.
13197
+ */
12976
13198
  async connect(options) {
12977
- const clientId = options.clientId || `client_${Date.now()}`;
13199
+ return this.openEnvironment({
13200
+ ...options,
13201
+ tag: this.resolveRequestedTag(options, "connect"),
13202
+ permissions: options.permissions || options.user?.permissions || []
13203
+ });
13204
+ }
13205
+ resolveRequestedTag(options, methodName) {
13206
+ const tag = options.tag?.trim() || options.tagName?.trim() || options.environment?.trim();
13207
+ if (!tag) {
13208
+ throw new Error(`${methodName}() requires \`tag\`.`);
13209
+ }
13210
+ return tag;
13211
+ }
13212
+ buildManagedEnvironmentName(tag, versionId) {
13213
+ return `__sdk__${tag}__${versionId}`;
13214
+ }
13215
+ matchesTagTrackedEnvironment(environment, tagName, tagId) {
13216
+ const environmentTagName = environment.tag?.name || environment.buildPolicy.tagName || null;
13217
+ return environment.tagId === tagId || environmentTagName === tagName || environment.environment === tagName || environment.envName === tagName || environment.environment === this.buildManagedEnvironmentName(tagName, environment.versionId) || environment.envName === this.buildManagedEnvironmentName(tagName, environment.versionId);
13218
+ }
13219
+ sortEnvironmentsByRecency(environments) {
13220
+ return [...environments].sort(
13221
+ (left, right) => right.updatedAt - left.updatedAt
13222
+ );
13223
+ }
13224
+ async resolveOpenEnvironmentData(options, methodName) {
12978
13225
  const ontology = options.ontology;
12979
13226
  if (!ontology) {
12980
- throw new Error("connect() requires `ontology`.");
13227
+ throw new Error(`${methodName}() requires \`ontology\`.`);
12981
13228
  }
12982
- const environmentName = options.environment;
12983
- if (!environmentName) {
12984
- throw new Error("connect() requires `environment`.");
13229
+ const tagName = options.tag?.trim();
13230
+ if (!tagName) {
13231
+ throw new Error(`${methodName}() requires \`tag\`.`);
12985
13232
  }
12986
- const tagName = options.tagName?.trim() || void 0;
12987
13233
  const user = await this.resolveConnectUser(options);
13234
+ if (!Array.isArray(user.permissions) || user.permissions.length === 0) {
13235
+ throw new Error(
13236
+ `${methodName}() requires at least one permission so the SDK can ensure assignments for new users.`
13237
+ );
13238
+ }
12988
13239
  const sandbox = await this.findOrCreateSandbox(ontology);
12989
13240
  for (const profileName of user.permissions) {
12990
13241
  const profileId = await this.ensurePermissionProfile(
@@ -12997,22 +13248,49 @@ var Granular = class _Granular {
12997
13248
  profileId
12998
13249
  );
12999
13250
  }
13000
- const envData = await this.environments.create(sandbox.sandboxId, {
13251
+ const tags = await this.request(
13252
+ `/control/sandboxes/${sandbox.sandboxId}/tags`
13253
+ );
13254
+ const tag = (tags.items || []).find(
13255
+ (candidate) => Boolean(candidate?.name === tagName)
13256
+ );
13257
+ if (!tag) {
13258
+ throw new Error(
13259
+ `Tag "${tagName}" was not found for ontology ${sandbox.sandboxId}.`
13260
+ );
13261
+ }
13262
+ const targetVersionId = tag.targetVersionId || tag.targetBuildId;
13263
+ if (!targetVersionId) {
13264
+ throw new Error(
13265
+ `Tag "${tagName}" does not currently point to a build/version.`
13266
+ );
13267
+ }
13268
+ const allEnvironments = await this.environments.list(sandbox.sandboxId);
13269
+ const userEnvironments = allEnvironments.filter(
13270
+ (environment) => environment.subjectId === user.granularId
13271
+ );
13272
+ const currentMatches = this.sortEnvironmentsByRecency(
13273
+ userEnvironments.filter(
13274
+ (environment) => this.matchesTagTrackedEnvironment(environment, tagName, tag.tagId) && environment.versionId === targetVersionId
13275
+ )
13276
+ );
13277
+ if (currentMatches.length > 0) {
13278
+ return currentMatches[0];
13279
+ }
13280
+ const outdatedMatches = this.sortEnvironmentsByRecency(
13281
+ userEnvironments.filter(
13282
+ (environment) => this.matchesTagTrackedEnvironment(environment, tagName, tag.tagId)
13283
+ )
13284
+ );
13285
+ if (outdatedMatches.length > 0 && options.createFreshIfOutdated !== true) {
13286
+ return outdatedMatches[0];
13287
+ }
13288
+ return this.environments.create(sandbox.sandboxId, {
13001
13289
  subjectId: user.granularId,
13002
- environment: environmentName,
13003
- tagName,
13290
+ environment: this.buildManagedEnvironmentName(tagName, targetVersionId),
13291
+ tagId: tag.tagId,
13004
13292
  permissionProfileId: null
13005
13293
  });
13006
- await this.activateEnvironment(envData.environmentId);
13007
- const session = await this.request("/ws/sessions", {
13008
- method: "POST",
13009
- body: JSON.stringify({
13010
- environmentId: envData.environmentId,
13011
- clientId,
13012
- initialHeap: options.initialHeap
13013
- })
13014
- });
13015
- return this.bindWebSocketEnvironment(envData, clientId, session);
13016
13294
  }
13017
13295
  /**
13018
13296
  * List active (open) sessions for an environment — each session is one agent conversation thread.
@@ -13075,6 +13353,7 @@ var Granular = class _Granular {
13075
13353
  const clientId = options.clientId || `client_${Date.now()}`;
13076
13354
  await this.activateEnvironment(options.environmentId);
13077
13355
  const envData = await this.environments.get(options.environmentId);
13356
+ const environment = this.bindEnvironmentHandle(envData);
13078
13357
  const session = await this.request("/ws/sessions", {
13079
13358
  method: "POST",
13080
13359
  body: JSON.stringify({
@@ -13083,7 +13362,7 @@ var Granular = class _Granular {
13083
13362
  initialHeap: options.initialHeap
13084
13363
  })
13085
13364
  });
13086
- return this.bindWebSocketEnvironment(envData, clientId, session);
13365
+ return this.bindWebSocketEnvironmentSession(environment, clientId, session);
13087
13366
  }
13088
13367
  /**
13089
13368
  * Connect to an existing open session (same conversation thread) using a freshly minted WebSocket token.
@@ -13095,7 +13374,8 @@ var Granular = class _Granular {
13095
13374
  body: JSON.stringify({})
13096
13375
  });
13097
13376
  const envData = await this.environments.get(minted.environmentId);
13098
- return this.bindWebSocketEnvironment(envData, clientId, minted);
13377
+ const environment = this.bindEnvironmentHandle(envData);
13378
+ return this.bindWebSocketEnvironmentSession(environment, clientId, minted);
13099
13379
  }
13100
13380
  /**
13101
13381
  * Mark a session closed in the control plane. If `environment` is the connected handle for that
@@ -13127,7 +13407,11 @@ var Granular = class _Granular {
13127
13407
  });
13128
13408
  return this.connectSession({ sessionId, clientId: options?.clientId });
13129
13409
  }
13130
- async bindWebSocketEnvironment(envData, clientId, session) {
13410
+ bindEnvironmentHandle(envData) {
13411
+ const graphqlEndpoint = `${this.httpUrl}/orchestrator/graphql`;
13412
+ return new Environment(this, envData, this.apiKey, graphqlEndpoint);
13413
+ }
13414
+ async bindWebSocketEnvironmentSession(environment, clientId, session) {
13131
13415
  const client = new WSClient({
13132
13416
  url: session.wsUrl,
13133
13417
  sessionId: session.sessionId,
@@ -13138,16 +13422,13 @@ var Granular = class _Granular {
13138
13422
  onReconnectError: this.onReconnectError
13139
13423
  });
13140
13424
  await client.connect();
13141
- const graphqlEndpoint = `${this.httpUrl}/orchestrator/graphql`;
13142
- const environment = new Environment(
13425
+ const environmentSession = new EnvironmentSession(
13143
13426
  client,
13144
- envData,
13145
- clientId,
13146
- this.apiKey,
13147
- graphqlEndpoint
13427
+ environment,
13428
+ clientId
13148
13429
  );
13149
- await environment.hello();
13150
- return environment;
13430
+ await environmentSession.hello();
13431
+ return environmentSession;
13151
13432
  }
13152
13433
  async activateEnvironment(environmentId) {
13153
13434
  await this.request(`/orchestrator/runtime/environments/${environmentId}/activate`, {
@@ -15539,7 +15820,9 @@ function buildSessionTranscript(input) {
15539
15820
  }
15540
15821
 
15541
15822
  exports.Environment = Environment;
15823
+ exports.EnvironmentSession = EnvironmentSession;
15542
15824
  exports.Granular = Granular;
15825
+ exports.OntologyHandle = OntologyHandle;
15543
15826
  exports.Session = Session;
15544
15827
  exports.WSClient = WSClient;
15545
15828
  exports.buildContinuationInstruction = buildContinuationInstruction;