@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/cli/index.js CHANGED
@@ -6597,9 +6597,9 @@ async function main() {
6597
6597
  });
6598
6598
 
6599
6599
  // Connect as one app user so the SDK can create an environment for the seed run.
6600
- const env = await granular.connect({
6600
+ const env = await granular.openEnvironment({
6601
6601
  ontology: SANDBOX_ID,
6602
- environment: 'dev',
6602
+ tag: 'dev',
6603
6603
  userId: '${template.seedUser.userId}',
6604
6604
  name: '${template.seedUser.name}',
6605
6605
  email: '${template.seedUser.email}',
@@ -6621,7 +6621,7 @@ async function main() {
6621
6621
  const updatedCount = results.length - createdCount;
6622
6622
  console.log(\`[Seed] Created \${createdCount} records, updated \${updatedCount} records.\`);
6623
6623
  } finally {
6624
- await env.disconnect();
6624
+ await session.disconnect();
6625
6625
  }
6626
6626
 
6627
6627
  console.log(\`Seeded \${records.length} records into \${SANDBOX_ID}\`);
@@ -6670,7 +6670,7 @@ async function main() {
6670
6670
  apiUrl: process.env.GRANULAR_API_URL ?? API_URL,
6671
6671
  });
6672
6672
 
6673
- await granular.registerEffects(SANDBOX_ID, [
6673
+ await granular.ontology(SANDBOX_ID).effects.registerMany([
6674
6674
  ${registrations}
6675
6675
  ]);
6676
6676
 
@@ -6688,7 +6688,7 @@ ${registrations}
6688
6688
  shuttingDown = true;
6689
6689
  clearInterval(keepAliveTimer);
6690
6690
  console.log(\`Shutting down after \${signal}...\`);
6691
- await granular.disconnectEffects(SANDBOX_ID);
6691
+ await granular.ontology(SANDBOX_ID).effects.disconnect();
6692
6692
  resolve();
6693
6693
  process.exit(0);
6694
6694
  };
@@ -6738,9 +6738,9 @@ test('granular connect + submitJob smoke test', {
6738
6738
  apiUrl: process.env.GRANULAR_API_URL ?? rc.apiUrl ?? DEFAULT_API_URL,
6739
6739
  });
6740
6740
 
6741
- const environment = await granular.connect({
6741
+ const environment = await granular.openEnvironment({
6742
6742
  ontology: rc.sandboxId,
6743
- environment: 'dev',
6743
+ tag: 'dev',
6744
6744
  userId: '${template.seedUser.userId}',
6745
6745
  name: '${template.seedUser.name}',
6746
6746
  email: '${template.seedUser.email}',
@@ -6751,7 +6751,8 @@ test('granular connect + submitJob smoke test', {
6751
6751
  assert.ok(environment.environmentId);
6752
6752
  assert.ok(environment.sessionId);
6753
6753
 
6754
- const job = await environment.submitJob(\`
6754
+ const session = await environment.sessions.create();
6755
+ const job = await session.submitJob(\`
6755
6756
  return {
6756
6757
  ok: true,
6757
6758
  template: '${template.id}',
@@ -6762,7 +6763,7 @@ return {
6762
6763
  assert.equal(result.ok, true);
6763
6764
  assert.equal(result.template, '${template.id}');
6764
6765
  } finally {
6765
- await environment.disconnect();
6766
+ await session.disconnect();
6766
6767
  }
6767
6768
  });
6768
6769
  `;
@@ -8960,7 +8961,7 @@ function manifestGuideGranularProductSection() {
8960
8961
  2. **Build** that declaration from \`granular.json\` so the service compiles types and tooling for a **sandbox** (a workspace identified by \`sbx_\u2026\`).
8961
8962
  3. **Store** **records** (instances of your classes) and **run jobs** (code executed in Granular\u2019s **sandbox runtime**) that call generated helpers and effects.
8962
8963
 
8963
- **Split of responsibility:** Data and domain rules live in Granular; **secrets, payment providers, email, and arbitrary HTTP** stay in **your** server via **effect handlers** you register with \`registerEffects\`.
8964
+ **Split of responsibility:** Data and domain rules live in Granular; **secrets, payment providers, email, and arbitrary HTTP** stay in **your** server via **effect handlers** you register with \`granular.ontology(sandboxId).effects.registerMany\`.
8964
8965
 
8965
8966
  ---
8966
8967
 
@@ -8985,10 +8986,10 @@ The rest of this guide assumes these terms.
8985
8986
  | **Manifest** | \`granular.json\` (definition under \`manifest\`). **Source** in the repo until you create a new ontology **version** from it. |
8986
8987
  | **Version** | Immutable ontology state derived from one manifest revision. Tags like \`dev\` and \`prod\` point to versions. |
8987
8988
  | **Build run** | The CI-style compilation process. \`granular build\` creates or reuses the ontology version, then runs a build for it. |
8988
- | **Environment** | Return value of \`granular.connect({ ontology, environment, \u2026 })\`. It is your **server-side** handle for \`recordObject\`, \`submitJob\`, \`graphql\`, etc., bound to one ontology environment slot. |
8989
- | **Session** | SDK base type; \`Environment\` **extends** \`Session\`. Job/prompt APIs live on \`Session\`; you still use the **\`Environment\`** instance from \`connect()\` in normal apps. |
8990
- | **Job** | Code string passed to \`environment.submitJob(code)\`. Runs in Granular\u2019s **sandbox runtime** with access to \`./sandbox-tools\` (generated classes + effect entrypoints). |
8991
- | **Effect** | Declared with \`withEffect\`; **handler** registered with \`registerEffects\` in **your** process. Jobs invoke effects; handlers do IO outside Granular. |
8989
+ | **Environment** | Return value of \`granular.openEnvironment({ ontology, tag, userId, \u2026 })\`. It is your **server-side** handle for \`recordObject\`, \`recordObjects\`, \`graphql\`, and \`sessions.create()\`. |
8990
+ | **Session** | Live runtime connection opened from an environment with \`environment.sessions.create()\` or \`environment.sessions.connect()\`. Job, prompt, and streaming APIs live here. |
8991
+ | **Job** | Code string passed to \`session.submitJob(code)\`. Runs in Granular\u2019s **sandbox runtime** with access to \`./sandbox-tools\` (generated classes + effect entrypoints). |
8992
+ | **Effect** | Declared with \`withEffect\`; **handler** registered in **your** process with \`granular.ontology(sandboxId).effects.registerMany(...)\`. Jobs invoke effects; handlers do IO outside Granular. |
8992
8993
  | **Class** | Entity **kind** in the domain (e.g. \`book\`) with \`has\` fields in the manifest. |
8993
8994
  | **Record / object** | One **instance**: \`className\`, \`id\`, \`fields\`, \`relationships\`. Use \`recordObject\` for one upsert, \`recordObjects\` for several immediate upserts, and record-import APIs for async bulk loads. |
8994
8995
  | **Relationship** | Declared link between two classes; defines **property names** and cardinality. Keys appear in \`recordObject({ relationships })\`. |
@@ -9007,10 +9008,10 @@ function manifestGuideEndToEndSection() {
9007
9008
  |------|--------|---------|
9008
9009
  | 1 | Edit \`granular.json\` (\`manifest\` \u2192 \`volumes\` \u2192 \`operations\`) | Domain **source** in the repo |
9009
9010
  | 2 | \`granular build\` | Manifest **uploaded**; ontology version created or reused; build run compiles it; errors surface here |
9010
- | 3 | (Optional) Run your **effects host** \u2014 e.g. \`npx tsx granular-effects.ts\` \u2014 calling \`registerEffects(sandboxId, \u2026)\` | Effect **handlers** attached in **your** process |
9011
- | 4 | \`new Granular({ apiKey })\` then \`connect({ ontology, environment, userId, permissions })\` | **\`Environment\`** for that ontology environment slot |
9011
+ | 3 | (Optional) Run your **effects host** \u2014 e.g. \`npx tsx granular-effects.ts\` \u2014 calling \`granular.ontology(sandboxId).effects.registerMany(...)\` | Effect **handlers** attached in **your** process |
9012
+ | 4 | \`new Granular({ apiKey })\` then \`openEnvironment({ ontology, tag, userId, permissions })\` | **\`Environment\`** handle for that ontology/user pair |
9012
9013
  | 5 | \`recordObject\` / \`recordObjects\` | **Records** stored with correct fields and relationship keys. Use \`recordObject\` for one targeted write. Use \`recordObjects\` for one synchronous multi-record write with chunking (default 100 rows/request), retries, and optional \`onChunkComplete\` / \`concurrency\` progress. For async bulk loads use \`enqueueRecordImport\` + import status APIs. |
9013
- | 6 | \`submitJob(\`\u2026\`)\` with imports from \`./sandbox-tools\` | **Jobs** run; may call **effects** \u2192 your handlers execute and return results |
9014
+ | 6 | \`const session = await environment.sessions.create();\` then \`session.submitJob(\`\u2026\`)\` | **Jobs** run; may call **effects** \u2192 your handlers execute and return results |
9014
9015
 
9015
9016
  **Accuracy tip:** After changing the manifest, always **re-build** before assuming generated types, \`./sandbox-tools\` names, or effect signatures match the file on disk.`;
9016
9017
  }
@@ -9024,9 +9025,9 @@ function sandboxDocMainConceptsSection() {
9024
9025
  | **Manifest** | \`granular.json\` \u2014 **source**; edit here, then \`granular build\`. |
9025
9026
  | **Version** | Immutable ontology state created from one manifest revision. |
9026
9027
  | **Build run** | Compilation run that validates and materializes a version. |
9027
- | **Environment** | \`connect()\` result \u2014 **record**, **submitJob**, **graphql** for this sandbox. |
9028
- | **Session** | Base type; \`Environment\` extends it (job APIs). |
9029
- | **Job** | \`submitJob\` code using \`./sandbox-tools\`. |
9028
+ | **Environment** | \`openEnvironment()\` result \u2014 **record**, **recordObjects**, **graphql**, and **session lifecycle** for this sandbox. |
9029
+ | **Session** | Live runtime connection created from an environment; job and prompt APIs live here. |
9030
+ | **Job** | \`session.submitJob(...)\` code using \`./sandbox-tools\`. |
9030
9031
  | **Effect** | Declared in manifest; **handler** in your process. |
9031
9032
 
9032
9033
  **Full product + manifest how-to:** [docs/granular-manifest.md](docs/granular-manifest.md).`;
@@ -14860,7 +14861,7 @@ function generateManifestAgentGuide(options) {
14860
14861
  );
14861
14862
  return `# Granular manifest guide (for coding agents)
14862
14863
 
14863
- This guide is for **coding agents** who may have **no prior context on Granular**. It explains the product, core terms, how to **author** \`granular.json\`, **build** the domain, and use the SDK (\`connect\` \u2192 **environment**, \`recordObject\`, \`submitJob\`, **effects**). For **this repository\u2019s** exact class names, relationship keys, and effect payloads only, see [GRANULAR_SANDBOX.md](../GRANULAR_SANDBOX.md).
14864
+ This guide is for **coding agents** who may have **no prior context on Granular**. It explains the product, core terms, how to **author** \`granular.json\`, **build** the domain, and use the SDK (\`openEnvironment\` \u2192 **environment**, \`environment.sessions.create()\` \u2192 **session**, \`recordObject\`, **effects**). For **this repository\u2019s** exact class names, relationship keys, and effect payloads only, see [GRANULAR_SANDBOX.md](../GRANULAR_SANDBOX.md).
14864
14865
 
14865
14866
  **Precedence:** User instructions in chat override this file. When in doubt, read \`granular.json\` and [AGENTS.md](../AGENTS.md).
14866
14867
 
@@ -15118,14 +15119,14 @@ Example:
15118
15119
 
15119
15120
  | Field | Meaning |
15120
15121
  |-------|---------|
15121
- | \`name\` | Must match the \`name\` in \`registerEffects\`. |
15122
+ | \`name\` | Must match the handler name you register from your effect host. |
15122
15123
  | \`attachedClass\` | Omit for **global** effects. Set for class-bound effects. |
15123
15124
  | \`isStatic\` | \`true\` \u2192 static method; \`false\` or omit \u2192 **instance** method (handler receives the object **id** first). |
15124
15125
  | \`inputSchema\` / \`outputSchema\` | JSON Schema; used for codegen and validation. |
15125
15126
 
15126
- **Lifecycle:** declare in manifest \u2192 \`granular build\` creates or reuses a version and runs the build \u2192 implement handlers \u2192 \`granular.registerEffects(sandboxId, [...])\` \u2192 jobs call generated methods \u2192 your handler runs and returns the result to the job.
15127
+ **Lifecycle:** declare in manifest \u2192 \`granular build\` creates or reuses a version and runs the build \u2192 implement handlers \u2192 \`granular.ontology(sandboxId).effects.registerMany([...])\` \u2192 jobs call generated methods \u2192 your handler runs and returns the result to the job.
15127
15128
 
15128
- **Permissions:** \`connect({ permissions: [...] })\` assigns profiles (e.g. \`default\`) that can allow or deny which effects a user may call.
15129
+ **Permissions:** \`openEnvironment({ permissions: [...] })\` assigns profiles (e.g. \`default\`) that can allow or deny which effects a user may call.
15129
15130
 
15130
15131
  ### Effect metamodels
15131
15132
 
@@ -15191,30 +15192,38 @@ ${effectMetamodelTable}
15191
15192
  | API | Purpose |
15192
15193
  |-----|---------|
15193
15194
  | \`new Granular({ apiKey, apiUrl?, endpointMode?, token?, tokenProvider?, \u2026 })\` | Auth. Env: \`GRANULAR_API_KEY\`, \`GRANULAR_API_URL\`, \`GRANULAR_ENDPOINT_MODE\`. |
15194
- | \`recordUser({ userId, name?, email?, permissions? })\` | Upsert user for later \`connect\`. |
15195
- | \`connect({ ontology, environment, tagName?, userId?, granularId?, user?, permissions?, clientId?, initialHeap? })\` | Opens a session \u2192 **\`Environment\`**. \`tagName\` is an advanced override. |
15196
- | \`registerEffects(sandboxId, effects)\` / \`registerEffect\` | Register handlers for manifest \`withEffect\` declarations. |
15197
- | \`unregisterEffect\` / \`unregisterAllEffects\` / \`disconnectEffects\` | Stop effect handlers for a sandbox. |
15195
+ | \`recordUser({ userId, name?, email?, permissions? })\` | Upsert user for later \`openEnvironment\`. |
15196
+ | \`openEnvironment({ ontology, tag?, userId?, granularId?, user?, permissions, createFreshIfOutdated?, ... })\` | Resolves or creates an **\`Environment\`** handle. |
15197
+ | \`granular.ontology(sandboxId).effects.registerMany(effects)\` / \`.register(effect)\` | Register handlers for manifest \`withEffect\` declarations. |
15198
+ | \`granular.ontology(sandboxId).effects.unregister(name)\` / \`.clear()\` / \`.disconnect()\` | Stop effect handlers for a sandbox. |
15198
15199
  | \`granular.sandboxes\` | \`.list()\`, \`.get\`, \`.create\`, \`.update\`, \`.delete\` |
15199
15200
  | \`granular.permissionProfiles\` | Permission profiles per sandbox. |
15200
- | \`granular.environments\` | List/create/delete environments (usually use \`connect()\`). |
15201
+ | \`granular.environments\` | List/create/delete environments (advanced control-plane API). |
15201
15202
  | \`granular.subjects\` | Subjects / assignments (see typings). |
15202
15203
 
15203
- ### \`Environment\` (connected session)
15204
+ ### \`Environment\` (sessionless environment handle)
15204
15205
 
15205
15206
  | API | Purpose |
15206
15207
  |-----|---------|
15207
- | \`environmentId\`, \`sandboxId\`, \`apiEndpoint\`, \u2026 | Session context. |
15208
+ | \`environmentId\`, \`sandboxId\`, \`apiEndpoint\`, \u2026 | Environment context. |
15208
15209
  | \`applyManifest(manifest)\` | Apply manifest operations at runtime (alternative to CLI build for dynamic ontologies). |
15209
15210
  | \`recordObject\` / \`recordObjects\` | Upsert instances and relationships. Use \`recordObject\` for one object or a targeted patch. Use \`recordObjects\` for synchronous multi-record ingestion with chunked HTTP writes (default 100 rows/chunk), retries, and optional \`onChunkComplete\`, \`batchSize\`, \`concurrency\`. |
15210
15211
  | \`enqueueRecordImport\`, \`listRecordImports\`, \`getRecordImport\`, \`getRecordImportSummary\`, \u2026 | **Async** bulk import (worker queue + aggregate progress). Prefer when loads are huge, returning an \`importId\` is enough up front, and background processing is acceptable. |
15211
15212
  | \`graphql(query, variables?)\` | **GraphQL** \u2014 see dedicated subsection below. |
15212
15213
  | \`defineRelationship\`, \`getRelationships\`, \`attach\`, \`detach\`, \`listRelated\` | Imperative relationship operations (same ideas as manifest \`defineRelationship\`). |
15214
+ | \`sessions.list()\`, \`sessions.create()\`, \`sessions.connect()\`, \`sessions.reopen()\`, \`sessions.close()\` | Session lifecycle for this environment. |
15215
+
15216
+ ### \`Session\` (live runtime connection)
15217
+
15218
+ | API | Purpose |
15219
+ |-----|---------|
15213
15220
  | \`submitJob(code)\` | Run code in the sandbox; import from \`./sandbox-tools\`. |
15221
+ | \`answerPrompt(...)\`, \`appendMessage(...)\` | Human-in-the-loop and conversation APIs. |
15214
15222
  | \`getDomain()\`, \`getDomainTypes()\`, \`getDomainDocs()\`, \`getDomainDocumentation()\` | Domain summary and generated TypeScript / docs. |
15215
- | \`getEffects()\` / \`getTools()\`, \`onEffectsChanged()\` | Effect catalog and updates. |
15216
- | \`checkReadiness()\`, \`on('readiness', \u2026)\` | Environment readiness. |
15223
+ | \`getEffects()\` / \`getTools()\`, \`session.on("effects:changed", ...)\` | Effect catalog and live updates. |
15224
+ | \`checkReadiness()\`, \`on('readiness', ...)\` | Runtime readiness. |
15217
15225
  | \`getHeap()\` | Session state snapshot (advanced). |
15226
+ | \`disconnect()\` | Close the live runtime session. |
15218
15227
  | \`rpc(method, params)\` | Low-level session RPC (advanced). |
15219
15228
  | \`disconnect()\` | End the session. |
15220
15229
 
@@ -15414,9 +15423,9 @@ function generateSandboxAgentDoc(manifest, meta) {
15414
15423
  lines.push(` // apiUrl: process.env.GRANULAR_API_URL, // optional`);
15415
15424
  lines.push(`});`);
15416
15425
  lines.push("");
15417
- lines.push(`const env = await granular.connect({`);
15426
+ lines.push(`const env = await granular.openEnvironment({`);
15418
15427
  lines.push(` ontology: '${meta.sandboxId}',`);
15419
- lines.push(` environment: 'dev',`);
15428
+ lines.push(` tag: 'dev',`);
15420
15429
  lines.push(` userId: 'your_app_user_id',`);
15421
15430
  lines.push(` permissions: ['default'],`);
15422
15431
  lines.push(`});`);
@@ -15535,7 +15544,7 @@ function generateSandboxAgentDoc(manifest, meta) {
15535
15544
  "1. **Build** the manifest so the domain package includes the effect (`granular build`)."
15536
15545
  );
15537
15546
  lines.push(
15538
- "2. **Register** a handler whose `name` (and `className` / `static` when applicable) matches \u2014 use `granular.registerEffects('" + meta.sandboxId + "', [...])` in your host process (see `granular-effects.ts` in starter projects)."
15547
+ "2. **Register** a handler whose `name` (and `className` / `static` when applicable) matches \u2014 use `granular.ontology('" + meta.sandboxId + "').effects.registerMany([...])` in your host process (see `granular-effects.ts` in starter projects)."
15539
15548
  );
15540
15549
  lines.push(
15541
15550
  "3. **Invoke** from sandbox code via `./sandbox-tools` inside `submitJob`; the handler you registered runs in your process and the result returns to the job."
@@ -15603,7 +15612,8 @@ function generateSandboxAgentDoc(manifest, meta) {
15603
15612
  const globalEffects = effects2.filter((e) => !e.attachedClass);
15604
15613
  const firstGlobal = globalEffects[0]?.name;
15605
15614
  lines.push("```typescript");
15606
- lines.push(`const job = await env.submitJob(\``);
15615
+ lines.push(`const session = await env.sessions.create();
15616
+ const job = await session.submitJob(\``);
15607
15617
  const importList = [...pascalImports, ...effectNames].filter(
15608
15618
  (v, i, a) => a.indexOf(v) === i
15609
15619
  );
@@ -15682,7 +15692,7 @@ function generateSandboxAgentDoc(manifest, meta) {
15682
15692
  "| Domain | `getDomain`, `getDomainTypes`, `getDomainDocumentation` |"
15683
15693
  );
15684
15694
  lines.push(
15685
- "| Effects | `registerEffects`, `getEffects`, `onEffectsChanged` |"
15695
+ '| Effects | `granular.ontology(...).effects.registerMany`, `session.getEffects()`, `session.on("effects:changed", ...)` |'
15686
15696
  );
15687
15697
  lines.push("| Ops | `checkReadiness`, `getHeap`, `rpc` |");
15688
15698
  lines.push("");
@@ -17769,7 +17779,11 @@ var WSClient = class {
17769
17779
  return null;
17770
17780
  }
17771
17781
  try {
17772
- const payloadRaw = this.decodeBase64Url(parts[1]);
17782
+ const payloadSegment = parts[1];
17783
+ if (!payloadSegment) {
17784
+ return null;
17785
+ }
17786
+ const payloadRaw = this.decodeBase64Url(payloadSegment);
17773
17787
  const payload = JSON.parse(payloadRaw);
17774
17788
  if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp)) {
17775
17789
  return null;
@@ -18433,27 +18447,27 @@ var Session = class {
18433
18447
  }
18434
18448
  async publishTools(tools, revision = "1.0.0") {
18435
18449
  throw new Error(
18436
- "Environment-scoped effect publication was removed. Declare effects in the manifest and register live handlers with granular.registerEffects(environment.sandboxId, effects)."
18450
+ "Environment-scoped effect publication was removed. Declare effects in the manifest and register live handlers with granular.ontology(environment.sandboxId).effects.registerMany(effects)."
18437
18451
  );
18438
18452
  }
18439
18453
  async publishEffect(effect) {
18440
18454
  throw new Error(
18441
- "Environment-scoped effect publication was removed. Use granular.registerEffect(environment.sandboxId, effect)."
18455
+ "Environment-scoped effect publication was removed. Use granular.ontology(environment.sandboxId).effects.register(effect)."
18442
18456
  );
18443
18457
  }
18444
18458
  async publishEffects(effects2) {
18445
18459
  throw new Error(
18446
- "Environment-scoped effect publication was removed. Use granular.registerEffects(environment.sandboxId, effects)."
18460
+ "Environment-scoped effect publication was removed. Use granular.ontology(environment.sandboxId).effects.registerMany(effects)."
18447
18461
  );
18448
18462
  }
18449
18463
  async unpublishEffect(name) {
18450
18464
  throw new Error(
18451
- "Environment-scoped effect publication was removed. Use granular.unregisterEffect(environment.sandboxId, effectName)."
18465
+ "Environment-scoped effect publication was removed. Use granular.ontology(environment.sandboxId).effects.unregister(effectName)."
18452
18466
  );
18453
18467
  }
18454
18468
  async unpublishAllEffects() {
18455
18469
  throw new Error(
18456
- "Environment-scoped effect publication was removed. Use granular.unregisterAllEffects(environment.sandboxId)."
18470
+ "Environment-scoped effect publication was removed. Use granular.ontology(environment.sandboxId).effects.clear()."
18457
18471
  );
18458
18472
  }
18459
18473
  /**
@@ -18846,6 +18860,7 @@ import { ${allImports} } from "./sandbox-tools";
18846
18860
  this.eventListeners.set(event, []);
18847
18861
  }
18848
18862
  this.eventListeners.get(event).push(handler);
18863
+ return () => this.off(event, handler);
18849
18864
  }
18850
18865
  /**
18851
18866
  * Unsubscribe from session events
@@ -19302,6 +19317,16 @@ var JobImplementation = class {
19302
19317
  handler(message);
19303
19318
  }
19304
19319
  }
19320
+ return () => {
19321
+ const handlers = this.eventListeners.get(event);
19322
+ if (!handlers) {
19323
+ return;
19324
+ }
19325
+ this.eventListeners.set(
19326
+ event,
19327
+ handlers.filter((current) => current !== handler)
19328
+ );
19329
+ };
19305
19330
  }
19306
19331
  replayAgentMessage(message) {
19307
19332
  this.captureAgentMessage(message);
@@ -19634,6 +19659,22 @@ function normalizeHeapSnapshot(raw) {
19634
19659
  updatedAt: typeof heap.updatedAt === "number" ? heap.updatedAt : Date.now()
19635
19660
  };
19636
19661
  }
19662
+ function deriveRuntimeBaseUrl(apiEndpoint) {
19663
+ try {
19664
+ const endpoint = new URL(apiEndpoint);
19665
+ const graphqlSuffix = "/orchestrator/graphql";
19666
+ if (endpoint.pathname.endsWith(graphqlSuffix)) {
19667
+ endpoint.pathname = endpoint.pathname.slice(0, -graphqlSuffix.length);
19668
+ } else if (endpoint.pathname.endsWith("/graphql")) {
19669
+ endpoint.pathname = endpoint.pathname.slice(0, -"/graphql".length);
19670
+ }
19671
+ endpoint.search = "";
19672
+ endpoint.hash = "";
19673
+ return endpoint.toString().replace(/\/$/, "");
19674
+ } catch {
19675
+ return apiEndpoint.replace(/\/orchestrator\/graphql$/, "").replace(/\/$/, "");
19676
+ }
19677
+ }
19637
19678
  function normalizeSubject(subject) {
19638
19679
  const granularId = subject.granularId || subject.subjectId;
19639
19680
  const userId = subject.userId || subject.identityId || granularId;
@@ -19673,12 +19714,13 @@ function normalizeEnvironmentData(environment) {
19673
19714
  tracking: environment.tracking || buildPolicy
19674
19715
  };
19675
19716
  }
19676
- var Environment = class extends Session {
19717
+ var Environment = class {
19718
+ granular;
19677
19719
  envData;
19678
19720
  _apiKey;
19679
19721
  _apiEndpoint;
19680
- constructor(client, envData, clientId, apiKey, apiEndpoint) {
19681
- super(client, clientId);
19722
+ constructor(granular, envData, apiKey, apiEndpoint) {
19723
+ this.granular = granular;
19682
19724
  this.envData = envData;
19683
19725
  this._apiKey = apiKey;
19684
19726
  this._apiEndpoint = apiEndpoint;
@@ -19719,35 +19761,126 @@ var Environment = class extends Session {
19719
19761
  get permissionProfileId() {
19720
19762
  return this.envData.permissionProfileId;
19721
19763
  }
19764
+ /** The current build policy backing this environment */
19765
+ get buildPolicy() {
19766
+ return this.envData.buildPolicy;
19767
+ }
19768
+ /** The current update state relative to the followed tag */
19769
+ get updateState() {
19770
+ return this.envData.updateState;
19771
+ }
19772
+ /** Convenience flag for whether this environment trails the current tag target */
19773
+ get isOutdated() {
19774
+ return this.envData.updateState === "update_available";
19775
+ }
19776
+ /** The followed tag name when this environment is tag-tracked */
19777
+ get tag() {
19778
+ return this.envData.tag?.name || this.envData.buildPolicy.tagName || null;
19779
+ }
19722
19780
  /** The GraphQL API endpoint URL */
19723
19781
  get apiEndpoint() {
19724
19782
  return this._apiEndpoint;
19725
19783
  }
19784
+ /** Internal auth token used for control-plane and runtime fallback requests */
19785
+ get authToken() {
19786
+ return this._apiKey;
19787
+ }
19788
+ /** Base runtime URL derived from the GraphQL endpoint */
19789
+ get runtimeBaseUrl() {
19790
+ return this.getRuntimeBaseUrl();
19791
+ }
19792
+ get sessions() {
19793
+ return {
19794
+ list: async (options) => this.listSessions(options?.status || "active"),
19795
+ create: async (options) => this.createSession(options),
19796
+ connect: async (sessionId, options) => this.connectSession(sessionId, options),
19797
+ reopen: async (sessionId, options) => this.reopenSession(sessionId, options),
19798
+ close: async (sessionId, session2) => this.closeSession(sessionId, session2)
19799
+ };
19800
+ }
19801
+ get data() {
19802
+ return {
19803
+ record: async (record) => this.recordObject(record),
19804
+ recordMany: async (records, options) => this.recordObjects(records, options),
19805
+ import: async (records, options) => this.enqueueRecordImport(records, options),
19806
+ listImports: async (status) => this.listRecordImports(status),
19807
+ getImport: async (importId) => this.getRecordImport(importId),
19808
+ getImportSummary: async () => this.getRecordImportSummary(),
19809
+ cancelImport: async (importId) => this.cancelRecordImport(importId),
19810
+ getAwaitingCount: async () => this.getAwaitingRecordCount()
19811
+ };
19812
+ }
19813
+ get feedback() {
19814
+ return {
19815
+ list: async () => this.listFeedback()
19816
+ };
19817
+ }
19726
19818
  /**
19727
- * Return a plain JS snapshot of the synced session heap.
19728
- *
19729
- * The heap lives in the Automerge document, so this method does not perform
19730
- * any extra network roundtrip.
19819
+ * Sessionless environments do not own a live transport, so disconnecting the
19820
+ * environment handle itself is a no-op. This keeps the public surface
19821
+ * symmetric with `EnvironmentSession.disconnect()` and lets callers always
19822
+ * clean up safely without tracking whether they currently hold an environment
19823
+ * or a session.
19731
19824
  */
19732
- getHeap() {
19733
- const doc = this.document;
19734
- return normalizeHeapSnapshot(doc?.heap);
19825
+ async disconnect() {
19735
19826
  }
19736
- getRuntimeBaseUrl() {
19737
- try {
19738
- const endpoint = new URL(this._apiEndpoint);
19739
- const graphqlSuffix = "/orchestrator/graphql";
19740
- if (endpoint.pathname.endsWith(graphqlSuffix)) {
19741
- endpoint.pathname = endpoint.pathname.slice(0, -graphqlSuffix.length);
19742
- } else if (endpoint.pathname.endsWith("/graphql")) {
19743
- endpoint.pathname = endpoint.pathname.slice(0, -"/graphql".length);
19744
- }
19745
- endpoint.search = "";
19746
- endpoint.hash = "";
19747
- return endpoint.toString().replace(/\/$/, "");
19748
- } catch {
19749
- return this._apiEndpoint.replace(/\/orchestrator\/graphql$/, "").replace(/\/$/, "");
19827
+ async listSessions(status = "active") {
19828
+ if (status === "all") {
19829
+ const [active, closed] = await Promise.all([
19830
+ this.granular.listOpenSessions({ environmentId: this.environmentId }),
19831
+ this.granular.listClosedSessions({ environmentId: this.environmentId })
19832
+ ]);
19833
+ return [...active, ...closed].sort(
19834
+ (left, right) => Date.parse(right.lastSeenAt) - Date.parse(left.lastSeenAt)
19835
+ );
19836
+ }
19837
+ return status === "closed" ? this.granular.listClosedSessions({ environmentId: this.environmentId }) : this.granular.listOpenSessions({ environmentId: this.environmentId });
19838
+ }
19839
+ async createSession(options) {
19840
+ return this.granular.createSession({
19841
+ environmentId: this.environmentId,
19842
+ clientId: options?.clientId,
19843
+ initialHeap: options?.initialHeap
19844
+ });
19845
+ }
19846
+ async connectSession(sessionId, options) {
19847
+ const session2 = await this.granular["connectSession"]({
19848
+ sessionId,
19849
+ clientId: options?.clientId
19850
+ });
19851
+ if (session2.environmentId !== this.environmentId) {
19852
+ await session2.disconnect().catch(() => {
19853
+ session2.disconnectTransport();
19854
+ });
19855
+ throw new Error(
19856
+ `Session ${sessionId} belongs to environment ${session2.environmentId}, not ${this.environmentId}.`
19857
+ );
19750
19858
  }
19859
+ return session2;
19860
+ }
19861
+ async reopenSession(sessionId, options) {
19862
+ const session2 = await this.granular.reopenSession(sessionId, {
19863
+ clientId: options?.clientId
19864
+ });
19865
+ if (session2.environmentId !== this.environmentId) {
19866
+ await session2.disconnect().catch(() => {
19867
+ session2.disconnectTransport();
19868
+ });
19869
+ throw new Error(
19870
+ `Session ${sessionId} belongs to environment ${session2.environmentId}, not ${this.environmentId}.`
19871
+ );
19872
+ }
19873
+ return session2;
19874
+ }
19875
+ async closeSession(sessionId, session2) {
19876
+ await this.granular.closeSession(sessionId, session2);
19877
+ }
19878
+ async listFeedback() {
19879
+ const response = await this.controlPlaneRequest(`/control/environments/${this.environmentId}/feedback`);
19880
+ return Array.isArray(response.items) ? response.items : [];
19881
+ }
19882
+ getRuntimeBaseUrl() {
19883
+ return deriveRuntimeBaseUrl(this._apiEndpoint);
19751
19884
  }
19752
19885
  async controlPlaneRequest(path6, options = {}) {
19753
19886
  const runtimeBase = this.getRuntimeBaseUrl();
@@ -19768,95 +19901,6 @@ var Environment = class extends Session {
19768
19901
  }
19769
19902
  return response.json();
19770
19903
  }
19771
- /**
19772
- * Close the session and disconnect from the sandbox.
19773
- *
19774
- * Sends `client.goodbye` over WebSocket first, then issues an HTTP fallback
19775
- * to the runtime goodbye endpoint if no definitive WS-side runtime notify
19776
- * acknowledgement was observed.
19777
- */
19778
- async disconnect() {
19779
- let wsNotifiedRuntime = false;
19780
- try {
19781
- const goodbye = await this.rpc(
19782
- "client.goodbye",
19783
- {
19784
- timestamp: Date.now()
19785
- }
19786
- );
19787
- wsNotifiedRuntime = Boolean(goodbye?.ok && goodbye?.via);
19788
- } catch {
19789
- wsNotifiedRuntime = false;
19790
- }
19791
- if (!wsNotifiedRuntime) {
19792
- try {
19793
- const runtimeBase = this.getRuntimeBaseUrl();
19794
- await fetch(
19795
- `${runtimeBase}/orchestrator/runtime/environments/${this.environmentId}/session-goodbye`,
19796
- {
19797
- method: "POST",
19798
- headers: {
19799
- "Content-Type": "application/json",
19800
- Authorization: `Bearer ${this._apiKey}`,
19801
- Connection: "close"
19802
- },
19803
- body: JSON.stringify({
19804
- reason: "sdk_disconnect_http_fallback",
19805
- sessionId: this.client.currentSessionId
19806
- })
19807
- }
19808
- );
19809
- } catch {
19810
- }
19811
- }
19812
- this.client.disconnect();
19813
- }
19814
- /**
19815
- * Close only the socket transport without sending `client.goodbye`.
19816
- *
19817
- * Use this when the caller intends to immediately reattach to the same
19818
- * session after an unexpected disconnect.
19819
- */
19820
- disconnectTransport() {
19821
- this.client.disconnect();
19822
- }
19823
- // ==================== GRAPH CONTAINER READINESS ====================
19824
- /** The last known graph container status, updated by checkReadiness() or on heartbeat */
19825
- graphContainerStatus = null;
19826
- /**
19827
- * Check if the graph container is ready and warm.
19828
- *
19829
- * Sends a lightweight heartbeat RPC to the Session DO which internally
19830
- * pings the FalkorDB container. The response includes `graphContainerStatus`,
19831
- * which is stored locally and emitted as a `readiness` event.
19832
- *
19833
- * Use this method to proactively warm the graph container before any
19834
- * GraphQL query that requires it, or to poll the container's state in
19835
- * the background.
19836
- *
19837
- * @returns The current graph container status object
19838
- *
19839
- * @example
19840
- * ```typescript
19841
- * const status = await env.checkReadiness();
19842
- * console.log(status.status); // 'hot' | 'warming' | 'unknown'
19843
- *
19844
- * // Or listen for live updates
19845
- * env.on('readiness', (status) => {
19846
- * console.log('Graph is now:', status.status);
19847
- * });
19848
- * ```
19849
- */
19850
- async checkReadiness() {
19851
- const result = await this.client.call("client.heartbeat", {});
19852
- const containerStatus = result?.graphContainerStatus ?? {
19853
- lastKeepAliveAt: Date.now(),
19854
- status: "unknown"
19855
- };
19856
- this.graphContainerStatus = containerStatus;
19857
- this.emit("readiness", containerStatus);
19858
- return containerStatus;
19859
- }
19860
19904
  // ==================== ID ↔ GRAPH PATH MAPPING ====================
19861
19905
  /**
19862
19906
  * Convert a class name + real-world ID into a unique graph path.
@@ -19986,7 +20030,9 @@ var Environment = class extends Session {
19986
20030
  }
19987
20031
  );
19988
20032
  if (result.errors?.length) {
19989
- throw new Error(`defineRelationship failed: ${result.errors[0].message}`);
20033
+ throw new Error(
20034
+ `defineRelationship failed: ${result.errors[0]?.message ?? "Unknown GraphQL error"}`
20035
+ );
19990
20036
  }
19991
20037
  return result.data.at.define_relationship;
19992
20038
  }
@@ -20022,7 +20068,9 @@ var Environment = class extends Session {
20022
20068
  { path: modelPath }
20023
20069
  );
20024
20070
  if (result.errors?.length) {
20025
- throw new Error(`getRelationships failed: ${result.errors[0].message}`);
20071
+ throw new Error(
20072
+ `getRelationships failed: ${result.errors[0]?.message ?? "Unknown GraphQL error"}`
20073
+ );
20026
20074
  }
20027
20075
  return result.data?.model?.relationships || [];
20028
20076
  }
@@ -20061,7 +20109,7 @@ var Environment = class extends Session {
20061
20109
  { target: targetPath }
20062
20110
  );
20063
20111
  if (result.errors?.length) {
20064
- throw new Error(`attach failed: ${result.errors[0].message}`);
20112
+ throw new Error(`attach failed: ${result.errors[0]?.message ?? "Unknown GraphQL error"}`);
20065
20113
  }
20066
20114
  }
20067
20115
  /**
@@ -20096,7 +20144,7 @@ var Environment = class extends Session {
20096
20144
  }`
20097
20145
  );
20098
20146
  if (result.errors?.length) {
20099
- throw new Error(`detach failed: ${result.errors[0].message}`);
20147
+ throw new Error(`detach failed: ${result.errors[0]?.message ?? "Unknown GraphQL error"}`);
20100
20148
  }
20101
20149
  }
20102
20150
  /**
@@ -20123,7 +20171,9 @@ var Environment = class extends Session {
20123
20171
  }`
20124
20172
  );
20125
20173
  if (result.errors?.length) {
20126
- throw new Error(`listRelated failed: ${result.errors[0].message}`);
20174
+ throw new Error(
20175
+ `listRelated failed: ${result.errors[0]?.message ?? "Unknown GraphQL error"}`
20176
+ );
20127
20177
  }
20128
20178
  return result.data?.at?.at?.list_related || [];
20129
20179
  }
@@ -20216,7 +20266,7 @@ var Environment = class extends Session {
20216
20266
  async _runGraphql(query, label) {
20217
20267
  const result = await this.graphql(query);
20218
20268
  if (result.errors?.length) {
20219
- throw new Error(`${label}: ${result.errors[0].message}`);
20269
+ throw new Error(`${label}: ${result.errors[0]?.message ?? "Unknown GraphQL error"}`);
20220
20270
  }
20221
20271
  return result.data;
20222
20272
  }
@@ -20561,7 +20611,11 @@ var Environment = class extends Session {
20561
20611
  */
20562
20612
  async recordObject(options) {
20563
20613
  const results = await this.recordObjects([options]);
20564
- return results[0];
20614
+ const result = results[0];
20615
+ if (!result) {
20616
+ throw new Error("recordObject: no result returned for record");
20617
+ }
20618
+ return result;
20565
20619
  }
20566
20620
  /**
20567
20621
  * Batch version of `recordObject()`.
@@ -20613,7 +20667,13 @@ var Environment = class extends Session {
20613
20667
  );
20614
20668
  }
20615
20669
  for (let index = 0; index < items.length; index += 1) {
20616
- results[plan.offset + index] = items[index];
20670
+ const item = items[index];
20671
+ if (!item) {
20672
+ throw new Error(
20673
+ `recordObjects: chunk ${plan.chunkIndex + 1} returned an empty result at index ${index}`
20674
+ );
20675
+ }
20676
+ results[plan.offset + index] = item;
20617
20677
  }
20618
20678
  if (onChunk) {
20619
20679
  const info2 = {
@@ -20717,36 +20777,186 @@ var Environment = class extends Session {
20717
20777
  }
20718
20778
  );
20719
20779
  }
20720
- // ==================== PUBLISH TOOLS ====================
20780
+ };
20781
+ var EnvironmentSession = class extends Session {
20782
+ environment;
20783
+ /** The last known graph container status, updated by checkReadiness() or on heartbeat */
20784
+ graphContainerStatus = null;
20785
+ constructor(client, environment, clientId) {
20786
+ super(client, clientId);
20787
+ this.environment = environment;
20788
+ }
20789
+ get environmentId() {
20790
+ return this.environment.environmentId;
20791
+ }
20792
+ get sandboxId() {
20793
+ return this.environment.sandboxId;
20794
+ }
20795
+ get ontologyId() {
20796
+ return this.environment.ontologyId;
20797
+ }
20798
+ get subjectId() {
20799
+ return this.environment.subjectId;
20800
+ }
20801
+ get envName() {
20802
+ return this.environment.envName;
20803
+ }
20804
+ get versionId() {
20805
+ return this.environment.versionId;
20806
+ }
20807
+ get granularId() {
20808
+ return this.environment.granularId;
20809
+ }
20810
+ get permissionProfileId() {
20811
+ return this.environment.permissionProfileId;
20812
+ }
20813
+ get apiEndpoint() {
20814
+ return this.environment.apiEndpoint;
20815
+ }
20816
+ get data() {
20817
+ return this.environment.data;
20818
+ }
20819
+ get feedback() {
20820
+ return this.environment.feedback;
20821
+ }
20721
20822
  /**
20722
- * Removed: environment-scoped effect publication is no longer supported.
20823
+ * Return a plain JS snapshot of the synced session heap.
20723
20824
  */
20724
- async publishTools(tools, revision = "1.0.0") {
20725
- return super.publishTools(tools, revision);
20825
+ getHeap() {
20826
+ const doc = this.document;
20827
+ return normalizeHeapSnapshot(doc?.heap);
20828
+ }
20829
+ async graphql(query, variables) {
20830
+ return this.environment.graphql(query, variables);
20831
+ }
20832
+ async defineRelationship(options) {
20833
+ return this.environment.defineRelationship(options);
20834
+ }
20835
+ async getRelationships(modelPath) {
20836
+ return this.environment.getRelationships(modelPath);
20837
+ }
20838
+ async attach(modelPath, submodelPath, targetPath) {
20839
+ return this.environment.attach(modelPath, submodelPath, targetPath);
20840
+ }
20841
+ async detach(modelPath, submodelPath, targetPath) {
20842
+ return this.environment.detach(modelPath, submodelPath, targetPath);
20843
+ }
20844
+ async listRelated(modelPath, submodelPath) {
20845
+ return this.environment.listRelated(modelPath, submodelPath);
20846
+ }
20847
+ async applyManifest(manifest) {
20848
+ return this.environment.applyManifest(manifest);
20849
+ }
20850
+ async recordObject(options) {
20851
+ return this.environment.recordObject(options);
20852
+ }
20853
+ async recordObjects(records, options) {
20854
+ return this.environment.recordObjects(records, options);
20855
+ }
20856
+ async enqueueRecordImport(records, options = {}) {
20857
+ return this.environment.enqueueRecordImport(records, options);
20858
+ }
20859
+ async listRecordImports(status) {
20860
+ return this.environment.listRecordImports(status);
20861
+ }
20862
+ async getRecordImportSummary() {
20863
+ return this.environment.getRecordImportSummary();
20864
+ }
20865
+ async getAwaitingRecordCount() {
20866
+ return this.environment.getAwaitingRecordCount();
20867
+ }
20868
+ async getRecordImport(importId) {
20869
+ return this.environment.getRecordImport(importId);
20870
+ }
20871
+ async cancelRecordImport(importId) {
20872
+ return this.environment.cancelRecordImport(importId);
20873
+ }
20874
+ async listFeedback() {
20875
+ return this.environment.listFeedback();
20726
20876
  }
20727
20877
  /**
20728
- * Removed: environment-scoped effect publication is no longer supported.
20878
+ * Close the session and disconnect from the sandbox.
20879
+ *
20880
+ * Sends `client.goodbye` over WebSocket first, then issues an HTTP fallback
20881
+ * to the runtime goodbye endpoint if no definitive WS-side runtime notify
20882
+ * acknowledgement was observed.
20729
20883
  */
20730
- async publishEffect(effect) {
20731
- return super.publishEffect(effect);
20884
+ async disconnect() {
20885
+ let wsNotifiedRuntime = false;
20886
+ try {
20887
+ const goodbye = await this.rpc(
20888
+ "client.goodbye",
20889
+ {
20890
+ timestamp: Date.now()
20891
+ }
20892
+ );
20893
+ wsNotifiedRuntime = Boolean(goodbye?.ok && goodbye?.via);
20894
+ } catch {
20895
+ wsNotifiedRuntime = false;
20896
+ }
20897
+ if (!wsNotifiedRuntime) {
20898
+ try {
20899
+ await fetch(
20900
+ `${this.environment.runtimeBaseUrl}/orchestrator/runtime/environments/${this.environmentId}/session-goodbye`,
20901
+ {
20902
+ method: "POST",
20903
+ headers: {
20904
+ "Content-Type": "application/json",
20905
+ Authorization: `Bearer ${this.environment.authToken}`,
20906
+ Connection: "close"
20907
+ },
20908
+ body: JSON.stringify({
20909
+ reason: "sdk_disconnect_http_fallback",
20910
+ sessionId: this.client.currentSessionId
20911
+ })
20912
+ }
20913
+ );
20914
+ } catch {
20915
+ }
20916
+ }
20917
+ this.client.disconnect();
20732
20918
  }
20733
20919
  /**
20734
- * Removed: environment-scoped effect publication is no longer supported.
20920
+ * Close only the socket transport without sending `client.goodbye`.
20735
20921
  */
20736
- async publishEffects(effects2) {
20737
- return super.publishEffects(effects2);
20922
+ disconnectTransport() {
20923
+ this.client.disconnect();
20738
20924
  }
20739
20925
  /**
20740
- * Removed: environment-scoped effect publication is no longer supported.
20926
+ * Backwards-compatible alias for `disconnect()`.
20741
20927
  */
20742
- async unpublishEffect(name) {
20743
- return super.unpublishEffect(name);
20928
+ async close() {
20929
+ await this.disconnect();
20744
20930
  }
20745
20931
  /**
20746
- * Removed: environment-scoped effect publication is no longer supported.
20932
+ * Check if the graph container is ready and warm.
20747
20933
  */
20748
- async unpublishAllEffects() {
20749
- return super.unpublishAllEffects();
20934
+ async checkReadiness() {
20935
+ const result = await this.client.call("client.heartbeat", {});
20936
+ const containerStatus = result?.graphContainerStatus ?? {
20937
+ lastKeepAliveAt: Date.now(),
20938
+ status: "unknown"
20939
+ };
20940
+ this.graphContainerStatus = containerStatus;
20941
+ this.emit("readiness", containerStatus);
20942
+ return containerStatus;
20943
+ }
20944
+ };
20945
+ var OntologyHandle = class {
20946
+ granular;
20947
+ ontologyNameOrId;
20948
+ constructor(granular, ontologyNameOrId) {
20949
+ this.granular = granular;
20950
+ this.ontologyNameOrId = ontologyNameOrId;
20951
+ }
20952
+ get effects() {
20953
+ return {
20954
+ register: async (effect) => this.granular.registerEffect(this.ontologyNameOrId, effect),
20955
+ registerMany: async (effects2) => this.granular.registerEffects(this.ontologyNameOrId, effects2),
20956
+ unregister: async (name) => this.granular.unregisterEffect(this.ontologyNameOrId, name),
20957
+ clear: async () => this.granular.unregisterAllEffects(this.ontologyNameOrId),
20958
+ disconnect: async () => this.granular.disconnectEffects(this.ontologyNameOrId)
20959
+ };
20750
20960
  }
20751
20961
  };
20752
20962
  var Granular = class _Granular {
@@ -20783,6 +20993,12 @@ var Granular = class _Granular {
20783
20993
  this.onReconnectError = options.onReconnectError;
20784
20994
  this.httpUrl = this.apiUrl.replace(/^wss:\/\//, "https://").replace(/^ws:\/\//, "http://").replace(/\/ws$/, "");
20785
20995
  }
20996
+ /**
20997
+ * Return an ontology-scoped handle for effects and other ontology-level APIs.
20998
+ */
20999
+ ontology(ontologyNameOrId) {
21000
+ return new OntologyHandle(this, ontologyNameOrId);
21001
+ }
20786
21002
  /**
20787
21003
  * Records/upserts a user and prepares them for sandbox connections
20788
21004
  *
@@ -20819,7 +21035,23 @@ var Granular = class _Granular {
20819
21035
  permissions: options.permissions || []
20820
21036
  });
20821
21037
  }
21038
+ /**
21039
+ * Alias for `recordUser()` with user-facing naming that matches upsert semantics.
21040
+ */
21041
+ async upsertUser(options) {
21042
+ return this.recordUser(options);
21043
+ }
20822
21044
  async resolveConnectUser(options) {
21045
+ const providedIdentityCount = [
21046
+ Boolean(options.user),
21047
+ Boolean(options.userId),
21048
+ Boolean(options.granularId)
21049
+ ].filter(Boolean).length;
21050
+ if (providedIdentityCount !== 1) {
21051
+ throw new Error(
21052
+ "openEnvironment() requires exactly one of userId, granularId, or a user object returned by recordUser()."
21053
+ );
21054
+ }
20823
21055
  if (options.user) {
20824
21056
  const user = normalizeUser(options.user);
20825
21057
  return {
@@ -20856,56 +21088,85 @@ var Granular = class _Granular {
20856
21088
  };
20857
21089
  }
20858
21090
  throw new Error(
20859
- "connect() requires either userId, granularId, or a user object returned by recordUser()."
21091
+ "openEnvironment() requires either userId, granularId, or a user object returned by recordUser()."
20860
21092
  );
20861
21093
  }
20862
21094
  /**
20863
- * Connect to an ontology environment and establish a real-time session.
20864
- *
20865
- * Effects are registered at the sandbox level via `granular.registerEffect()`
20866
- * or `granular.registerEffects()`. Sessions pick up live availability from
20867
- * the sandbox registry automatically.
20868
- *
20869
- * @param options - Connection options
20870
- * @returns An active environment session
21095
+ * Open or resolve an ontology environment for one user without opening a session.
20871
21096
  *
20872
21097
  * @example
20873
21098
  * ```typescript
20874
- * const environment = await granular.connect({
21099
+ * const environment = await granular.openEnvironment({
20875
21100
  * ontology: 'my-ontology',
20876
- * environment: 'dev',
21101
+ * tag: 'dev',
20877
21102
  * userId: 'user_123',
20878
21103
  * permissions: ['agent'],
20879
21104
  * });
20880
21105
  *
20881
- * await granular.registerEffect('my-sandbox', {
20882
- * name: 'greet',
20883
- * description: 'Say hello',
20884
- * inputSchema: { type: 'object', properties: {} },
20885
- * handler: async () => 'Hello!',
21106
+ * await environment.data.record({
21107
+ * className: 'customer',
21108
+ * id: 'acme',
21109
+ * fields: { name: 'Acme' },
20886
21110
  * });
20887
21111
  *
20888
- * // Submit job
20889
- * const job = await environment.submitJob(`
20890
- * import { tools } from './sandbox-tools';
20891
- * return await tools.greet({});
20892
- * `);
20893
- *
20894
- * console.log(await job.result); // 'Hello!'
21112
+ * const session = await environment.sessions.create();
21113
+ * const job = await session.submitJob(`return "hello";`);
21114
+ * console.log(await job.result);
20895
21115
  * ```
20896
21116
  */
21117
+ async openEnvironment(options) {
21118
+ const envData = await this.resolveOpenEnvironmentData(
21119
+ options,
21120
+ "openEnvironment"
21121
+ );
21122
+ return this.bindEnvironmentHandle(envData);
21123
+ }
21124
+ /**
21125
+ * Deprecated compatibility alias for `openEnvironment()`.
21126
+ *
21127
+ * `connect()` no longer opens a runtime session automatically.
21128
+ */
20897
21129
  async connect(options) {
20898
- const clientId = options.clientId || `client_${Date.now()}`;
21130
+ return this.openEnvironment({
21131
+ ...options,
21132
+ tag: this.resolveRequestedTag(options, "connect"),
21133
+ permissions: options.permissions || options.user?.permissions || []
21134
+ });
21135
+ }
21136
+ resolveRequestedTag(options, methodName) {
21137
+ const tag2 = options.tag?.trim() || options.tagName?.trim() || options.environment?.trim();
21138
+ if (!tag2) {
21139
+ throw new Error(`${methodName}() requires \`tag\`.`);
21140
+ }
21141
+ return tag2;
21142
+ }
21143
+ buildManagedEnvironmentName(tag2, versionId) {
21144
+ return `__sdk__${tag2}__${versionId}`;
21145
+ }
21146
+ matchesTagTrackedEnvironment(environment, tagName, tagId) {
21147
+ const environmentTagName = environment.tag?.name || environment.buildPolicy.tagName || null;
21148
+ 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);
21149
+ }
21150
+ sortEnvironmentsByRecency(environments) {
21151
+ return [...environments].sort(
21152
+ (left, right) => right.updatedAt - left.updatedAt
21153
+ );
21154
+ }
21155
+ async resolveOpenEnvironmentData(options, methodName) {
20899
21156
  const ontology = options.ontology;
20900
21157
  if (!ontology) {
20901
- throw new Error("connect() requires `ontology`.");
21158
+ throw new Error(`${methodName}() requires \`ontology\`.`);
20902
21159
  }
20903
- const environmentName = options.environment;
20904
- if (!environmentName) {
20905
- throw new Error("connect() requires `environment`.");
21160
+ const tagName = options.tag?.trim();
21161
+ if (!tagName) {
21162
+ throw new Error(`${methodName}() requires \`tag\`.`);
20906
21163
  }
20907
- const tagName = options.tagName?.trim() || void 0;
20908
21164
  const user = await this.resolveConnectUser(options);
21165
+ if (!Array.isArray(user.permissions) || user.permissions.length === 0) {
21166
+ throw new Error(
21167
+ `${methodName}() requires at least one permission so the SDK can ensure assignments for new users.`
21168
+ );
21169
+ }
20909
21170
  const sandbox = await this.findOrCreateSandbox(ontology);
20910
21171
  for (const profileName of user.permissions) {
20911
21172
  const profileId = await this.ensurePermissionProfile(
@@ -20918,22 +21179,49 @@ var Granular = class _Granular {
20918
21179
  profileId
20919
21180
  );
20920
21181
  }
20921
- const envData = await this.environments.create(sandbox.sandboxId, {
21182
+ const tags = await this.request(
21183
+ `/control/sandboxes/${sandbox.sandboxId}/tags`
21184
+ );
21185
+ const tag2 = (tags.items || []).find(
21186
+ (candidate) => Boolean(candidate?.name === tagName)
21187
+ );
21188
+ if (!tag2) {
21189
+ throw new Error(
21190
+ `Tag "${tagName}" was not found for ontology ${sandbox.sandboxId}.`
21191
+ );
21192
+ }
21193
+ const targetVersionId = tag2.targetVersionId || tag2.targetBuildId;
21194
+ if (!targetVersionId) {
21195
+ throw new Error(
21196
+ `Tag "${tagName}" does not currently point to a build/version.`
21197
+ );
21198
+ }
21199
+ const allEnvironments = await this.environments.list(sandbox.sandboxId);
21200
+ const userEnvironments = allEnvironments.filter(
21201
+ (environment) => environment.subjectId === user.granularId
21202
+ );
21203
+ const currentMatches = this.sortEnvironmentsByRecency(
21204
+ userEnvironments.filter(
21205
+ (environment) => this.matchesTagTrackedEnvironment(environment, tagName, tag2.tagId) && environment.versionId === targetVersionId
21206
+ )
21207
+ );
21208
+ if (currentMatches.length > 0) {
21209
+ return currentMatches[0];
21210
+ }
21211
+ const outdatedMatches = this.sortEnvironmentsByRecency(
21212
+ userEnvironments.filter(
21213
+ (environment) => this.matchesTagTrackedEnvironment(environment, tagName, tag2.tagId)
21214
+ )
21215
+ );
21216
+ if (outdatedMatches.length > 0 && options.createFreshIfOutdated !== true) {
21217
+ return outdatedMatches[0];
21218
+ }
21219
+ return this.environments.create(sandbox.sandboxId, {
20922
21220
  subjectId: user.granularId,
20923
- environment: environmentName,
20924
- tagName,
21221
+ environment: this.buildManagedEnvironmentName(tagName, targetVersionId),
21222
+ tagId: tag2.tagId,
20925
21223
  permissionProfileId: null
20926
21224
  });
20927
- await this.activateEnvironment(envData.environmentId);
20928
- const session2 = await this.request("/ws/sessions", {
20929
- method: "POST",
20930
- body: JSON.stringify({
20931
- environmentId: envData.environmentId,
20932
- clientId,
20933
- initialHeap: options.initialHeap
20934
- })
20935
- });
20936
- return this.bindWebSocketEnvironment(envData, clientId, session2);
20937
21225
  }
20938
21226
  /**
20939
21227
  * List active (open) sessions for an environment — each session is one agent conversation thread.
@@ -20996,6 +21284,7 @@ var Granular = class _Granular {
20996
21284
  const clientId = options.clientId || `client_${Date.now()}`;
20997
21285
  await this.activateEnvironment(options.environmentId);
20998
21286
  const envData = await this.environments.get(options.environmentId);
21287
+ const environment = this.bindEnvironmentHandle(envData);
20999
21288
  const session2 = await this.request("/ws/sessions", {
21000
21289
  method: "POST",
21001
21290
  body: JSON.stringify({
@@ -21004,7 +21293,7 @@ var Granular = class _Granular {
21004
21293
  initialHeap: options.initialHeap
21005
21294
  })
21006
21295
  });
21007
- return this.bindWebSocketEnvironment(envData, clientId, session2);
21296
+ return this.bindWebSocketEnvironmentSession(environment, clientId, session2);
21008
21297
  }
21009
21298
  /**
21010
21299
  * Connect to an existing open session (same conversation thread) using a freshly minted WebSocket token.
@@ -21016,7 +21305,8 @@ var Granular = class _Granular {
21016
21305
  body: JSON.stringify({})
21017
21306
  });
21018
21307
  const envData = await this.environments.get(minted.environmentId);
21019
- return this.bindWebSocketEnvironment(envData, clientId, minted);
21308
+ const environment = this.bindEnvironmentHandle(envData);
21309
+ return this.bindWebSocketEnvironmentSession(environment, clientId, minted);
21020
21310
  }
21021
21311
  /**
21022
21312
  * Mark a session closed in the control plane. If `environment` is the connected handle for that
@@ -21048,7 +21338,11 @@ var Granular = class _Granular {
21048
21338
  });
21049
21339
  return this.connectSession({ sessionId, clientId: options?.clientId });
21050
21340
  }
21051
- async bindWebSocketEnvironment(envData, clientId, session2) {
21341
+ bindEnvironmentHandle(envData) {
21342
+ const graphqlEndpoint = `${this.httpUrl}/orchestrator/graphql`;
21343
+ return new Environment(this, envData, this.apiKey, graphqlEndpoint);
21344
+ }
21345
+ async bindWebSocketEnvironmentSession(environment, clientId, session2) {
21052
21346
  const client = new WSClient({
21053
21347
  url: session2.wsUrl,
21054
21348
  sessionId: session2.sessionId,
@@ -21059,16 +21353,13 @@ var Granular = class _Granular {
21059
21353
  onReconnectError: this.onReconnectError
21060
21354
  });
21061
21355
  await client.connect();
21062
- const graphqlEndpoint = `${this.httpUrl}/orchestrator/graphql`;
21063
- const environment = new Environment(
21356
+ const environmentSession = new EnvironmentSession(
21064
21357
  client,
21065
- envData,
21066
- clientId,
21067
- this.apiKey,
21068
- graphqlEndpoint
21358
+ environment,
21359
+ clientId
21069
21360
  );
21070
- await environment.hello();
21071
- return environment;
21361
+ await environmentSession.hello();
21362
+ return environmentSession;
21072
21363
  }
21073
21364
  async activateEnvironment(environmentId) {
21074
21365
  await this.request(`/orchestrator/runtime/environments/${environmentId}/activate`, {
@@ -21801,15 +22092,7 @@ async function resolveEnvironmentData(granular, options) {
21801
22092
  userId: options.userId ?? "granular-cli",
21802
22093
  permissions: options.permissions ?? ["default"]
21803
22094
  });
21804
- try {
21805
- return await granular.environments.get(connection.environmentId);
21806
- } finally {
21807
- try {
21808
- await connection.disconnect();
21809
- } catch {
21810
- connection?.client?.disconnect?.();
21811
- }
21812
- }
22095
+ return await granular.environments.get(connection.environmentId);
21813
22096
  }
21814
22097
  async function listSessionsForEnvironment(granular, environmentId, status) {
21815
22098
  if (status === "all") {
@@ -21830,6 +22113,7 @@ async function connectRuntime(options) {
21830
22113
  return {
21831
22114
  granular: granular2,
21832
22115
  environment: environment2,
22116
+ session: environment2,
21833
22117
  ontologyId: environment2.ontologyId || environment2.sandboxId
21834
22118
  };
21835
22119
  }
@@ -21838,15 +22122,17 @@ async function connectRuntime(options) {
21838
22122
  if (!ontologyId) {
21839
22123
  throw new Error("No ontology configured. Run `granular init` first or pass `--ontology`.");
21840
22124
  }
21841
- const environment = await granular.connect({
22125
+ const environmentHandle = await granular.connect({
21842
22126
  ontology: ontologyId,
21843
22127
  environment: options.environment ?? "dev",
21844
22128
  userId: options.userId ?? "granular-cli",
21845
22129
  permissions: options.permissions ?? ["default"]
21846
22130
  });
22131
+ const environment = await environmentHandle.createSession();
21847
22132
  return {
21848
22133
  granular,
21849
22134
  environment,
22135
+ session: environment,
21850
22136
  ontologyId
21851
22137
  };
21852
22138
  }
@@ -21901,7 +22187,7 @@ async function connectTestCommand(options) {
21901
22187
  ontologyId,
21902
22188
  sandboxId: environment.sandboxId,
21903
22189
  environmentId: environment.environmentId,
21904
- environment: environment.environment,
22190
+ environment: environment.envName,
21905
22191
  sessionId: environment.sessionId,
21906
22192
  subjectId: environment.subjectId,
21907
22193
  versionId: environment.versionId
@@ -22014,7 +22300,7 @@ async function sessionCreateCommand(options) {
22014
22300
  ok: true,
22015
22301
  ontologyId: envData.sandboxId,
22016
22302
  environmentId: environment.environmentId,
22017
- environment: environment.environment,
22303
+ environment: environment.envName,
22018
22304
  sessionId: environment.sessionId,
22019
22305
  subjectId: environment.subjectId,
22020
22306
  versionId: environment.versionId
@@ -22116,13 +22402,13 @@ async function graphqlCommand(options) {
22116
22402
  permissions: normalizePermissions(options.permissions),
22117
22403
  sessionId: options.session
22118
22404
  },
22119
- async ({ environment, ontologyId }) => {
22405
+ async ({ environment, session: session2, ontologyId }) => {
22120
22406
  const result = await environment.graphql(options.query, variables);
22121
22407
  const payload = {
22122
22408
  ok: !result.errors || result.errors.length === 0,
22123
22409
  ontologyId,
22124
22410
  environmentId: environment.environmentId,
22125
- sessionId: environment.sessionId,
22411
+ sessionId: session2.sessionId,
22126
22412
  query: options.query,
22127
22413
  variables: variables ?? null,
22128
22414
  result
@@ -22271,8 +22557,8 @@ async function jobRunCommand(options) {
22271
22557
  permissions: normalizePermissions(options.permissions),
22272
22558
  sessionId: options.session
22273
22559
  },
22274
- async ({ environment, ontologyId }) => {
22275
- const job2 = await environment.submitJob(code);
22560
+ async ({ environment, session: session2, ontologyId }) => {
22561
+ const job2 = await session2.submitJob(code);
22276
22562
  const stdout = [];
22277
22563
  const stderr = [];
22278
22564
  job2.on("stdout", (line) => {
@@ -22296,7 +22582,7 @@ async function jobRunCommand(options) {
22296
22582
  ok: true,
22297
22583
  ontologyId,
22298
22584
  environmentId: environment.environmentId,
22299
- sessionId: environment.sessionId,
22585
+ sessionId: session2.sessionId,
22300
22586
  jobId: job2.id,
22301
22587
  status: job2.status,
22302
22588
  stdout,