@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.mjs CHANGED
@@ -3983,7 +3983,11 @@ var WSClient = class {
3983
3983
  return null;
3984
3984
  }
3985
3985
  try {
3986
- const payloadRaw = this.decodeBase64Url(parts[1]);
3986
+ const payloadSegment = parts[1];
3987
+ if (!payloadSegment) {
3988
+ return null;
3989
+ }
3990
+ const payloadRaw = this.decodeBase64Url(payloadSegment);
3987
3991
  const payload = JSON.parse(payloadRaw);
3988
3992
  if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp)) {
3989
3993
  return null;
@@ -4647,27 +4651,27 @@ var Session = class {
4647
4651
  }
4648
4652
  async publishTools(tools, revision = "1.0.0") {
4649
4653
  throw new Error(
4650
- "Environment-scoped effect publication was removed. Declare effects in the manifest and register live handlers with granular.registerEffects(environment.sandboxId, effects)."
4654
+ "Environment-scoped effect publication was removed. Declare effects in the manifest and register live handlers with granular.ontology(environment.sandboxId).effects.registerMany(effects)."
4651
4655
  );
4652
4656
  }
4653
4657
  async publishEffect(effect) {
4654
4658
  throw new Error(
4655
- "Environment-scoped effect publication was removed. Use granular.registerEffect(environment.sandboxId, effect)."
4659
+ "Environment-scoped effect publication was removed. Use granular.ontology(environment.sandboxId).effects.register(effect)."
4656
4660
  );
4657
4661
  }
4658
4662
  async publishEffects(effects) {
4659
4663
  throw new Error(
4660
- "Environment-scoped effect publication was removed. Use granular.registerEffects(environment.sandboxId, effects)."
4664
+ "Environment-scoped effect publication was removed. Use granular.ontology(environment.sandboxId).effects.registerMany(effects)."
4661
4665
  );
4662
4666
  }
4663
4667
  async unpublishEffect(name) {
4664
4668
  throw new Error(
4665
- "Environment-scoped effect publication was removed. Use granular.unregisterEffect(environment.sandboxId, effectName)."
4669
+ "Environment-scoped effect publication was removed. Use granular.ontology(environment.sandboxId).effects.unregister(effectName)."
4666
4670
  );
4667
4671
  }
4668
4672
  async unpublishAllEffects() {
4669
4673
  throw new Error(
4670
- "Environment-scoped effect publication was removed. Use granular.unregisterAllEffects(environment.sandboxId)."
4674
+ "Environment-scoped effect publication was removed. Use granular.ontology(environment.sandboxId).effects.clear()."
4671
4675
  );
4672
4676
  }
4673
4677
  /**
@@ -5060,6 +5064,7 @@ import { ${allImports} } from "./sandbox-tools";
5060
5064
  this.eventListeners.set(event, []);
5061
5065
  }
5062
5066
  this.eventListeners.get(event).push(handler);
5067
+ return () => this.off(event, handler);
5063
5068
  }
5064
5069
  /**
5065
5070
  * Unsubscribe from session events
@@ -5516,6 +5521,16 @@ var JobImplementation = class {
5516
5521
  handler(message);
5517
5522
  }
5518
5523
  }
5524
+ return () => {
5525
+ const handlers = this.eventListeners.get(event);
5526
+ if (!handlers) {
5527
+ return;
5528
+ }
5529
+ this.eventListeners.set(
5530
+ event,
5531
+ handlers.filter((current) => current !== handler)
5532
+ );
5533
+ };
5519
5534
  }
5520
5535
  replayAgentMessage(message) {
5521
5536
  this.captureAgentMessage(message);
@@ -11691,6 +11706,22 @@ function normalizeHeapSnapshot(raw) {
11691
11706
  updatedAt: typeof heap.updatedAt === "number" ? heap.updatedAt : Date.now()
11692
11707
  };
11693
11708
  }
11709
+ function deriveRuntimeBaseUrl(apiEndpoint) {
11710
+ try {
11711
+ const endpoint = new URL(apiEndpoint);
11712
+ const graphqlSuffix = "/orchestrator/graphql";
11713
+ if (endpoint.pathname.endsWith(graphqlSuffix)) {
11714
+ endpoint.pathname = endpoint.pathname.slice(0, -graphqlSuffix.length);
11715
+ } else if (endpoint.pathname.endsWith("/graphql")) {
11716
+ endpoint.pathname = endpoint.pathname.slice(0, -"/graphql".length);
11717
+ }
11718
+ endpoint.search = "";
11719
+ endpoint.hash = "";
11720
+ return endpoint.toString().replace(/\/$/, "");
11721
+ } catch {
11722
+ return apiEndpoint.replace(/\/orchestrator\/graphql$/, "").replace(/\/$/, "");
11723
+ }
11724
+ }
11694
11725
  function normalizeSubject(subject) {
11695
11726
  const granularId = subject.granularId || subject.subjectId;
11696
11727
  const userId = subject.userId || subject.identityId || granularId;
@@ -11730,12 +11761,13 @@ function normalizeEnvironmentData(environment) {
11730
11761
  tracking: environment.tracking || buildPolicy
11731
11762
  };
11732
11763
  }
11733
- var Environment = class extends Session {
11764
+ var Environment = class {
11765
+ granular;
11734
11766
  envData;
11735
11767
  _apiKey;
11736
11768
  _apiEndpoint;
11737
- constructor(client, envData, clientId, apiKey, apiEndpoint) {
11738
- super(client, clientId);
11769
+ constructor(granular, envData, apiKey, apiEndpoint) {
11770
+ this.granular = granular;
11739
11771
  this.envData = envData;
11740
11772
  this._apiKey = apiKey;
11741
11773
  this._apiEndpoint = apiEndpoint;
@@ -11776,35 +11808,126 @@ var Environment = class extends Session {
11776
11808
  get permissionProfileId() {
11777
11809
  return this.envData.permissionProfileId;
11778
11810
  }
11811
+ /** The current build policy backing this environment */
11812
+ get buildPolicy() {
11813
+ return this.envData.buildPolicy;
11814
+ }
11815
+ /** The current update state relative to the followed tag */
11816
+ get updateState() {
11817
+ return this.envData.updateState;
11818
+ }
11819
+ /** Convenience flag for whether this environment trails the current tag target */
11820
+ get isOutdated() {
11821
+ return this.envData.updateState === "update_available";
11822
+ }
11823
+ /** The followed tag name when this environment is tag-tracked */
11824
+ get tag() {
11825
+ return this.envData.tag?.name || this.envData.buildPolicy.tagName || null;
11826
+ }
11779
11827
  /** The GraphQL API endpoint URL */
11780
11828
  get apiEndpoint() {
11781
11829
  return this._apiEndpoint;
11782
11830
  }
11831
+ /** Internal auth token used for control-plane and runtime fallback requests */
11832
+ get authToken() {
11833
+ return this._apiKey;
11834
+ }
11835
+ /** Base runtime URL derived from the GraphQL endpoint */
11836
+ get runtimeBaseUrl() {
11837
+ return this.getRuntimeBaseUrl();
11838
+ }
11839
+ get sessions() {
11840
+ return {
11841
+ list: async (options) => this.listSessions(options?.status || "active"),
11842
+ create: async (options) => this.createSession(options),
11843
+ connect: async (sessionId, options) => this.connectSession(sessionId, options),
11844
+ reopen: async (sessionId, options) => this.reopenSession(sessionId, options),
11845
+ close: async (sessionId, session) => this.closeSession(sessionId, session)
11846
+ };
11847
+ }
11848
+ get data() {
11849
+ return {
11850
+ record: async (record) => this.recordObject(record),
11851
+ recordMany: async (records, options) => this.recordObjects(records, options),
11852
+ import: async (records, options) => this.enqueueRecordImport(records, options),
11853
+ listImports: async (status) => this.listRecordImports(status),
11854
+ getImport: async (importId) => this.getRecordImport(importId),
11855
+ getImportSummary: async () => this.getRecordImportSummary(),
11856
+ cancelImport: async (importId) => this.cancelRecordImport(importId),
11857
+ getAwaitingCount: async () => this.getAwaitingRecordCount()
11858
+ };
11859
+ }
11860
+ get feedback() {
11861
+ return {
11862
+ list: async () => this.listFeedback()
11863
+ };
11864
+ }
11783
11865
  /**
11784
- * Return a plain JS snapshot of the synced session heap.
11785
- *
11786
- * The heap lives in the Automerge document, so this method does not perform
11787
- * any extra network roundtrip.
11866
+ * Sessionless environments do not own a live transport, so disconnecting the
11867
+ * environment handle itself is a no-op. This keeps the public surface
11868
+ * symmetric with `EnvironmentSession.disconnect()` and lets callers always
11869
+ * clean up safely without tracking whether they currently hold an environment
11870
+ * or a session.
11788
11871
  */
11789
- getHeap() {
11790
- const doc = this.document;
11791
- return normalizeHeapSnapshot(doc?.heap);
11872
+ async disconnect() {
11792
11873
  }
11793
- getRuntimeBaseUrl() {
11794
- try {
11795
- const endpoint = new URL(this._apiEndpoint);
11796
- const graphqlSuffix = "/orchestrator/graphql";
11797
- if (endpoint.pathname.endsWith(graphqlSuffix)) {
11798
- endpoint.pathname = endpoint.pathname.slice(0, -graphqlSuffix.length);
11799
- } else if (endpoint.pathname.endsWith("/graphql")) {
11800
- endpoint.pathname = endpoint.pathname.slice(0, -"/graphql".length);
11801
- }
11802
- endpoint.search = "";
11803
- endpoint.hash = "";
11804
- return endpoint.toString().replace(/\/$/, "");
11805
- } catch {
11806
- return this._apiEndpoint.replace(/\/orchestrator\/graphql$/, "").replace(/\/$/, "");
11874
+ async listSessions(status = "active") {
11875
+ if (status === "all") {
11876
+ const [active, closed] = await Promise.all([
11877
+ this.granular.listOpenSessions({ environmentId: this.environmentId }),
11878
+ this.granular.listClosedSessions({ environmentId: this.environmentId })
11879
+ ]);
11880
+ return [...active, ...closed].sort(
11881
+ (left, right) => Date.parse(right.lastSeenAt) - Date.parse(left.lastSeenAt)
11882
+ );
11883
+ }
11884
+ return status === "closed" ? this.granular.listClosedSessions({ environmentId: this.environmentId }) : this.granular.listOpenSessions({ environmentId: this.environmentId });
11885
+ }
11886
+ async createSession(options) {
11887
+ return this.granular.createSession({
11888
+ environmentId: this.environmentId,
11889
+ clientId: options?.clientId,
11890
+ initialHeap: options?.initialHeap
11891
+ });
11892
+ }
11893
+ async connectSession(sessionId, options) {
11894
+ const session = await this.granular["connectSession"]({
11895
+ sessionId,
11896
+ clientId: options?.clientId
11897
+ });
11898
+ if (session.environmentId !== this.environmentId) {
11899
+ await session.disconnect().catch(() => {
11900
+ session.disconnectTransport();
11901
+ });
11902
+ throw new Error(
11903
+ `Session ${sessionId} belongs to environment ${session.environmentId}, not ${this.environmentId}.`
11904
+ );
11905
+ }
11906
+ return session;
11907
+ }
11908
+ async reopenSession(sessionId, options) {
11909
+ const session = await this.granular.reopenSession(sessionId, {
11910
+ clientId: options?.clientId
11911
+ });
11912
+ if (session.environmentId !== this.environmentId) {
11913
+ await session.disconnect().catch(() => {
11914
+ session.disconnectTransport();
11915
+ });
11916
+ throw new Error(
11917
+ `Session ${sessionId} belongs to environment ${session.environmentId}, not ${this.environmentId}.`
11918
+ );
11807
11919
  }
11920
+ return session;
11921
+ }
11922
+ async closeSession(sessionId, session) {
11923
+ await this.granular.closeSession(sessionId, session);
11924
+ }
11925
+ async listFeedback() {
11926
+ const response = await this.controlPlaneRequest(`/control/environments/${this.environmentId}/feedback`);
11927
+ return Array.isArray(response.items) ? response.items : [];
11928
+ }
11929
+ getRuntimeBaseUrl() {
11930
+ return deriveRuntimeBaseUrl(this._apiEndpoint);
11808
11931
  }
11809
11932
  async controlPlaneRequest(path, options = {}) {
11810
11933
  const runtimeBase = this.getRuntimeBaseUrl();
@@ -11825,95 +11948,6 @@ var Environment = class extends Session {
11825
11948
  }
11826
11949
  return response.json();
11827
11950
  }
11828
- /**
11829
- * Close the session and disconnect from the sandbox.
11830
- *
11831
- * Sends `client.goodbye` over WebSocket first, then issues an HTTP fallback
11832
- * to the runtime goodbye endpoint if no definitive WS-side runtime notify
11833
- * acknowledgement was observed.
11834
- */
11835
- async disconnect() {
11836
- let wsNotifiedRuntime = false;
11837
- try {
11838
- const goodbye = await this.rpc(
11839
- "client.goodbye",
11840
- {
11841
- timestamp: Date.now()
11842
- }
11843
- );
11844
- wsNotifiedRuntime = Boolean(goodbye?.ok && goodbye?.via);
11845
- } catch {
11846
- wsNotifiedRuntime = false;
11847
- }
11848
- if (!wsNotifiedRuntime) {
11849
- try {
11850
- const runtimeBase = this.getRuntimeBaseUrl();
11851
- await fetch(
11852
- `${runtimeBase}/orchestrator/runtime/environments/${this.environmentId}/session-goodbye`,
11853
- {
11854
- method: "POST",
11855
- headers: {
11856
- "Content-Type": "application/json",
11857
- Authorization: `Bearer ${this._apiKey}`,
11858
- Connection: "close"
11859
- },
11860
- body: JSON.stringify({
11861
- reason: "sdk_disconnect_http_fallback",
11862
- sessionId: this.client.currentSessionId
11863
- })
11864
- }
11865
- );
11866
- } catch {
11867
- }
11868
- }
11869
- this.client.disconnect();
11870
- }
11871
- /**
11872
- * Close only the socket transport without sending `client.goodbye`.
11873
- *
11874
- * Use this when the caller intends to immediately reattach to the same
11875
- * session after an unexpected disconnect.
11876
- */
11877
- disconnectTransport() {
11878
- this.client.disconnect();
11879
- }
11880
- // ==================== GRAPH CONTAINER READINESS ====================
11881
- /** The last known graph container status, updated by checkReadiness() or on heartbeat */
11882
- graphContainerStatus = null;
11883
- /**
11884
- * Check if the graph container is ready and warm.
11885
- *
11886
- * Sends a lightweight heartbeat RPC to the Session DO which internally
11887
- * pings the FalkorDB container. The response includes `graphContainerStatus`,
11888
- * which is stored locally and emitted as a `readiness` event.
11889
- *
11890
- * Use this method to proactively warm the graph container before any
11891
- * GraphQL query that requires it, or to poll the container's state in
11892
- * the background.
11893
- *
11894
- * @returns The current graph container status object
11895
- *
11896
- * @example
11897
- * ```typescript
11898
- * const status = await env.checkReadiness();
11899
- * console.log(status.status); // 'hot' | 'warming' | 'unknown'
11900
- *
11901
- * // Or listen for live updates
11902
- * env.on('readiness', (status) => {
11903
- * console.log('Graph is now:', status.status);
11904
- * });
11905
- * ```
11906
- */
11907
- async checkReadiness() {
11908
- const result = await this.client.call("client.heartbeat", {});
11909
- const containerStatus = result?.graphContainerStatus ?? {
11910
- lastKeepAliveAt: Date.now(),
11911
- status: "unknown"
11912
- };
11913
- this.graphContainerStatus = containerStatus;
11914
- this.emit("readiness", containerStatus);
11915
- return containerStatus;
11916
- }
11917
11951
  // ==================== ID ↔ GRAPH PATH MAPPING ====================
11918
11952
  /**
11919
11953
  * Convert a class name + real-world ID into a unique graph path.
@@ -12043,7 +12077,9 @@ var Environment = class extends Session {
12043
12077
  }
12044
12078
  );
12045
12079
  if (result.errors?.length) {
12046
- throw new Error(`defineRelationship failed: ${result.errors[0].message}`);
12080
+ throw new Error(
12081
+ `defineRelationship failed: ${result.errors[0]?.message ?? "Unknown GraphQL error"}`
12082
+ );
12047
12083
  }
12048
12084
  return result.data.at.define_relationship;
12049
12085
  }
@@ -12079,7 +12115,9 @@ var Environment = class extends Session {
12079
12115
  { path: modelPath }
12080
12116
  );
12081
12117
  if (result.errors?.length) {
12082
- throw new Error(`getRelationships failed: ${result.errors[0].message}`);
12118
+ throw new Error(
12119
+ `getRelationships failed: ${result.errors[0]?.message ?? "Unknown GraphQL error"}`
12120
+ );
12083
12121
  }
12084
12122
  return result.data?.model?.relationships || [];
12085
12123
  }
@@ -12118,7 +12156,7 @@ var Environment = class extends Session {
12118
12156
  { target: targetPath }
12119
12157
  );
12120
12158
  if (result.errors?.length) {
12121
- throw new Error(`attach failed: ${result.errors[0].message}`);
12159
+ throw new Error(`attach failed: ${result.errors[0]?.message ?? "Unknown GraphQL error"}`);
12122
12160
  }
12123
12161
  }
12124
12162
  /**
@@ -12153,7 +12191,7 @@ var Environment = class extends Session {
12153
12191
  }`
12154
12192
  );
12155
12193
  if (result.errors?.length) {
12156
- throw new Error(`detach failed: ${result.errors[0].message}`);
12194
+ throw new Error(`detach failed: ${result.errors[0]?.message ?? "Unknown GraphQL error"}`);
12157
12195
  }
12158
12196
  }
12159
12197
  /**
@@ -12180,7 +12218,9 @@ var Environment = class extends Session {
12180
12218
  }`
12181
12219
  );
12182
12220
  if (result.errors?.length) {
12183
- throw new Error(`listRelated failed: ${result.errors[0].message}`);
12221
+ throw new Error(
12222
+ `listRelated failed: ${result.errors[0]?.message ?? "Unknown GraphQL error"}`
12223
+ );
12184
12224
  }
12185
12225
  return result.data?.at?.at?.list_related || [];
12186
12226
  }
@@ -12273,7 +12313,7 @@ var Environment = class extends Session {
12273
12313
  async _runGraphql(query, label) {
12274
12314
  const result = await this.graphql(query);
12275
12315
  if (result.errors?.length) {
12276
- throw new Error(`${label}: ${result.errors[0].message}`);
12316
+ throw new Error(`${label}: ${result.errors[0]?.message ?? "Unknown GraphQL error"}`);
12277
12317
  }
12278
12318
  return result.data;
12279
12319
  }
@@ -12618,7 +12658,11 @@ var Environment = class extends Session {
12618
12658
  */
12619
12659
  async recordObject(options) {
12620
12660
  const results = await this.recordObjects([options]);
12621
- return results[0];
12661
+ const result = results[0];
12662
+ if (!result) {
12663
+ throw new Error("recordObject: no result returned for record");
12664
+ }
12665
+ return result;
12622
12666
  }
12623
12667
  /**
12624
12668
  * Batch version of `recordObject()`.
@@ -12670,7 +12714,13 @@ var Environment = class extends Session {
12670
12714
  );
12671
12715
  }
12672
12716
  for (let index = 0; index < items.length; index += 1) {
12673
- results[plan.offset + index] = items[index];
12717
+ const item = items[index];
12718
+ if (!item) {
12719
+ throw new Error(
12720
+ `recordObjects: chunk ${plan.chunkIndex + 1} returned an empty result at index ${index}`
12721
+ );
12722
+ }
12723
+ results[plan.offset + index] = item;
12674
12724
  }
12675
12725
  if (onChunk) {
12676
12726
  const info = {
@@ -12774,36 +12824,186 @@ var Environment = class extends Session {
12774
12824
  }
12775
12825
  );
12776
12826
  }
12777
- // ==================== PUBLISH TOOLS ====================
12827
+ };
12828
+ var EnvironmentSession = class extends Session {
12829
+ environment;
12830
+ /** The last known graph container status, updated by checkReadiness() or on heartbeat */
12831
+ graphContainerStatus = null;
12832
+ constructor(client, environment, clientId) {
12833
+ super(client, clientId);
12834
+ this.environment = environment;
12835
+ }
12836
+ get environmentId() {
12837
+ return this.environment.environmentId;
12838
+ }
12839
+ get sandboxId() {
12840
+ return this.environment.sandboxId;
12841
+ }
12842
+ get ontologyId() {
12843
+ return this.environment.ontologyId;
12844
+ }
12845
+ get subjectId() {
12846
+ return this.environment.subjectId;
12847
+ }
12848
+ get envName() {
12849
+ return this.environment.envName;
12850
+ }
12851
+ get versionId() {
12852
+ return this.environment.versionId;
12853
+ }
12854
+ get granularId() {
12855
+ return this.environment.granularId;
12856
+ }
12857
+ get permissionProfileId() {
12858
+ return this.environment.permissionProfileId;
12859
+ }
12860
+ get apiEndpoint() {
12861
+ return this.environment.apiEndpoint;
12862
+ }
12863
+ get data() {
12864
+ return this.environment.data;
12865
+ }
12866
+ get feedback() {
12867
+ return this.environment.feedback;
12868
+ }
12778
12869
  /**
12779
- * Removed: environment-scoped effect publication is no longer supported.
12870
+ * Return a plain JS snapshot of the synced session heap.
12780
12871
  */
12781
- async publishTools(tools, revision = "1.0.0") {
12782
- return super.publishTools(tools, revision);
12872
+ getHeap() {
12873
+ const doc = this.document;
12874
+ return normalizeHeapSnapshot(doc?.heap);
12875
+ }
12876
+ async graphql(query, variables) {
12877
+ return this.environment.graphql(query, variables);
12878
+ }
12879
+ async defineRelationship(options) {
12880
+ return this.environment.defineRelationship(options);
12881
+ }
12882
+ async getRelationships(modelPath) {
12883
+ return this.environment.getRelationships(modelPath);
12884
+ }
12885
+ async attach(modelPath, submodelPath, targetPath) {
12886
+ return this.environment.attach(modelPath, submodelPath, targetPath);
12887
+ }
12888
+ async detach(modelPath, submodelPath, targetPath) {
12889
+ return this.environment.detach(modelPath, submodelPath, targetPath);
12890
+ }
12891
+ async listRelated(modelPath, submodelPath) {
12892
+ return this.environment.listRelated(modelPath, submodelPath);
12893
+ }
12894
+ async applyManifest(manifest) {
12895
+ return this.environment.applyManifest(manifest);
12896
+ }
12897
+ async recordObject(options) {
12898
+ return this.environment.recordObject(options);
12899
+ }
12900
+ async recordObjects(records, options) {
12901
+ return this.environment.recordObjects(records, options);
12902
+ }
12903
+ async enqueueRecordImport(records, options = {}) {
12904
+ return this.environment.enqueueRecordImport(records, options);
12905
+ }
12906
+ async listRecordImports(status) {
12907
+ return this.environment.listRecordImports(status);
12908
+ }
12909
+ async getRecordImportSummary() {
12910
+ return this.environment.getRecordImportSummary();
12911
+ }
12912
+ async getAwaitingRecordCount() {
12913
+ return this.environment.getAwaitingRecordCount();
12914
+ }
12915
+ async getRecordImport(importId) {
12916
+ return this.environment.getRecordImport(importId);
12917
+ }
12918
+ async cancelRecordImport(importId) {
12919
+ return this.environment.cancelRecordImport(importId);
12920
+ }
12921
+ async listFeedback() {
12922
+ return this.environment.listFeedback();
12783
12923
  }
12784
12924
  /**
12785
- * Removed: environment-scoped effect publication is no longer supported.
12925
+ * Close the session and disconnect from the sandbox.
12926
+ *
12927
+ * Sends `client.goodbye` over WebSocket first, then issues an HTTP fallback
12928
+ * to the runtime goodbye endpoint if no definitive WS-side runtime notify
12929
+ * acknowledgement was observed.
12786
12930
  */
12787
- async publishEffect(effect) {
12788
- return super.publishEffect(effect);
12931
+ async disconnect() {
12932
+ let wsNotifiedRuntime = false;
12933
+ try {
12934
+ const goodbye = await this.rpc(
12935
+ "client.goodbye",
12936
+ {
12937
+ timestamp: Date.now()
12938
+ }
12939
+ );
12940
+ wsNotifiedRuntime = Boolean(goodbye?.ok && goodbye?.via);
12941
+ } catch {
12942
+ wsNotifiedRuntime = false;
12943
+ }
12944
+ if (!wsNotifiedRuntime) {
12945
+ try {
12946
+ await fetch(
12947
+ `${this.environment.runtimeBaseUrl}/orchestrator/runtime/environments/${this.environmentId}/session-goodbye`,
12948
+ {
12949
+ method: "POST",
12950
+ headers: {
12951
+ "Content-Type": "application/json",
12952
+ Authorization: `Bearer ${this.environment.authToken}`,
12953
+ Connection: "close"
12954
+ },
12955
+ body: JSON.stringify({
12956
+ reason: "sdk_disconnect_http_fallback",
12957
+ sessionId: this.client.currentSessionId
12958
+ })
12959
+ }
12960
+ );
12961
+ } catch {
12962
+ }
12963
+ }
12964
+ this.client.disconnect();
12789
12965
  }
12790
12966
  /**
12791
- * Removed: environment-scoped effect publication is no longer supported.
12967
+ * Close only the socket transport without sending `client.goodbye`.
12792
12968
  */
12793
- async publishEffects(effects) {
12794
- return super.publishEffects(effects);
12969
+ disconnectTransport() {
12970
+ this.client.disconnect();
12795
12971
  }
12796
12972
  /**
12797
- * Removed: environment-scoped effect publication is no longer supported.
12973
+ * Backwards-compatible alias for `disconnect()`.
12798
12974
  */
12799
- async unpublishEffect(name) {
12800
- return super.unpublishEffect(name);
12975
+ async close() {
12976
+ await this.disconnect();
12801
12977
  }
12802
12978
  /**
12803
- * Removed: environment-scoped effect publication is no longer supported.
12979
+ * Check if the graph container is ready and warm.
12804
12980
  */
12805
- async unpublishAllEffects() {
12806
- return super.unpublishAllEffects();
12981
+ async checkReadiness() {
12982
+ const result = await this.client.call("client.heartbeat", {});
12983
+ const containerStatus = result?.graphContainerStatus ?? {
12984
+ lastKeepAliveAt: Date.now(),
12985
+ status: "unknown"
12986
+ };
12987
+ this.graphContainerStatus = containerStatus;
12988
+ this.emit("readiness", containerStatus);
12989
+ return containerStatus;
12990
+ }
12991
+ };
12992
+ var OntologyHandle = class {
12993
+ granular;
12994
+ ontologyNameOrId;
12995
+ constructor(granular, ontologyNameOrId) {
12996
+ this.granular = granular;
12997
+ this.ontologyNameOrId = ontologyNameOrId;
12998
+ }
12999
+ get effects() {
13000
+ return {
13001
+ register: async (effect) => this.granular.registerEffect(this.ontologyNameOrId, effect),
13002
+ registerMany: async (effects) => this.granular.registerEffects(this.ontologyNameOrId, effects),
13003
+ unregister: async (name) => this.granular.unregisterEffect(this.ontologyNameOrId, name),
13004
+ clear: async () => this.granular.unregisterAllEffects(this.ontologyNameOrId),
13005
+ disconnect: async () => this.granular.disconnectEffects(this.ontologyNameOrId)
13006
+ };
12807
13007
  }
12808
13008
  };
12809
13009
  var Granular = class _Granular {
@@ -12840,6 +13040,12 @@ var Granular = class _Granular {
12840
13040
  this.onReconnectError = options.onReconnectError;
12841
13041
  this.httpUrl = this.apiUrl.replace(/^wss:\/\//, "https://").replace(/^ws:\/\//, "http://").replace(/\/ws$/, "");
12842
13042
  }
13043
+ /**
13044
+ * Return an ontology-scoped handle for effects and other ontology-level APIs.
13045
+ */
13046
+ ontology(ontologyNameOrId) {
13047
+ return new OntologyHandle(this, ontologyNameOrId);
13048
+ }
12843
13049
  /**
12844
13050
  * Records/upserts a user and prepares them for sandbox connections
12845
13051
  *
@@ -12876,7 +13082,23 @@ var Granular = class _Granular {
12876
13082
  permissions: options.permissions || []
12877
13083
  });
12878
13084
  }
13085
+ /**
13086
+ * Alias for `recordUser()` with user-facing naming that matches upsert semantics.
13087
+ */
13088
+ async upsertUser(options) {
13089
+ return this.recordUser(options);
13090
+ }
12879
13091
  async resolveConnectUser(options) {
13092
+ const providedIdentityCount = [
13093
+ Boolean(options.user),
13094
+ Boolean(options.userId),
13095
+ Boolean(options.granularId)
13096
+ ].filter(Boolean).length;
13097
+ if (providedIdentityCount !== 1) {
13098
+ throw new Error(
13099
+ "openEnvironment() requires exactly one of userId, granularId, or a user object returned by recordUser()."
13100
+ );
13101
+ }
12880
13102
  if (options.user) {
12881
13103
  const user = normalizeUser(options.user);
12882
13104
  return {
@@ -12913,56 +13135,85 @@ var Granular = class _Granular {
12913
13135
  };
12914
13136
  }
12915
13137
  throw new Error(
12916
- "connect() requires either userId, granularId, or a user object returned by recordUser()."
13138
+ "openEnvironment() requires either userId, granularId, or a user object returned by recordUser()."
12917
13139
  );
12918
13140
  }
12919
13141
  /**
12920
- * Connect to an ontology environment and establish a real-time session.
12921
- *
12922
- * Effects are registered at the sandbox level via `granular.registerEffect()`
12923
- * or `granular.registerEffects()`. Sessions pick up live availability from
12924
- * the sandbox registry automatically.
12925
- *
12926
- * @param options - Connection options
12927
- * @returns An active environment session
13142
+ * Open or resolve an ontology environment for one user without opening a session.
12928
13143
  *
12929
13144
  * @example
12930
13145
  * ```typescript
12931
- * const environment = await granular.connect({
13146
+ * const environment = await granular.openEnvironment({
12932
13147
  * ontology: 'my-ontology',
12933
- * environment: 'dev',
13148
+ * tag: 'dev',
12934
13149
  * userId: 'user_123',
12935
13150
  * permissions: ['agent'],
12936
13151
  * });
12937
13152
  *
12938
- * await granular.registerEffect('my-sandbox', {
12939
- * name: 'greet',
12940
- * description: 'Say hello',
12941
- * inputSchema: { type: 'object', properties: {} },
12942
- * handler: async () => 'Hello!',
13153
+ * await environment.data.record({
13154
+ * className: 'customer',
13155
+ * id: 'acme',
13156
+ * fields: { name: 'Acme' },
12943
13157
  * });
12944
13158
  *
12945
- * // Submit job
12946
- * const job = await environment.submitJob(`
12947
- * import { tools } from './sandbox-tools';
12948
- * return await tools.greet({});
12949
- * `);
12950
- *
12951
- * console.log(await job.result); // 'Hello!'
13159
+ * const session = await environment.sessions.create();
13160
+ * const job = await session.submitJob(`return "hello";`);
13161
+ * console.log(await job.result);
12952
13162
  * ```
12953
13163
  */
13164
+ async openEnvironment(options) {
13165
+ const envData = await this.resolveOpenEnvironmentData(
13166
+ options,
13167
+ "openEnvironment"
13168
+ );
13169
+ return this.bindEnvironmentHandle(envData);
13170
+ }
13171
+ /**
13172
+ * Deprecated compatibility alias for `openEnvironment()`.
13173
+ *
13174
+ * `connect()` no longer opens a runtime session automatically.
13175
+ */
12954
13176
  async connect(options) {
12955
- const clientId = options.clientId || `client_${Date.now()}`;
13177
+ return this.openEnvironment({
13178
+ ...options,
13179
+ tag: this.resolveRequestedTag(options, "connect"),
13180
+ permissions: options.permissions || options.user?.permissions || []
13181
+ });
13182
+ }
13183
+ resolveRequestedTag(options, methodName) {
13184
+ const tag = options.tag?.trim() || options.tagName?.trim() || options.environment?.trim();
13185
+ if (!tag) {
13186
+ throw new Error(`${methodName}() requires \`tag\`.`);
13187
+ }
13188
+ return tag;
13189
+ }
13190
+ buildManagedEnvironmentName(tag, versionId) {
13191
+ return `__sdk__${tag}__${versionId}`;
13192
+ }
13193
+ matchesTagTrackedEnvironment(environment, tagName, tagId) {
13194
+ const environmentTagName = environment.tag?.name || environment.buildPolicy.tagName || null;
13195
+ 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);
13196
+ }
13197
+ sortEnvironmentsByRecency(environments) {
13198
+ return [...environments].sort(
13199
+ (left, right) => right.updatedAt - left.updatedAt
13200
+ );
13201
+ }
13202
+ async resolveOpenEnvironmentData(options, methodName) {
12956
13203
  const ontology = options.ontology;
12957
13204
  if (!ontology) {
12958
- throw new Error("connect() requires `ontology`.");
13205
+ throw new Error(`${methodName}() requires \`ontology\`.`);
12959
13206
  }
12960
- const environmentName = options.environment;
12961
- if (!environmentName) {
12962
- throw new Error("connect() requires `environment`.");
13207
+ const tagName = options.tag?.trim();
13208
+ if (!tagName) {
13209
+ throw new Error(`${methodName}() requires \`tag\`.`);
12963
13210
  }
12964
- const tagName = options.tagName?.trim() || void 0;
12965
13211
  const user = await this.resolveConnectUser(options);
13212
+ if (!Array.isArray(user.permissions) || user.permissions.length === 0) {
13213
+ throw new Error(
13214
+ `${methodName}() requires at least one permission so the SDK can ensure assignments for new users.`
13215
+ );
13216
+ }
12966
13217
  const sandbox = await this.findOrCreateSandbox(ontology);
12967
13218
  for (const profileName of user.permissions) {
12968
13219
  const profileId = await this.ensurePermissionProfile(
@@ -12975,22 +13226,49 @@ var Granular = class _Granular {
12975
13226
  profileId
12976
13227
  );
12977
13228
  }
12978
- const envData = await this.environments.create(sandbox.sandboxId, {
13229
+ const tags = await this.request(
13230
+ `/control/sandboxes/${sandbox.sandboxId}/tags`
13231
+ );
13232
+ const tag = (tags.items || []).find(
13233
+ (candidate) => Boolean(candidate?.name === tagName)
13234
+ );
13235
+ if (!tag) {
13236
+ throw new Error(
13237
+ `Tag "${tagName}" was not found for ontology ${sandbox.sandboxId}.`
13238
+ );
13239
+ }
13240
+ const targetVersionId = tag.targetVersionId || tag.targetBuildId;
13241
+ if (!targetVersionId) {
13242
+ throw new Error(
13243
+ `Tag "${tagName}" does not currently point to a build/version.`
13244
+ );
13245
+ }
13246
+ const allEnvironments = await this.environments.list(sandbox.sandboxId);
13247
+ const userEnvironments = allEnvironments.filter(
13248
+ (environment) => environment.subjectId === user.granularId
13249
+ );
13250
+ const currentMatches = this.sortEnvironmentsByRecency(
13251
+ userEnvironments.filter(
13252
+ (environment) => this.matchesTagTrackedEnvironment(environment, tagName, tag.tagId) && environment.versionId === targetVersionId
13253
+ )
13254
+ );
13255
+ if (currentMatches.length > 0) {
13256
+ return currentMatches[0];
13257
+ }
13258
+ const outdatedMatches = this.sortEnvironmentsByRecency(
13259
+ userEnvironments.filter(
13260
+ (environment) => this.matchesTagTrackedEnvironment(environment, tagName, tag.tagId)
13261
+ )
13262
+ );
13263
+ if (outdatedMatches.length > 0 && options.createFreshIfOutdated !== true) {
13264
+ return outdatedMatches[0];
13265
+ }
13266
+ return this.environments.create(sandbox.sandboxId, {
12979
13267
  subjectId: user.granularId,
12980
- environment: environmentName,
12981
- tagName,
13268
+ environment: this.buildManagedEnvironmentName(tagName, targetVersionId),
13269
+ tagId: tag.tagId,
12982
13270
  permissionProfileId: null
12983
13271
  });
12984
- await this.activateEnvironment(envData.environmentId);
12985
- const session = await this.request("/ws/sessions", {
12986
- method: "POST",
12987
- body: JSON.stringify({
12988
- environmentId: envData.environmentId,
12989
- clientId,
12990
- initialHeap: options.initialHeap
12991
- })
12992
- });
12993
- return this.bindWebSocketEnvironment(envData, clientId, session);
12994
13272
  }
12995
13273
  /**
12996
13274
  * List active (open) sessions for an environment — each session is one agent conversation thread.
@@ -13053,6 +13331,7 @@ var Granular = class _Granular {
13053
13331
  const clientId = options.clientId || `client_${Date.now()}`;
13054
13332
  await this.activateEnvironment(options.environmentId);
13055
13333
  const envData = await this.environments.get(options.environmentId);
13334
+ const environment = this.bindEnvironmentHandle(envData);
13056
13335
  const session = await this.request("/ws/sessions", {
13057
13336
  method: "POST",
13058
13337
  body: JSON.stringify({
@@ -13061,7 +13340,7 @@ var Granular = class _Granular {
13061
13340
  initialHeap: options.initialHeap
13062
13341
  })
13063
13342
  });
13064
- return this.bindWebSocketEnvironment(envData, clientId, session);
13343
+ return this.bindWebSocketEnvironmentSession(environment, clientId, session);
13065
13344
  }
13066
13345
  /**
13067
13346
  * Connect to an existing open session (same conversation thread) using a freshly minted WebSocket token.
@@ -13073,7 +13352,8 @@ var Granular = class _Granular {
13073
13352
  body: JSON.stringify({})
13074
13353
  });
13075
13354
  const envData = await this.environments.get(minted.environmentId);
13076
- return this.bindWebSocketEnvironment(envData, clientId, minted);
13355
+ const environment = this.bindEnvironmentHandle(envData);
13356
+ return this.bindWebSocketEnvironmentSession(environment, clientId, minted);
13077
13357
  }
13078
13358
  /**
13079
13359
  * Mark a session closed in the control plane. If `environment` is the connected handle for that
@@ -13105,7 +13385,11 @@ var Granular = class _Granular {
13105
13385
  });
13106
13386
  return this.connectSession({ sessionId, clientId: options?.clientId });
13107
13387
  }
13108
- async bindWebSocketEnvironment(envData, clientId, session) {
13388
+ bindEnvironmentHandle(envData) {
13389
+ const graphqlEndpoint = `${this.httpUrl}/orchestrator/graphql`;
13390
+ return new Environment(this, envData, this.apiKey, graphqlEndpoint);
13391
+ }
13392
+ async bindWebSocketEnvironmentSession(environment, clientId, session) {
13109
13393
  const client = new WSClient({
13110
13394
  url: session.wsUrl,
13111
13395
  sessionId: session.sessionId,
@@ -13116,16 +13400,13 @@ var Granular = class _Granular {
13116
13400
  onReconnectError: this.onReconnectError
13117
13401
  });
13118
13402
  await client.connect();
13119
- const graphqlEndpoint = `${this.httpUrl}/orchestrator/graphql`;
13120
- const environment = new Environment(
13403
+ const environmentSession = new EnvironmentSession(
13121
13404
  client,
13122
- envData,
13123
- clientId,
13124
- this.apiKey,
13125
- graphqlEndpoint
13405
+ environment,
13406
+ clientId
13126
13407
  );
13127
- await environment.hello();
13128
- return environment;
13408
+ await environmentSession.hello();
13409
+ return environmentSession;
13129
13410
  }
13130
13411
  async activateEnvironment(environmentId) {
13131
13412
  await this.request(`/orchestrator/runtime/environments/${environmentId}/activate`, {
@@ -15516,6 +15797,6 @@ function buildSessionTranscript(input) {
15516
15797
  });
15517
15798
  }
15518
15799
 
15519
- export { Environment, Granular, Session, WSClient, buildContinuationInstruction, buildGranularAgentCheckpointBlock, buildGranularAgentDomainBlock, buildGranularAgentHeapBlock, buildGranularAgentLoopBlock, buildGranularAgentReferentBlock, buildGranularAgentSessionBlock, buildGranularAgentSystemPrompt, buildGranularAgentToolBlock, buildGranularAgentWorkflowBlock, buildSessionTranscript, createHarnessVerifierSnapshot, evaluateContinuation, extractPromptTokens, getCurrentClosureId, getExclusivePromptTarget, hasOpenPrompt, invokeRegisteredEffect, isLocalApiUrl, normalizeEffectBehaviors, normalizePrompt, normalizePromptText, normalizePromptType, projectConversationReferentFocus, projectConversationReferentSummary, projectHeapSummary, projectLoopSummary, projectWorkflowFocus, projectWorkflowSummary, resolveApiUrl, resolveAuthTokenForApiUrl, resolveJobPresentation, resolvePromptAnswer, reviewGeneratedJobCode, scorePromptChoiceMatch };
15800
+ export { Environment, EnvironmentSession, Granular, OntologyHandle, Session, WSClient, buildContinuationInstruction, buildGranularAgentCheckpointBlock, buildGranularAgentDomainBlock, buildGranularAgentHeapBlock, buildGranularAgentLoopBlock, buildGranularAgentReferentBlock, buildGranularAgentSessionBlock, buildGranularAgentSystemPrompt, buildGranularAgentToolBlock, buildGranularAgentWorkflowBlock, buildSessionTranscript, createHarnessVerifierSnapshot, evaluateContinuation, extractPromptTokens, getCurrentClosureId, getExclusivePromptTarget, hasOpenPrompt, invokeRegisteredEffect, isLocalApiUrl, normalizeEffectBehaviors, normalizePrompt, normalizePromptText, normalizePromptType, projectConversationReferentFocus, projectConversationReferentSummary, projectHeapSummary, projectLoopSummary, projectWorkflowFocus, projectWorkflowSummary, resolveApiUrl, resolveAuthTokenForApiUrl, resolveJobPresentation, resolvePromptAnswer, reviewGeneratedJobCode, scorePromptChoiceMatch };
15520
15801
  //# sourceMappingURL=index.mjs.map
15521
15802
  //# sourceMappingURL=index.mjs.map