@granular-software/sdk 0.4.50 → 0.4.52

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
@@ -60,11 +60,11 @@ var __export = (target, all) => {
60
60
  for (var name in all)
61
61
  __defProp(target, name, { get: all[name], enumerable: true });
62
62
  };
63
- var __copyProps = (to, from, except, desc) => {
64
- if (from && typeof from === "object" || typeof from === "function") {
65
- for (let key of __getOwnPropNames(from))
63
+ var __copyProps = (to, from2, except, desc) => {
64
+ if (from2 && typeof from2 === "object" || typeof from2 === "function") {
65
+ for (let key of __getOwnPropNames(from2))
66
66
  if (!__hasOwnProp.call(to, key) && key !== except)
67
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
67
+ __defProp(to, key, { get: () => from2[key], enumerable: !(desc = __getOwnPropDesc(from2, key)) || desc.enumerable });
68
68
  }
69
69
  return to;
70
70
  };
@@ -7898,14 +7898,14 @@ var ApiClient = class {
7898
7898
  async getVersion(versionId) {
7899
7899
  return this.request(`/control/versions/${versionId}`);
7900
7900
  }
7901
- async triggerBuild(sandboxId, manifestId) {
7901
+ async triggerBuild(sandboxId, manifestId, releaseKey) {
7902
7902
  return this.request(`/control/sandboxes/${sandboxId}/builds`, {
7903
7903
  method: "POST",
7904
- body: JSON.stringify({ manifestId })
7904
+ body: JSON.stringify({ manifestId, releaseKey })
7905
7905
  });
7906
7906
  }
7907
- async createVersion(sandboxId, manifestId) {
7908
- return this.triggerBuild(sandboxId, manifestId);
7907
+ async createVersion(sandboxId, manifestId, releaseKey) {
7908
+ return this.triggerBuild(sandboxId, manifestId, releaseKey);
7909
7909
  }
7910
7910
  async listTags(sandboxId) {
7911
7911
  const result = await this.request(
@@ -8889,7 +8889,7 @@ createChalk({ level: stderrColor ? stderrColor.level : 0 });
8889
8889
  var source_default = chalk;
8890
8890
 
8891
8891
  // ../../node_modules/mimic-function/index.js
8892
- var copyProperty = (to, from, property, ignoreNonConfigurable) => {
8892
+ var copyProperty = (to, from2, property, ignoreNonConfigurable) => {
8893
8893
  if (property === "length" || property === "prototype") {
8894
8894
  return;
8895
8895
  }
@@ -8897,7 +8897,7 @@ var copyProperty = (to, from, property, ignoreNonConfigurable) => {
8897
8897
  return;
8898
8898
  }
8899
8899
  const toDescriptor = Object.getOwnPropertyDescriptor(to, property);
8900
- const fromDescriptor = Object.getOwnPropertyDescriptor(from, property);
8900
+ const fromDescriptor = Object.getOwnPropertyDescriptor(from2, property);
8901
8901
  if (!canCopyProperty(toDescriptor, fromDescriptor) && ignoreNonConfigurable) {
8902
8902
  return;
8903
8903
  }
@@ -8906,8 +8906,8 @@ var copyProperty = (to, from, property, ignoreNonConfigurable) => {
8906
8906
  var canCopyProperty = function(toDescriptor, fromDescriptor) {
8907
8907
  return toDescriptor === void 0 || toDescriptor.configurable || toDescriptor.writable === fromDescriptor.writable && toDescriptor.enumerable === fromDescriptor.enumerable && toDescriptor.configurable === fromDescriptor.configurable && (toDescriptor.writable || toDescriptor.value === fromDescriptor.value);
8908
8908
  };
8909
- var changePrototype = (to, from) => {
8910
- const fromPrototype = Object.getPrototypeOf(from);
8909
+ var changePrototype = (to, from2) => {
8910
+ const fromPrototype = Object.getPrototypeOf(from2);
8911
8911
  if (fromPrototype === Object.getPrototypeOf(to)) {
8912
8912
  return;
8913
8913
  }
@@ -8917,20 +8917,20 @@ var wrappedToString = (withName, fromBody) => `/* Wrapped ${withName}*/
8917
8917
  ${fromBody}`;
8918
8918
  var toStringDescriptor = Object.getOwnPropertyDescriptor(Function.prototype, "toString");
8919
8919
  var toStringName = Object.getOwnPropertyDescriptor(Function.prototype.toString, "name");
8920
- var changeToString = (to, from, name) => {
8920
+ var changeToString = (to, from2, name) => {
8921
8921
  const withName = name === "" ? "" : `with ${name.trim()}() `;
8922
- const newToString = wrappedToString.bind(null, withName, from.toString());
8922
+ const newToString = wrappedToString.bind(null, withName, from2.toString());
8923
8923
  Object.defineProperty(newToString, "name", toStringName);
8924
8924
  const { writable, enumerable, configurable } = toStringDescriptor;
8925
8925
  Object.defineProperty(to, "toString", { value: newToString, writable, enumerable, configurable });
8926
8926
  };
8927
- function mimicFunction(to, from, { ignoreNonConfigurable = false } = {}) {
8927
+ function mimicFunction(to, from2, { ignoreNonConfigurable = false } = {}) {
8928
8928
  const { name } = to;
8929
- for (const property of Reflect.ownKeys(from)) {
8930
- copyProperty(to, from, property, ignoreNonConfigurable);
8929
+ for (const property of Reflect.ownKeys(from2)) {
8930
+ copyProperty(to, from2, property, ignoreNonConfigurable);
8931
8931
  }
8932
- changePrototype(to, from);
8933
- changeToString(to, from, name);
8932
+ changePrototype(to, from2);
8933
+ changeToString(to, from2, name);
8934
8934
  return to;
8935
8935
  }
8936
8936
 
@@ -15624,11 +15624,20 @@ function buildStateMachineModelMutations(modelPath, machines) {
15624
15624
  return mutations;
15625
15625
  }
15626
15626
  function buildMachineTypes(classSummary, machine) {
15627
+ const stateGlossary = machine.states.map((state) => {
15628
+ const label = state.label && state.label !== state.name ? state.label : null;
15629
+ const meaning = [label, state.description].filter(Boolean).join(" \u2014 ");
15630
+ const finalMarker = state.isFinal ? " Final state." : "";
15631
+ return `${state.name}${meaning ? `: ${meaning}` : "."}${finalMarker}`;
15632
+ });
15627
15633
  return [
15628
15634
  {
15629
15635
  kind: "union",
15630
15636
  name: stateTypeName(classSummary.name, machine.name),
15631
- docs: [`Allowed states for ${classSummary.name}.${machine.name}.`],
15637
+ docs: [
15638
+ `Allowed states for ${classSummary.name}.${machine.name}.`,
15639
+ ...stateGlossary
15640
+ ],
15632
15641
  members: machine.states.map((state) => state.name)
15633
15642
  },
15634
15643
  {
@@ -15976,7 +15985,7 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
15976
15985
  },
15977
15986
  add_transition: async (value, {
15978
15987
  name,
15979
- from,
15988
+ from: from2,
15980
15989
  to,
15981
15990
  label,
15982
15991
  description,
@@ -15991,7 +16000,7 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
15991
16000
  value.target.add_state_machine_transition(
15992
16001
  value.name,
15993
16002
  name,
15994
- from,
16003
+ from2,
15995
16004
  to,
15996
16005
  {
15997
16006
  label,
@@ -16814,7 +16823,7 @@ ${effectMetamodelTable}
16814
16823
  | \`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. |
16815
16824
  | \`graphql(query, variables?)\` | **GraphQL** \u2014 see dedicated subsection below. |
16816
16825
  | \`defineRelationship\`, \`getRelationships\`, \`attach\`, \`detach\`, \`listRelated\` | Imperative relationship operations (same ideas as manifest \`defineRelationship\`). |
16817
- | \`sessions.list()\`, \`sessions.create()\`, \`sessions.connect()\`, \`sessions.reopen()\`, \`sessions.close()\` | Session lifecycle for this environment. |
16826
+ | \`sessions.list({ status?, sessionScope?, limit?, offset? })\`, \`sessions.create({ sessionScope? })\`, \`sessions.connect()\`, \`sessions.reopen()\`, \`sessions.close()\` | Scoped, bounded session history and lifecycle for this environment. |
16818
16827
 
16819
16828
  ### \`Session\` (live runtime connection)
16820
16829
 
@@ -16862,7 +16871,7 @@ Use \`environment.graphql(query, variables?)\` when you need **query/mutation ac
16862
16871
  | \`granular simulate\` | Open simulator in browser. |
16863
16872
  | \`granular simulate --print-url\` | Print a deep-linkable simulator URL without opening the browser. |
16864
16873
  | \`granular connect test --json\` | Verify auth and environment connectivity with a real session. |
16865
- | \`granular session create/list/heap/doc --json\` | Create, enumerate, and inspect real session state. |
16874
+ | \`granular session create/list/heap/doc --json\` | Create, enumerate, and inspect real session state. \`session list\` defaults to 25 rows; use \`--session-scope\`, \`--subject-id\`, \`--limit\`, and \`--offset\` for deterministic paging. |
16866
16875
  | \`granular graphql --query '...' --json\` | Run a GraphQL query or mutation against the live graph. |
16867
16876
  | \`granular effects list/diff --json\` | Inspect declared versus live ready effects. |
16868
16877
  | \`granular job run --file ./job.ts --json\` | Execute a real runtime job from the terminal or CI. |
@@ -17542,8 +17551,8 @@ Use this for command execution, environment setup, and shipping flows.
17542
17551
  | \`granular graphql --query '...' --json\` | Query the live graph through the environment GraphQL API |
17543
17552
  | \`granular effects list --json\` | Inspect declared and live effects for an environment |
17544
17553
  | \`granular effects diff --json\` | Compare declared effects to live ready handlers |
17545
- | \`granular session create --json\` | Create a fresh session for an environment |
17546
- | \`granular session list --json\` | List indexed sessions for an environment |
17554
+ | \`granular session create --session-scope <scope> --json\` | Create a fresh, application-scoped session for an environment |
17555
+ | \`granular session list --session-scope <scope> --limit 25 --offset 0 --json\` | List one bounded page of indexed sessions |
17547
17556
  | \`granular session heap --json\` | Inspect session heap |
17548
17557
  | \`granular session doc --json\` | Inspect the Automerge-backed session document |
17549
17558
  | \`granular job run --file ./job.ts\` | Execute a real job against the ontology runtime |
@@ -17626,8 +17635,8 @@ Use this for runtime debugging after the ontology builds but behavior does not m
17626
17635
 
17627
17636
  | Goal | Preferred path |
17628
17637
  | --- | --- |
17629
- | Create or rotate a session | \`granular session create --json\` |
17630
- | List known sessions | \`granular session list --json\` |
17638
+ | Create or rotate a session | \`granular session create --session-scope <scope> --json\` |
17639
+ | List known sessions | \`granular session list --session-scope <scope> --limit 25 --offset 0 --json\` |
17631
17640
  | Inspect heap | \`granular session heap --json\` |
17632
17641
  | Inspect full document | \`granular session doc --json\` |
17633
17642
  | Verify connectivity | \`granular connect test\` |
@@ -18938,7 +18947,7 @@ async function deployCommand(options = {}) {
18938
18947
  ]);
18939
18948
  const devTag = tags.find((tag2) => tag2.name === "dev");
18940
18949
  const prodTag = tags.find((tag2) => tag2.name === "prod");
18941
- const existingVersion = versions.find(
18950
+ const existingVersion = options.releaseKey ? null : versions.find(
18942
18951
  (version2) => version2.manifestDigest === uploadedManifest.digest
18943
18952
  ) || null;
18944
18953
  const existingVersionId = existingVersion?.buildId || null;
@@ -18956,19 +18965,27 @@ async function deployCommand(options = {}) {
18956
18965
  } else {
18957
18966
  const version2 = await api.createVersion(
18958
18967
  config.sandboxId,
18959
- uploadedManifest.manifestId
18960
- );
18961
- const startTime = Date.now();
18962
- building.text = ` Running build for version... ${brand.muted(version2.buildId)}`;
18963
- completed = await api.waitForVersionBuild(version2.buildId, (status) => {
18964
- const elapsed = Math.round((Date.now() - startTime) / 1e3);
18965
- building.text = ` Running build for version... ${brand.muted(`${status} (${elapsed}s)`)}`;
18966
- });
18967
- totalTime = Math.round((Date.now() - startTime) / 1e3);
18968
- deployedToDev = true;
18969
- building.succeed(
18970
- ` Dev now points to ${brand.secondary(completed.buildId)} (${totalTime}s)`
18968
+ uploadedManifest.manifestId,
18969
+ options.releaseKey
18971
18970
  );
18971
+ if (version2.createdNewVersion === false && version2.status === "completed") {
18972
+ completed = version2;
18973
+ building.succeed(
18974
+ ` Reused ${brand.secondary(version2.buildId)}; the release key is unchanged.`
18975
+ );
18976
+ } else {
18977
+ const startTime = Date.now();
18978
+ building.text = ` Running build for version... ${brand.muted(version2.buildId)}`;
18979
+ completed = await api.waitForVersionBuild(version2.buildId, (status) => {
18980
+ const elapsed = Math.round((Date.now() - startTime) / 1e3);
18981
+ building.text = ` Running build for version... ${brand.muted(`${status} (${elapsed}s)`)}`;
18982
+ });
18983
+ totalTime = Math.round((Date.now() - startTime) / 1e3);
18984
+ deployedToDev = true;
18985
+ building.succeed(
18986
+ ` Dev now points to ${brand.secondary(completed.buildId)} (${totalTime}s)`
18987
+ );
18988
+ }
18972
18989
  }
18973
18990
  if (!completed) {
18974
18991
  throw new Error("Could not resolve the current ontology version.");
@@ -18999,6 +19016,7 @@ async function deployCommand(options = {}) {
18999
19016
  Release: options.prod ? deployedToDev ? "dev and prod now point to this version" : "prod promoted to the version already on dev" : "dev updated to this version",
19000
19017
  "Agent doc": "GRANULAR_SANDBOX.md"
19001
19018
  });
19019
+ console.log(`GRANULAR_RELEASE_CHANGED=${deployedToDev ? "true" : "false"}`);
19002
19020
  console.log();
19003
19021
  if (options.prod) {
19004
19022
  info(
@@ -19560,16 +19578,37 @@ var WSClient = class {
19560
19578
  tokenRefreshTimer = null;
19561
19579
  isExplicitlyDisconnected = false;
19562
19580
  reconnectAttempts = 0;
19581
+ connectPromise = null;
19582
+ connectionEpoch = 0;
19583
+ cancelConnectAttempt = null;
19563
19584
  options;
19564
19585
  constructor(options) {
19565
19586
  this.options = options;
19566
19587
  this.url = options.url;
19567
19588
  this.sessionId = options.sessionId;
19568
19589
  this.token = options.token;
19590
+ if (options.initialDocumentSnapshot) {
19591
+ this.seedDocumentSnapshot(options.initialDocumentSnapshot);
19592
+ }
19569
19593
  }
19570
19594
  get currentSessionId() {
19571
19595
  return this.sessionId;
19572
19596
  }
19597
+ seedDocumentSnapshot(document) {
19598
+ if (!document || typeof document !== "object" || Array.isArray(document)) {
19599
+ return;
19600
+ }
19601
+ try {
19602
+ this.doc = document instanceof Uint8Array ? Automerge__namespace.load(document) : Automerge__namespace.from(document);
19603
+ this.syncState = Automerge__namespace.initSyncState();
19604
+ this.emit("sync", this.doc);
19605
+ } catch (error2) {
19606
+ console.warn("[Granular] Failed to seed cached session document", error2);
19607
+ }
19608
+ }
19609
+ saveDocumentSnapshot() {
19610
+ return Automerge__namespace.save(this.doc);
19611
+ }
19573
19612
  clearTokenRefreshTimer() {
19574
19613
  if (this.tokenRefreshTimer) {
19575
19614
  clearTimeout(this.tokenRefreshTimer);
@@ -19679,8 +19718,23 @@ var WSClient = class {
19679
19718
  * Connect to the WebSocket server
19680
19719
  * @returns {Promise<void>} Resolves when connection is open
19681
19720
  */
19682
- async connect() {
19721
+ async connect(options = {}) {
19722
+ if (this.ws?.readyState === READY_STATE_OPEN) return;
19723
+ if (this.connectPromise) return this.connectPromise;
19724
+ const connectPromise = this.connectAttempt(options.signal);
19725
+ this.connectPromise = connectPromise;
19726
+ try {
19727
+ await connectPromise;
19728
+ } finally {
19729
+ if (this.connectPromise === connectPromise) {
19730
+ this.connectPromise = null;
19731
+ }
19732
+ }
19733
+ }
19734
+ async connectAttempt(signal) {
19735
+ if (signal?.aborted) throw new Error("WebSocket connect aborted");
19683
19736
  const token = await this.resolveTokenForConnect();
19737
+ if (signal?.aborted) throw new Error("WebSocket connect aborted");
19684
19738
  this.isExplicitlyDisconnected = false;
19685
19739
  this.scheduleTokenRefresh();
19686
19740
  if (this.reconnectTimer) {
@@ -19692,7 +19746,7 @@ var WSClient = class {
19692
19746
  try {
19693
19747
  const wsModule = await import('ws');
19694
19748
  WebSocketClass = wsModule.default || wsModule;
19695
- } catch (e) {
19749
+ } catch {
19696
19750
  }
19697
19751
  }
19698
19752
  if (!WebSocketClass) {
@@ -19700,83 +19754,97 @@ var WSClient = class {
19700
19754
  'No WebSocket implementation found. If using Node.js, please install "ws" and pass the constructor to the SDK options: { WebSocketCtor: WebSocket }.'
19701
19755
  );
19702
19756
  }
19757
+ const epoch = ++this.connectionEpoch;
19758
+ const wsUrl = new URL(this.url);
19759
+ wsUrl.searchParams.set("sessionId", this.sessionId);
19760
+ wsUrl.searchParams.set("token", token);
19761
+ const socket = new WebSocketClass(wsUrl.toString());
19762
+ this.ws = socket;
19703
19763
  return new Promise((resolve2, reject) => {
19704
- try {
19705
- const wsUrl = new URL(this.url);
19706
- wsUrl.searchParams.set("sessionId", this.sessionId);
19707
- wsUrl.searchParams.set("token", token);
19708
- this.ws = new WebSocketClass(wsUrl.toString());
19709
- if (!this.ws) throw new Error("Failed to create WebSocket");
19710
- const socket = this.ws;
19711
- if (typeof socket.on === "function") {
19712
- socket.on("open", () => {
19713
- if (this.reconnectTimer) {
19714
- clearTimeout(this.reconnectTimer);
19715
- this.reconnectTimer = null;
19716
- }
19717
- this.reconnectAttempts = 0;
19718
- this.emit("open", {});
19719
- resolve2();
19720
- });
19721
- socket.on("message", (data) => {
19722
- try {
19723
- const message = JSON.parse(data.toString());
19724
- this.handleMessage(message);
19725
- } catch (error2) {
19726
- console.error("[Granular] Failed to parse message:", error2);
19727
- }
19728
- });
19729
- socket.on("error", (error2) => {
19730
- this.emit("error", error2);
19731
- if (socket.readyState !== READY_STATE_OPEN) {
19732
- reject(error2);
19733
- }
19734
- });
19735
- socket.on("close", (code, reason) => {
19736
- this.handleDisconnect({
19737
- code,
19738
- reason: this.normalizeReason(reason),
19739
- // ws does not provide wasClean on Node-style close callback
19740
- wasClean: code === 1e3
19741
- });
19742
- });
19764
+ let settled = false;
19765
+ const isCurrent = () => this.connectionEpoch === epoch && this.ws === socket;
19766
+ const finish = (error2) => {
19767
+ if (settled) return;
19768
+ settled = true;
19769
+ if (this.cancelConnectAttempt === handleAbort) {
19770
+ this.cancelConnectAttempt = null;
19771
+ }
19772
+ signal?.removeEventListener("abort", handleAbort);
19773
+ if (error2) {
19774
+ reject(error2 instanceof Error ? error2 : new Error(String(error2)));
19743
19775
  } else {
19744
- this.ws.onopen = () => {
19745
- if (this.reconnectTimer) {
19746
- clearTimeout(this.reconnectTimer);
19747
- this.reconnectTimer = null;
19748
- }
19749
- this.reconnectAttempts = 0;
19750
- this.emit("open", {});
19751
- resolve2();
19752
- };
19753
- this.ws.onmessage = (event) => {
19754
- try {
19755
- const data = event.data;
19756
- const message = JSON.parse(data.toString());
19757
- this.handleMessage(message);
19758
- } catch (error2) {
19759
- console.error("[Granular] Failed to parse message:", error2);
19760
- }
19761
- };
19762
- this.ws.onerror = (event) => {
19763
- const error2 = new Error("WebSocket error");
19764
- error2.event = event;
19765
- this.emit("error", error2);
19766
- if (this.ws?.readyState !== READY_STATE_OPEN) {
19767
- reject(error2);
19768
- }
19769
- };
19770
- this.ws.onclose = (event) => {
19771
- this.handleDisconnect({
19772
- code: event.code,
19773
- reason: event.reason,
19774
- wasClean: event.wasClean
19775
- });
19776
- };
19776
+ resolve2();
19777
19777
  }
19778
- } catch (error2) {
19779
- reject(error2);
19778
+ };
19779
+ const closeStaleSocket = () => {
19780
+ try {
19781
+ socket.close(1e3, "Stale connection attempt");
19782
+ } catch {
19783
+ }
19784
+ };
19785
+ const handleAbort = () => {
19786
+ if (isCurrent()) {
19787
+ this.connectionEpoch += 1;
19788
+ this.ws = null;
19789
+ }
19790
+ closeStaleSocket();
19791
+ finish(new Error("WebSocket connect aborted"));
19792
+ };
19793
+ this.cancelConnectAttempt = handleAbort;
19794
+ const handleOpen = () => {
19795
+ if (!isCurrent()) {
19796
+ closeStaleSocket();
19797
+ return;
19798
+ }
19799
+ this.reconnectAttempts = 0;
19800
+ this.emit("open", {});
19801
+ finish();
19802
+ };
19803
+ const handleMessage = (data) => {
19804
+ if (!isCurrent()) return;
19805
+ try {
19806
+ const text = typeof data === "string" ? data : data && typeof data === "object" && "toString" in data ? String(data.toString()) : "";
19807
+ this.handleMessage(JSON.parse(text));
19808
+ } catch (error2) {
19809
+ console.error("[Granular] Failed to parse message:", error2);
19810
+ }
19811
+ };
19812
+ const handleError = (error2) => {
19813
+ if (!isCurrent()) return;
19814
+ const typedError = error2 instanceof Error ? error2 : new Error("WebSocket error");
19815
+ this.emit("error", typedError);
19816
+ if (socket.readyState !== READY_STATE_OPEN) finish(typedError);
19817
+ };
19818
+ const handleClose = (close) => {
19819
+ if (!isCurrent()) return;
19820
+ if (!settled) {
19821
+ finish(
19822
+ new Error(
19823
+ `WebSocket closed before ready${close.code ? ` (code=${close.code})` : ""}`
19824
+ )
19825
+ );
19826
+ }
19827
+ this.handleDisconnect({
19828
+ code: close.code,
19829
+ reason: this.normalizeReason(close.reason),
19830
+ wasClean: close.wasClean
19831
+ });
19832
+ };
19833
+ signal?.addEventListener("abort", handleAbort, { once: true });
19834
+ const nodeSocket = socket;
19835
+ if (typeof nodeSocket.on === "function") {
19836
+ nodeSocket.on("open", handleOpen);
19837
+ nodeSocket.on("message", handleMessage);
19838
+ nodeSocket.on("error", handleError);
19839
+ nodeSocket.on(
19840
+ "close",
19841
+ (code, reason) => handleClose({ code, reason, wasClean: code === 1e3 })
19842
+ );
19843
+ } else {
19844
+ socket.onopen = handleOpen;
19845
+ socket.onmessage = (event) => handleMessage(event.data);
19846
+ socket.onerror = handleError;
19847
+ socket.onclose = (event) => handleClose(event);
19780
19848
  }
19781
19849
  });
19782
19850
  }
@@ -19794,9 +19862,58 @@ var WSClient = class {
19794
19862
  return void 0;
19795
19863
  }
19796
19864
  rejectPending(error2) {
19797
- this.messageQueue.forEach((pending) => pending.reject(error2));
19865
+ this.messageQueue.forEach((pending) => {
19866
+ clearTimeout(pending.timeout);
19867
+ pending.reject(error2);
19868
+ });
19798
19869
  this.messageQueue = [];
19799
19870
  }
19871
+ emitReconnectErrorMessage(error2) {
19872
+ const reconnectInfo = {
19873
+ error: error2,
19874
+ sessionId: this.sessionId,
19875
+ timestamp: Date.now()
19876
+ };
19877
+ this.emit("reconnect_error", reconnectInfo);
19878
+ if (this.options.onReconnectError) {
19879
+ try {
19880
+ this.options.onReconnectError(reconnectInfo);
19881
+ } catch (callbackError) {
19882
+ console.error(
19883
+ "[Granular] onReconnectError callback failed:",
19884
+ callbackError
19885
+ );
19886
+ }
19887
+ }
19888
+ }
19889
+ scheduleReconnectAttempt() {
19890
+ if (this.isExplicitlyDisconnected || this.reconnectTimer) return null;
19891
+ const baseReconnectDelayMs = typeof this.options.reconnectDelayMs === "number" && Number.isFinite(this.options.reconnectDelayMs) && this.options.reconnectDelayMs > 0 ? this.options.reconnectDelayMs : DEFAULT_RECONNECT_DELAY_MS;
19892
+ const maxReconnectAttempts = typeof this.options.maxReconnectAttempts === "number" && Number.isFinite(this.options.maxReconnectAttempts) && this.options.maxReconnectAttempts >= 0 ? Math.floor(this.options.maxReconnectAttempts) : DEFAULT_MAX_RECONNECT_ATTEMPTS;
19893
+ if (this.reconnectAttempts >= maxReconnectAttempts) {
19894
+ this.emitReconnectErrorMessage(
19895
+ `WebSocket reconnect attempts exhausted after ${maxReconnectAttempts} attempt(s).`
19896
+ );
19897
+ return null;
19898
+ }
19899
+ this.reconnectAttempts += 1;
19900
+ const reconnectDelayMs = Math.min(
19901
+ 3e4,
19902
+ baseReconnectDelayMs * 2 ** Math.max(0, this.reconnectAttempts - 1)
19903
+ );
19904
+ this.reconnectTimer = setTimeout(() => {
19905
+ this.reconnectTimer = null;
19906
+ console.log("[Granular] Attempting reconnect...");
19907
+ this.connect().catch((error2) => {
19908
+ console.error("[Granular] Reconnect failed:", error2);
19909
+ this.emitReconnectErrorMessage(
19910
+ error2 instanceof Error ? error2.message : String(error2)
19911
+ );
19912
+ this.scheduleReconnectAttempt();
19913
+ });
19914
+ }, reconnectDelayMs);
19915
+ return reconnectDelayMs;
19916
+ }
19800
19917
  buildDisconnectError(info2) {
19801
19918
  const details = [
19802
19919
  info2.code !== void 0 ? `code=${info2.code}` : void 0,
@@ -19806,8 +19923,6 @@ var WSClient = class {
19806
19923
  return new Error(`WebSocket disconnected${suffix}`);
19807
19924
  }
19808
19925
  handleDisconnect(close = {}) {
19809
- const baseReconnectDelayMs = typeof this.options.reconnectDelayMs === "number" && Number.isFinite(this.options.reconnectDelayMs) && this.options.reconnectDelayMs > 0 ? this.options.reconnectDelayMs : DEFAULT_RECONNECT_DELAY_MS;
19810
- const maxReconnectAttempts = typeof this.options.maxReconnectAttempts === "number" && Number.isFinite(this.options.maxReconnectAttempts) && this.options.maxReconnectAttempts >= 0 ? Math.floor(this.options.maxReconnectAttempts) : DEFAULT_MAX_RECONNECT_ATTEMPTS;
19811
19926
  const unexpected = !this.isExplicitlyDisconnected;
19812
19927
  const info2 = {
19813
19928
  code: close.code,
@@ -19827,32 +19942,9 @@ var WSClient = class {
19827
19942
  const disconnectError = this.buildDisconnectError(info2);
19828
19943
  this.rejectPending(disconnectError);
19829
19944
  this.emit("disconnect", info2);
19830
- if (this.reconnectAttempts >= maxReconnectAttempts) {
19831
- const reconnectInfo = {
19832
- error: `WebSocket reconnect attempts exhausted after ${maxReconnectAttempts} attempt(s).`,
19833
- sessionId: this.sessionId,
19834
- timestamp: Date.now()
19835
- };
19836
- this.emit("reconnect_error", reconnectInfo);
19837
- if (this.options.onReconnectError) {
19838
- try {
19839
- this.options.onReconnectError(reconnectInfo);
19840
- } catch (callbackError) {
19841
- console.error(
19842
- "[Granular] onReconnectError callback failed:",
19843
- callbackError
19844
- );
19845
- }
19846
- }
19847
- return;
19848
- }
19849
- this.reconnectAttempts += 1;
19850
- const reconnectDelayMs = Math.min(
19851
- 3e4,
19852
- baseReconnectDelayMs * 2 ** Math.max(0, this.reconnectAttempts - 1)
19853
- );
19854
- info2.reconnectScheduled = true;
19855
- info2.reconnectDelayMs = reconnectDelayMs;
19945
+ const reconnectDelayMs = this.scheduleReconnectAttempt();
19946
+ info2.reconnectScheduled = reconnectDelayMs !== null;
19947
+ if (reconnectDelayMs !== null) info2.reconnectDelayMs = reconnectDelayMs;
19856
19948
  if (this.options.onUnexpectedClose) {
19857
19949
  try {
19858
19950
  this.options.onUnexpectedClose(info2);
@@ -19863,28 +19955,6 @@ var WSClient = class {
19863
19955
  );
19864
19956
  }
19865
19957
  }
19866
- this.reconnectTimer = setTimeout(() => {
19867
- console.log("[Granular] Attempting reconnect...");
19868
- this.connect().catch((error2) => {
19869
- console.error("[Granular] Reconnect failed:", error2);
19870
- const reconnectInfo = {
19871
- error: error2 instanceof Error ? error2.message : String(error2),
19872
- sessionId: this.sessionId,
19873
- timestamp: Date.now()
19874
- };
19875
- this.emit("reconnect_error", reconnectInfo);
19876
- if (this.options.onReconnectError) {
19877
- try {
19878
- this.options.onReconnectError(reconnectInfo);
19879
- } catch (callbackError) {
19880
- console.error(
19881
- "[Granular] onReconnectError callback failed:",
19882
- callbackError
19883
- );
19884
- }
19885
- }
19886
- });
19887
- }, reconnectDelayMs);
19888
19958
  }
19889
19959
  }
19890
19960
  handleMessage(message) {
@@ -19995,6 +20065,7 @@ var WSClient = class {
19995
20065
  const response = message;
19996
20066
  const pending = this.messageQueue.find((q) => q.id === response.id);
19997
20067
  if (pending) {
20068
+ clearTimeout(pending.timeout);
19998
20069
  if (response.type === "rpc_error") {
19999
20070
  pending.reject(
20000
20071
  new Error(
@@ -20040,16 +20111,22 @@ var WSClient = class {
20040
20111
  id
20041
20112
  };
20042
20113
  return new Promise((resolve2, reject) => {
20043
- this.messageQueue.push({ resolve: resolve2, reject, id });
20044
- this.ws.send(JSON.stringify(request));
20045
20114
  const timeoutMs = rpcTimeoutMsForMethod(method);
20046
- setTimeout(() => {
20115
+ const timeout = setTimeout(() => {
20047
20116
  const pending = this.messageQueue.find((q) => q.id === id);
20048
20117
  if (pending) {
20049
20118
  this.messageQueue = this.messageQueue.filter((q) => q.id !== id);
20050
20119
  reject(new Error(`RPC timeout: ${method}`));
20051
20120
  }
20052
20121
  }, timeoutMs);
20122
+ this.messageQueue.push({ resolve: resolve2, reject, id, timeout });
20123
+ try {
20124
+ this.ws.send(JSON.stringify(request));
20125
+ } catch (error2) {
20126
+ clearTimeout(timeout);
20127
+ this.messageQueue = this.messageQueue.filter((q) => q.id !== id);
20128
+ reject(error2 instanceof Error ? error2 : new Error(String(error2)));
20129
+ }
20053
20130
  });
20054
20131
  }
20055
20132
  async handleIncomingRpc(request) {
@@ -20135,15 +20212,18 @@ var WSClient = class {
20135
20212
  /**
20136
20213
  * Disconnect the WebSocket and clear state
20137
20214
  */
20138
- disconnect() {
20215
+ disconnect(options = {}) {
20139
20216
  this.isExplicitlyDisconnected = true;
20217
+ this.cancelConnectAttempt?.();
20218
+ this.cancelConnectAttempt = null;
20219
+ this.connectionEpoch += 1;
20140
20220
  if (this.reconnectTimer) {
20141
20221
  clearTimeout(this.reconnectTimer);
20142
20222
  this.reconnectTimer = null;
20143
20223
  }
20144
20224
  this.clearTokenRefreshTimer();
20145
20225
  if (this.ws) {
20146
- this.ws.close(1e3, "Client disconnect");
20226
+ this.ws.close(1e3, options.reason || "Client disconnect");
20147
20227
  this.ws = null;
20148
20228
  }
20149
20229
  this.rejectPending(new Error("Client explicitly disconnected"));
@@ -20239,8 +20319,12 @@ function normalizePrompt(rawValue) {
20239
20319
  const source = promptRecord || raw;
20240
20320
  const id = typeof source.id === "string" ? source.id : typeof raw.id === "string" ? raw.id : typeof raw.promptId === "string" ? raw.promptId : "";
20241
20321
  if (!id) return null;
20322
+ const jobId = typeof source.jobId === "string" && source.jobId.trim() ? source.jobId.trim() : typeof raw.jobId === "string" && raw.jobId.trim() ? raw.jobId.trim() : void 0;
20323
+ const turnId = typeof source.turnId === "string" && source.turnId.trim() ? source.turnId.trim() : typeof raw.turnId === "string" && raw.turnId.trim() ? raw.turnId.trim() : void 0;
20242
20324
  return {
20243
20325
  id,
20326
+ ...jobId ? { jobId } : {},
20327
+ ...turnId ? { turnId } : {},
20244
20328
  type: normalizePromptType(source === raw ? raw : { ...raw, ...source }),
20245
20329
  title: typeof source.title === "string" ? source.title : "Input required",
20246
20330
  message: typeof source.message === "string" ? source.message : "",
@@ -20327,6 +20411,9 @@ var Session = class {
20327
20411
  this.initialQuota = options.initialQuota || null;
20328
20412
  this.setupEventHandlers();
20329
20413
  this.setupToolInvokeHandler();
20414
+ this.currentDomainRevision = this.extractDomainRevisionFromDoc(
20415
+ this.client.doc
20416
+ );
20330
20417
  }
20331
20418
  extractDomainRevisionFromDoc(doc) {
20332
20419
  const domain = doc?.domain;
@@ -21232,6 +21319,7 @@ function normalizeJobAgentMessageEnvelope(data) {
21232
21319
  }
21233
21320
  return {
21234
21321
  jobId: d.jobId,
21322
+ ...typeof d.turnId === "string" && d.turnId.trim() ? { turnId: d.turnId.trim() } : {},
21235
21323
  message: {
21236
21324
  messageId: d.messageId,
21237
21325
  kind: d.kind === "artifacts" ? "artifacts" : "text",
@@ -21890,9 +21978,14 @@ function normalizeShowRefs(value) {
21890
21978
  variableNames: normalizeRefs(record.variableNames),
21891
21979
  fileIds: normalizeRefs(record.fileIds),
21892
21980
  sessionArtifactIds: normalizeRefs(record.sessionArtifactIds),
21893
- actionSuggestions: normalizeActionSuggestions(record.actionSuggestions)
21981
+ actionSuggestions: normalizeActionSuggestions(record.actionSuggestions),
21982
+ tables: Array.isArray(record.tables) ? record.tables.filter(
21983
+ (table2) => Boolean(
21984
+ table2 && typeof table2 === "object" && !Array.isArray(table2) && Array.isArray(table2.columns) && Array.isArray(table2.rows)
21985
+ )
21986
+ ) : void 0
21894
21987
  };
21895
- return show.entryPaths || show.listNames || show.variableNames || show.fileIds || show.sessionArtifactIds || show.actionSuggestions ? show : void 0;
21988
+ return show.entryPaths || show.listNames || show.variableNames || show.fileIds || show.sessionArtifactIds || show.actionSuggestions || show.tables ? show : void 0;
21896
21989
  }
21897
21990
  function normalizeActionSuggestions(value) {
21898
21991
  if (!Array.isArray(value)) return void 0;
@@ -21914,6 +22007,76 @@ function normalizeActionSuggestions(value) {
21914
22007
  }
21915
22008
  return suggestions.length ? suggestions : void 0;
21916
22009
  }
22010
+ var TRANSCRIPT_MESSAGE_PART_LIMIT = 128;
22011
+ var TRANSCRIPT_MESSAGE_PART_TEXT_LIMIT = 2e5;
22012
+ var TRANSCRIPT_MESSAGE_PART_ACTION_LABEL_LIMIT = 1e3;
22013
+ var TRANSCRIPT_MESSAGE_ACTION_LIMIT = 64;
22014
+ function normalizeConversationMessageActions(value) {
22015
+ if (!Array.isArray(value) || value.length === 0) return void 0;
22016
+ const actions = [];
22017
+ for (const item of value.slice(0, TRANSCRIPT_MESSAGE_ACTION_LIMIT)) {
22018
+ const record = asRecord3(item);
22019
+ const kind = record?.kind;
22020
+ const label = trimString(record?.label ?? record?.title);
22021
+ const status = record?.status;
22022
+ if (kind !== "frontend" && kind !== "backend" && kind !== "system" || !label || label.length > TRANSCRIPT_MESSAGE_PART_ACTION_LABEL_LIMIT) {
22023
+ continue;
22024
+ }
22025
+ actions.push({
22026
+ kind,
22027
+ label,
22028
+ ...status === "done" || status === "queued" || status === "failed" ? { status } : {}
22029
+ });
22030
+ }
22031
+ return actions.length ? actions : void 0;
22032
+ }
22033
+ function normalizeConversationMessageParts(value, canonicalContent, canonicalActions) {
22034
+ if (!Array.isArray(value) || value.length === 0 || value.length > TRANSCRIPT_MESSAGE_PART_LIMIT) {
22035
+ return void 0;
22036
+ }
22037
+ const parts = [];
22038
+ const canonicalActionsById = new Map(
22039
+ (canonicalActions || []).map((action) => [
22040
+ `${action.kind}:${action.label}`,
22041
+ action
22042
+ ])
22043
+ );
22044
+ const seenActionIds = /* @__PURE__ */ new Set();
22045
+ let textLength = 0;
22046
+ for (const item of value) {
22047
+ const record = asRecord3(item);
22048
+ if (!record) return void 0;
22049
+ if (record.type === "text") {
22050
+ if (typeof record.text !== "string" || record.text.length === 0) {
22051
+ return void 0;
22052
+ }
22053
+ textLength += record.text.length;
22054
+ if (textLength > TRANSCRIPT_MESSAGE_PART_TEXT_LIMIT) return void 0;
22055
+ parts.push({ type: "text", text: record.text });
22056
+ continue;
22057
+ }
22058
+ if (record.type !== "action") return void 0;
22059
+ const action = asRecord3(record.action);
22060
+ const kind = action?.kind;
22061
+ const label = trimString(action?.label);
22062
+ if (kind !== "frontend" && kind !== "backend" && kind !== "system" || !label || label.length > TRANSCRIPT_MESSAGE_PART_ACTION_LABEL_LIMIT) {
22063
+ return void 0;
22064
+ }
22065
+ const actionId = `${kind}:${label}`;
22066
+ const canonicalAction = canonicalActionsById.get(actionId);
22067
+ if (!canonicalAction) return void 0;
22068
+ if (seenActionIds.has(actionId)) continue;
22069
+ seenActionIds.add(actionId);
22070
+ parts.push({
22071
+ type: "action",
22072
+ action: canonicalAction
22073
+ });
22074
+ }
22075
+ const orderedText = parts.filter(
22076
+ (part) => part.type === "text"
22077
+ ).map((part) => part.text).join("");
22078
+ return orderedText === canonicalContent ? parts : void 0;
22079
+ }
21917
22080
  function stringifyTranscriptValue(value, fallback2 = "") {
21918
22081
  if (typeof value === "string") {
21919
22082
  return value.trim() || fallback2;
@@ -22072,10 +22235,12 @@ function normalizeConversationMessage(raw, artifactsById) {
22072
22235
  const content = trimString(
22073
22236
  record.content ?? record.reply ?? record.message ?? record.text
22074
22237
  );
22238
+ const actions = role === "assistant" ? normalizeConversationMessageActions(record.actions) : void 0;
22239
+ const parts = role === "assistant" ? normalizeConversationMessageParts(record.parts, content, actions) : void 0;
22075
22240
  const show = normalizeShowRefs(record.show);
22076
22241
  const id = asString(record.id) || crypto.randomUUID();
22077
22242
  const timestamp = asNumber(record.timestamp) || asNumber(record.ts) || 0;
22078
- if (!content && !show) return null;
22243
+ if (!content && !show && !actions?.length) return null;
22079
22244
  const artifactHistory = buildArtifactHistory(show, artifactsById);
22080
22245
  const historyContent = role === "assistant" ? content && artifactHistory ? `[Assistant reply]
22081
22246
  ${content}
@@ -22090,6 +22255,8 @@ ${content}` : artifactHistory : void 0;
22090
22255
  jobId: asString(record.jobId),
22091
22256
  promptId: asString(record.promptId),
22092
22257
  show,
22258
+ actions,
22259
+ parts,
22093
22260
  historyContent,
22094
22261
  source: "conversation"
22095
22262
  };
@@ -22738,7 +22905,12 @@ async function recordOpenAIUsageSpend(options) {
22738
22905
  const metadata = {
22739
22906
  ...options.metadata || {},
22740
22907
  ...options.usage.rawUsage !== void 0 ? { openaiUsage: options.usage.rawUsage } : {},
22741
- usageContext: context
22908
+ usageContext: context,
22909
+ pricingContextTier: options.usage.pricingContextTier,
22910
+ cacheWritePricePerMillionMicros: options.usage.cacheWritePricePerMillionMicros,
22911
+ cacheWriteTokens: options.usage.cacheWriteTokens,
22912
+ cacheWriteCostMicros: options.usage.cacheWriteCostMicros,
22913
+ longContextThresholdTokens: options.usage.longContextThresholdTokens
22742
22914
  };
22743
22915
  const response = await fetch(
22744
22916
  `${toGranularHttpBase(options.apiUrl)}/control/spend/events`,
@@ -22809,6 +22981,18 @@ function buildEffectMetamodelMutations(toolPath, spec) {
22809
22981
  }
22810
22982
 
22811
22983
  // src/client.ts
22984
+ var DEFAULT_CONVERSATION_SESSION_LIST_LIMIT = 100;
22985
+ var MAX_CONVERSATION_SESSION_LIST_LIMIT = 500;
22986
+ var MAX_CONVERSATION_SESSION_LIST_OFFSET = 1e5;
22987
+ function boundedSessionListInteger(value, name, fallback2, minimum, maximum) {
22988
+ if (value === void 0) return fallback2;
22989
+ if (!Number.isInteger(value) || value < minimum || value > maximum) {
22990
+ throw new RangeError(
22991
+ `Session list ${name} must be an integer between ${minimum} and ${maximum}.`
22992
+ );
22993
+ }
22994
+ return value;
22995
+ }
22812
22996
  var STANDARD_MODULES_OPERATIONS = [
22813
22997
  {
22814
22998
  create: "entity",
@@ -23026,7 +23210,8 @@ function normalizeEnvironmentSetupSummary(setup) {
23026
23210
  environmentId: String(setup.environmentId || ""),
23027
23211
  sandboxId: String(setup.sandboxId || ""),
23028
23212
  subjectId: String(setup.subjectId || ""),
23029
- triggerReason: setup.triggerReason === "fresh_after_version_update" ? "fresh_after_version_update" : "new_environment",
23213
+ triggerReason: setup.triggerReason === "fresh_after_version_update" ? "fresh_after_version_update" : setup.triggerReason === "explicit_reset" ? "explicit_reset" : "new_environment",
23214
+ operationKey: typeof setup.operationKey === "string" ? setup.operationKey : null,
23030
23215
  lifecycleStatus: setup.lifecycleStatus === "completed" || setup.lifecycleStatus === "failed" ? setup.lifecycleStatus : "running",
23031
23216
  stage: typeof setup.stage === "string" ? setup.stage : null,
23032
23217
  totalObjectsToImport: Number(setup.totalObjectsToImport || 0),
@@ -23147,7 +23332,7 @@ var Environment = class _Environment {
23147
23332
  }
23148
23333
  get sessions() {
23149
23334
  return {
23150
- list: async (options) => this.listSessions(options?.status || "active"),
23335
+ list: async (options = {}) => this.listSessions(options),
23151
23336
  create: async (options) => this.createSession(options),
23152
23337
  connect: async (sessionId, options) => this.connectSession(sessionId, options),
23153
23338
  reopen: async (sessionId, options) => this.reopenSession(sessionId, options),
@@ -23282,17 +23467,12 @@ var Environment = class _Environment {
23282
23467
  */
23283
23468
  async disconnect() {
23284
23469
  }
23285
- async listSessions(status = "active") {
23286
- if (status === "all") {
23287
- const [active, closed] = await Promise.all([
23288
- this.granular.listOpenSessions({ environmentId: this.environmentId }),
23289
- this.granular.listClosedSessions({ environmentId: this.environmentId })
23290
- ]);
23291
- return [...active, ...closed].sort(
23292
- (left, right) => Date.parse(right.lastSeenAt) - Date.parse(left.lastSeenAt)
23293
- );
23294
- }
23295
- return status === "closed" ? this.granular.listClosedSessions({ environmentId: this.environmentId }) : this.granular.listOpenSessions({ environmentId: this.environmentId });
23470
+ async listSessions(optionsOrStatus = {}) {
23471
+ const options = typeof optionsOrStatus === "string" ? { status: optionsOrStatus } : optionsOrStatus;
23472
+ return this.granular.listSessions({
23473
+ ...options,
23474
+ environmentId: this.environmentId
23475
+ });
23296
23476
  }
23297
23477
  async getUserEnvironmentState(options = {}) {
23298
23478
  return this.granular.getUserEnvironmentState({
@@ -23310,6 +23490,7 @@ var Environment = class _Environment {
23310
23490
  return this.granular.createSession({
23311
23491
  environmentId: this.environmentId,
23312
23492
  clientId: options?.clientId,
23493
+ sessionScope: options?.sessionScope,
23313
23494
  initialHeap: options?.initialHeap
23314
23495
  });
23315
23496
  }
@@ -24312,6 +24493,7 @@ var Environment = class _Environment {
24312
24493
  records: recordsToImport,
24313
24494
  batchSize: options.batchSize,
24314
24495
  setupRunId: options.setupRunId,
24496
+ operationKey: options.operationKey,
24315
24497
  writeMode: options.writeMode
24316
24498
  })
24317
24499
  }
@@ -24872,7 +25054,7 @@ var EnvironmentSession = class extends Session {
24872
25054
  * Close only the socket transport without sending `client.goodbye`.
24873
25055
  */
24874
25056
  disconnectTransport() {
24875
- this.client.disconnect();
25057
+ this.client.disconnect({ reason: "Transport detach" });
24876
25058
  }
24877
25059
  /**
24878
25060
  * Backwards-compatible alias for `disconnect()`.
@@ -25116,6 +25298,107 @@ var Granular = class _Granular {
25116
25298
  await this.maybeRunEnvironmentImporter(resolved, environment);
25117
25299
  return environment;
25118
25300
  }
25301
+ /**
25302
+ * Read the active environment selected by Granular for an already-recorded
25303
+ * external user. This is intentionally read-only: browser/login code must
25304
+ * not create subjects or environments as a side effect.
25305
+ */
25306
+ async getActiveEnvironmentForUser(options) {
25307
+ const sandboxId = options.sandboxId.trim();
25308
+ const tagName = options.tag.trim();
25309
+ const userId = options.userId.trim();
25310
+ if (!sandboxId || !tagName || !userId) {
25311
+ throw new Error(
25312
+ "getActiveEnvironmentForUser() requires sandboxId, tag, and userId."
25313
+ );
25314
+ }
25315
+ const subjects = await this.request(
25316
+ `/control/subjects?identityId=${encodeURIComponent(userId)}`
25317
+ );
25318
+ const subject = (subjects.items || []).find(
25319
+ (item) => item.identityId === userId || item.userId === userId
25320
+ );
25321
+ if (!subject?.subjectId && !subject?.granularId) {
25322
+ return null;
25323
+ }
25324
+ const subjectId = subject.subjectId || subject.granularId;
25325
+ const tags = await this.request(
25326
+ `/control/sandboxes/${encodeURIComponent(sandboxId)}/tags`
25327
+ );
25328
+ const tag2 = (tags.items || []).find(
25329
+ (item) => item?.name === tagName
25330
+ );
25331
+ if (!tag2) return null;
25332
+ const query = new URLSearchParams({
25333
+ tagId: tag2.tagId,
25334
+ slot: options.slot?.trim() || "default"
25335
+ });
25336
+ try {
25337
+ const payload = await this.request(
25338
+ `/control/sandboxes/${encodeURIComponent(sandboxId)}/subjects/${encodeURIComponent(subjectId)}/active-environment?${query.toString()}`
25339
+ );
25340
+ return payload.environment ? this.bindEnvironmentHandle(
25341
+ normalizeEnvironmentData(payload.environment)
25342
+ ) : null;
25343
+ } catch (error2) {
25344
+ const message = error2 instanceof Error ? error2.message : String(error2);
25345
+ if (message.includes("404") || message.includes("not found")) {
25346
+ return null;
25347
+ }
25348
+ throw error2;
25349
+ }
25350
+ }
25351
+ /**
25352
+ * Register one reviewed, pre-existing environment as the active workspace
25353
+ * for an external user. This is for a controlled migration only: it does
25354
+ * not create an environment and it does not run an importer.
25355
+ */
25356
+ async adoptEnvironmentForUser(options) {
25357
+ const sandboxId = options.sandboxId.trim();
25358
+ const tagName = options.tag.trim();
25359
+ const userId = options.userId.trim();
25360
+ const environmentId = options.environmentId.trim();
25361
+ if (!sandboxId || !tagName || !userId || !environmentId) {
25362
+ throw new Error(
25363
+ "adoptEnvironmentForUser() requires sandboxId, tag, userId, and environmentId."
25364
+ );
25365
+ }
25366
+ const subjects = await this.request(
25367
+ `/control/subjects?identityId=${encodeURIComponent(userId)}`
25368
+ );
25369
+ const subject = (subjects.items || []).find(
25370
+ (item) => item.identityId === userId || item.userId === userId
25371
+ );
25372
+ if (!subject?.subjectId && !subject?.granularId) {
25373
+ throw new Error(`No Granular subject exists for user ${userId}.`);
25374
+ }
25375
+ const tags = await this.request(`/control/sandboxes/${encodeURIComponent(sandboxId)}/tags`);
25376
+ const tag2 = (tags.items || []).find(
25377
+ (item) => item?.name === tagName
25378
+ );
25379
+ if (!tag2) {
25380
+ throw new Error(`Tag ${tagName} was not found in sandbox ${sandboxId}.`);
25381
+ }
25382
+ const payload = await this.request(
25383
+ "/control/environment-activations/adopt",
25384
+ {
25385
+ method: "POST",
25386
+ body: JSON.stringify({
25387
+ environmentId,
25388
+ subjectId: subject.subjectId || subject.granularId,
25389
+ tagId: tag2.tagId,
25390
+ slot: options.slot?.trim() || "default",
25391
+ confirmExistingData: true
25392
+ })
25393
+ }
25394
+ );
25395
+ if (!payload.environment) {
25396
+ throw new Error("Granular did not return the adopted environment.");
25397
+ }
25398
+ return this.bindEnvironmentHandle(
25399
+ normalizeEnvironmentData(payload.environment)
25400
+ );
25401
+ }
25119
25402
  /**
25120
25403
  * Deprecated compatibility alias for `openEnvironment()`.
25121
25404
  *
@@ -25147,7 +25430,9 @@ var Granular = class _Granular {
25147
25430
  requestedOntology,
25148
25431
  sandboxId: environmentData.sandboxId,
25149
25432
  subjectId: environmentData.subjectId,
25150
- setupTriggerReason: options.reason || "new_environment"
25433
+ externalUserId: environmentData.subjectId,
25434
+ setupTriggerReason: options.reason || "new_environment",
25435
+ setupOperationKey: options.operationKey
25151
25436
  },
25152
25437
  environment
25153
25438
  );
@@ -25159,20 +25444,17 @@ var Granular = class _Granular {
25159
25444
  }
25160
25445
  return tag2;
25161
25446
  }
25162
- buildManagedEnvironmentName(tag2, versionId) {
25163
- return `__sdk__${tag2}__${versionId}__pinned`;
25447
+ buildManagedEnvironmentName(tag2, versionId, resetKey) {
25448
+ if (!resetKey) {
25449
+ return `__sdk__${tag2}__${versionId}__tracked`;
25450
+ }
25451
+ const safeResetKey = resetKey.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 80);
25452
+ return `__sdk__${tag2}__${versionId}__reset__${safeResetKey}`;
25164
25453
  }
25165
25454
  isManagedEnvironmentName(environment, tagName) {
25166
25455
  const name = environment.environment || environment.envName || "";
25167
25456
  return name.startsWith(`__sdk__${tagName}__`);
25168
25457
  }
25169
- isPinnedToVersion(environment, versionId) {
25170
- return environment.buildPolicy.mode === "pinned" && (environment.versionId === versionId || environment.buildPolicy.versionId === versionId || environment.buildPolicy.buildId === versionId);
25171
- }
25172
- matchesTagTrackedEnvironment(environment, tagName, tagId) {
25173
- const environmentTagName = environment.tag?.name || environment.buildPolicy.tagName || null;
25174
- 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);
25175
- }
25176
25458
  sortEnvironmentsByRecency(environments) {
25177
25459
  return [...environments].sort(
25178
25460
  (left, right) => right.updatedAt - left.updatedAt
@@ -25222,61 +25504,187 @@ var Granular = class _Granular {
25222
25504
  `Tag "${tagName}" does not currently point to a build/version.`
25223
25505
  );
25224
25506
  }
25507
+ const slot = options.slot?.trim() || "default";
25508
+ const resetKey = options.resetKey?.trim() || void 0;
25509
+ const resolveActive = async (operationKey) => {
25510
+ const query = new URLSearchParams({ tagId: tag2.tagId, slot });
25511
+ if (operationKey) query.set("operationKey", operationKey);
25512
+ try {
25513
+ const payload = await this.request(
25514
+ `/control/sandboxes/${encodeURIComponent(sandbox.sandboxId)}/subjects/${encodeURIComponent(user.granularId)}/active-environment?${query.toString()}`
25515
+ );
25516
+ return payload.environment ? normalizeEnvironmentData(payload.environment) : null;
25517
+ } catch (error2) {
25518
+ const message = error2 instanceof Error ? error2.message : String(error2);
25519
+ if (message.includes("404") || message.includes("not found")) {
25520
+ return null;
25521
+ }
25522
+ throw error2;
25523
+ }
25524
+ };
25525
+ const activate = async (environment2) => {
25526
+ const payload = await this.request(
25527
+ "/control/environment-activations",
25528
+ {
25529
+ method: "POST",
25530
+ body: JSON.stringify({
25531
+ environmentId: environment2.environmentId,
25532
+ tagId: tag2.tagId,
25533
+ slot,
25534
+ operationKey: resetKey,
25535
+ operation: resetKey ? "explicit_reset" : void 0
25536
+ })
25537
+ }
25538
+ );
25539
+ return normalizeEnvironmentData(payload.environment);
25540
+ };
25541
+ if (resetKey) {
25542
+ const resetEnvironment = await resolveActive(resetKey);
25543
+ if (resetEnvironment) {
25544
+ return {
25545
+ environment: resetEnvironment,
25546
+ requestedOntology: ontology,
25547
+ sandboxId: sandbox.sandboxId,
25548
+ subjectId: user.granularId,
25549
+ externalUserId: user.userId,
25550
+ // Retrying an explicit reset must also resume its durable setup run.
25551
+ // Otherwise a Container crash after queue submission would leave a
25552
+ // valid environment permanently marked as "running".
25553
+ setupTriggerReason: "explicit_reset",
25554
+ setupOperationKey: resetKey
25555
+ };
25556
+ }
25557
+ } else {
25558
+ const active = await resolveActive();
25559
+ if (active && (active.versionId === targetVersionId || options.createFreshIfOutdated !== true)) {
25560
+ return {
25561
+ environment: active,
25562
+ requestedOntology: ontology,
25563
+ sandboxId: sandbox.sandboxId,
25564
+ subjectId: user.granularId,
25565
+ externalUserId: user.userId
25566
+ };
25567
+ }
25568
+ }
25225
25569
  const allEnvironments = await this.environments.list(sandbox.sandboxId);
25226
25570
  const userEnvironments = allEnvironments.filter(
25227
- (environment) => environment.subjectId === user.granularId
25571
+ (environment2) => environment2.subjectId === user.granularId
25228
25572
  );
25229
25573
  const currentMatches = this.sortEnvironmentsByRecency(
25230
25574
  userEnvironments.filter(
25231
- (environment) => this.matchesTagTrackedEnvironment(environment, tagName, tag2.tagId) && environment.versionId === targetVersionId && (!this.isManagedEnvironmentName(environment, tagName) || this.isPinnedToVersion(environment, targetVersionId))
25575
+ (environment2) => environment2.tagId === tag2.tagId && environment2.versionId === targetVersionId
25232
25576
  )
25233
25577
  );
25234
- if (currentMatches.length > 0) {
25578
+ if (!resetKey && currentMatches.length > 0) {
25579
+ const environment2 = await activate(currentMatches[0]);
25235
25580
  return {
25236
- environment: currentMatches[0],
25581
+ environment: environment2,
25237
25582
  requestedOntology: ontology,
25238
25583
  sandboxId: sandbox.sandboxId,
25239
- subjectId: user.granularId
25584
+ subjectId: user.granularId,
25585
+ externalUserId: user.userId
25240
25586
  };
25241
25587
  }
25242
25588
  const outdatedMatches = this.sortEnvironmentsByRecency(
25243
- userEnvironments.filter(
25244
- (environment) => this.matchesTagTrackedEnvironment(environment, tagName, tag2.tagId)
25245
- )
25589
+ userEnvironments.filter((environment2) => environment2.tagId === tag2.tagId)
25246
25590
  );
25247
25591
  if (outdatedMatches.length > 0 && options.createFreshIfOutdated !== true) {
25592
+ const environment2 = await activate(outdatedMatches[0]);
25248
25593
  return {
25249
- environment: outdatedMatches[0],
25594
+ environment: environment2,
25250
25595
  requestedOntology: ontology,
25251
25596
  sandboxId: sandbox.sandboxId,
25252
- subjectId: user.granularId
25597
+ subjectId: user.granularId,
25598
+ externalUserId: user.userId
25253
25599
  };
25254
25600
  }
25601
+ const created = await this.environments.create(sandbox.sandboxId, {
25602
+ subjectId: user.granularId,
25603
+ environment: this.buildManagedEnvironmentName(
25604
+ tagName,
25605
+ targetVersionId,
25606
+ resetKey
25607
+ ),
25608
+ tagId: tag2.tagId,
25609
+ permissionProfileId: null
25610
+ });
25611
+ const environment = await activate(created);
25255
25612
  return {
25256
- environment: await this.environments.create(sandbox.sandboxId, {
25257
- subjectId: user.granularId,
25258
- environment: this.buildManagedEnvironmentName(tagName, targetVersionId),
25259
- tagId: tag2.tagId,
25260
- versionId: targetVersionId,
25261
- permissionProfileId: null
25262
- }),
25613
+ environment,
25263
25614
  requestedOntology: ontology,
25264
25615
  sandboxId: sandbox.sandboxId,
25265
25616
  subjectId: user.granularId,
25266
- setupTriggerReason: outdatedMatches.length > 0 ? "fresh_after_version_update" : "new_environment"
25617
+ externalUserId: user.userId,
25618
+ setupTriggerReason: resetKey ? "explicit_reset" : outdatedMatches.length > 0 ? "fresh_after_version_update" : "new_environment",
25619
+ setupOperationKey: resetKey
25267
25620
  };
25268
25621
  }
25269
25622
  /**
25270
- * List active (open) sessions for an environment each session is one agent conversation thread.
25623
+ * List indexed sessions using ownership filters and bounded pagination.
25624
+ */
25625
+ async listSessions(options) {
25626
+ const environmentId = options.environmentId?.trim();
25627
+ const sandboxId = options.sandboxId?.trim();
25628
+ const subjectId = options.subjectId?.trim();
25629
+ if (!environmentId && !sandboxId && !subjectId) {
25630
+ throw new Error(
25631
+ "listSessions() requires environmentId, sandboxId, or subjectId so history cannot be scanned accidentally."
25632
+ );
25633
+ }
25634
+ const status = options.status || "active";
25635
+ const allowedStatuses = /* @__PURE__ */ new Set([
25636
+ "active",
25637
+ "closed",
25638
+ "expired",
25639
+ "failed",
25640
+ "timeout",
25641
+ "all"
25642
+ ]);
25643
+ if (!allowedStatuses.has(status)) {
25644
+ throw new Error(`Unsupported session status: ${String(status)}`);
25645
+ }
25646
+ const limit = boundedSessionListInteger(
25647
+ options.limit,
25648
+ "limit",
25649
+ DEFAULT_CONVERSATION_SESSION_LIST_LIMIT,
25650
+ 1,
25651
+ MAX_CONVERSATION_SESSION_LIST_LIMIT
25652
+ );
25653
+ const offset = boundedSessionListInteger(
25654
+ options.offset,
25655
+ "offset",
25656
+ 0,
25657
+ 0,
25658
+ MAX_CONVERSATION_SESSION_LIST_OFFSET
25659
+ );
25660
+ const query = new URLSearchParams({
25661
+ limit: String(limit),
25662
+ offset: String(offset)
25663
+ });
25664
+ if (environmentId) query.set("environmentId", environmentId);
25665
+ if (sandboxId) query.set("sandboxId", sandboxId);
25666
+ if (subjectId) query.set("userId", subjectId);
25667
+ if (options.sessionScope?.trim()) {
25668
+ query.set("sessionScope", options.sessionScope.trim());
25669
+ }
25670
+ if (status !== "all") query.set("status", status);
25671
+ const res = await this.request(
25672
+ `/control/sessions?${query.toString()}`
25673
+ );
25674
+ const items = Array.isArray(res.items) ? res.items : [];
25675
+ return items.map((row) => this.normalizeConversationSession(row));
25676
+ }
25677
+ /**
25678
+ * List active (open) sessions for an environment.
25271
25679
  */
25272
25680
  async listOpenSessions(filters) {
25273
- return this.listSessionsForEnvironment(filters.environmentId, "active");
25681
+ return this.listSessions({ ...filters, status: "active" });
25274
25682
  }
25275
25683
  /**
25276
25684
  * List closed sessions for an environment (conversations that have disconnected).
25277
25685
  */
25278
25686
  async listClosedSessions(filters) {
25279
- return this.listSessionsForEnvironment(filters.environmentId, "closed");
25687
+ return this.listSessions({ ...filters, status: "closed" });
25280
25688
  }
25281
25689
  async getUserEnvironmentState(options) {
25282
25690
  const query = new URLSearchParams({
@@ -25311,14 +25719,6 @@ var Granular = class _Granular {
25311
25719
  });
25312
25720
  return result.readAtBySessionId || {};
25313
25721
  }
25314
- async listSessionsForEnvironment(environmentId, status) {
25315
- const query = new URLSearchParams({ environmentId, status });
25316
- const res = await this.request(
25317
- `/control/sessions?${query.toString()}`
25318
- );
25319
- const items = Array.isArray(res.items) ? res.items : [];
25320
- return items.map((row) => this.normalizeConversationSession(row));
25321
- }
25322
25722
  normalizeConversationSession(row) {
25323
25723
  const sessionId = String(row.sessionId ?? row.session_id ?? "");
25324
25724
  const environmentId = String(row.environmentId ?? row.environment_id ?? "");
@@ -25377,6 +25777,7 @@ var Granular = class _Granular {
25377
25777
  */
25378
25778
  async createSession(options) {
25379
25779
  const clientId = options.clientId || `client_${Date.now()}`;
25780
+ const sessionScope = options.sessionScope?.trim() || void 0;
25380
25781
  await this.activateEnvironment(options.environmentId);
25381
25782
  const envData = await this.environments.get(options.environmentId);
25382
25783
  const environment = this.bindEnvironmentHandle(envData);
@@ -25385,6 +25786,8 @@ var Granular = class _Granular {
25385
25786
  body: JSON.stringify({
25386
25787
  environmentId: options.environmentId,
25387
25788
  clientId,
25789
+ sessionScope,
25790
+ capabilities: sessionScope ? { sessionScope } : void 0,
25388
25791
  initialHeap: options.initialHeap
25389
25792
  })
25390
25793
  });
@@ -25492,11 +25895,24 @@ var Granular = class _Granular {
25492
25895
  {
25493
25896
  method: "POST",
25494
25897
  body: JSON.stringify({
25495
- triggerReason: resolved.setupTriggerReason
25898
+ triggerReason: resolved.setupTriggerReason,
25899
+ operationKey: resolved.setupOperationKey
25496
25900
  })
25497
25901
  }
25498
25902
  );
25499
25903
  const setupRunId = setupRun.setupRunId;
25904
+ let claim = null;
25905
+ for (let attempt = 0; attempt < 3; attempt += 1) {
25906
+ claim = await this.request(
25907
+ `/control/environment-setup-runs/${setupRunId}/importer-claim`,
25908
+ { method: "POST", body: JSON.stringify({}) }
25909
+ );
25910
+ if (claim.action !== "busy") break;
25911
+ await sleep(Math.min(3e4, Math.max(250, claim.retryAfterMs || 1e3)));
25912
+ }
25913
+ if (!claim) {
25914
+ throw new Error(`Unable to claim environment setup run ${setupRunId}.`);
25915
+ }
25500
25916
  const updateSetupRun = async (patch) => {
25501
25917
  await this.request(
25502
25918
  `/control/environment-setup-runs/${setupRunId}`,
@@ -25506,10 +25922,26 @@ var Granular = class _Granular {
25506
25922
  }
25507
25923
  );
25508
25924
  };
25925
+ if (claim.action === "submitted") {
25926
+ const completedSetupRun = await this.request(
25927
+ `/control/environment-setup-runs/${setupRunId}`,
25928
+ { method: "PATCH", body: JSON.stringify({ markHookCompleted: true }) }
25929
+ );
25930
+ const refreshedEnvironment = await this.environments.get(
25931
+ environment.environmentId
25932
+ );
25933
+ environment.syncEnvironmentData(refreshedEnvironment);
25934
+ return completedSetupRun;
25935
+ }
25936
+ if (claim.action === "busy" || claim.action === "terminal") {
25937
+ return claim.summary;
25938
+ }
25939
+ let importSequence = 0;
25509
25940
  const importerContext = {
25510
25941
  environmentId: environment.environmentId,
25511
25942
  sandboxId: environment.sandboxId,
25512
25943
  subjectId: environment.subjectId,
25944
+ externalUserId: resolved.externalUserId,
25513
25945
  reason: resolved.setupTriggerReason,
25514
25946
  incrementTotalObjectsToImportCount: async (n) => {
25515
25947
  const safeIncrement = Math.max(0, Math.trunc(n));
@@ -25526,7 +25958,10 @@ var Granular = class _Granular {
25526
25958
  importRecords: async (records, options) => environment.enqueueRecordImport(records, {
25527
25959
  batchSize: options?.batchSize,
25528
25960
  writeMode: options?.writeMode,
25529
- setupRunId
25961
+ setupRunId,
25962
+ // Sequence is deterministic for a retry of one importer hook. It
25963
+ // prevents a Container restart from creating a second queue import.
25964
+ operationKey: `${setupRunId}:import:${importSequence++}`
25530
25965
  })
25531
25966
  };
25532
25967
  try {
@@ -26706,15 +27141,22 @@ async function resolveEnvironmentData(granular, options) {
26706
27141
  const ontologyId = await resolveOntologyId(granular, options.ontology);
26707
27142
  const environmentName = options.environment ?? "dev";
26708
27143
  const environments = await granular.environments.list(ontologyId);
26709
- const existing = environments.find(
26710
- (environment) => matchesEnvironmentName(environment, environmentName)
27144
+ const matchingEnvironments = environments.filter(
27145
+ (environment) => matchesEnvironmentName(environment, environmentName) && (!options.subjectId || environment.subjectId === options.subjectId)
26711
27146
  );
27147
+ if (matchingEnvironments.length > 1) {
27148
+ throw new Error(
27149
+ `Environment slot \`${environmentName}\` matches ${matchingEnvironments.length} subject environments. Pass --environment-id or --subject-id so session history cannot resolve to an arbitrary user.`
27150
+ );
27151
+ }
27152
+ const existing = matchingEnvironments[0];
26712
27153
  if (existing) {
26713
27154
  return existing;
26714
27155
  }
26715
27156
  if (!options.createIfMissing) {
27157
+ const subjectSuffix = options.subjectId ? ` for subject \`${options.subjectId}\`` : "";
26716
27158
  throw new Error(
26717
- `No environment named \`${environmentName}\` found for ontology \`${ontologyId}\`. Run \`granular connect test\` or \`granular session create\` first, or pass \`--environment-id\`.`
27159
+ `No environment named \`${environmentName}\`${subjectSuffix} found for ontology \`${ontologyId}\`. Run \`granular connect test\` or \`granular session create\` first, or pass \`--environment-id\`.`
26718
27160
  );
26719
27161
  }
26720
27162
  const connection = await granular.connect({
@@ -26725,17 +27167,15 @@ async function resolveEnvironmentData(granular, options) {
26725
27167
  });
26726
27168
  return await granular.environments.get(connection.environmentId);
26727
27169
  }
26728
- async function listSessionsForEnvironment(granular, environmentId, status) {
26729
- if (status === "all") {
26730
- const [active, closed] = await Promise.all([
26731
- granular.listOpenSessions({ environmentId }),
26732
- granular.listClosedSessions({ environmentId })
26733
- ]);
26734
- return [...active, ...closed].sort((left, right) => {
26735
- return Date.parse(right.lastSeenAt) - Date.parse(left.lastSeenAt);
26736
- });
26737
- }
26738
- return status === "closed" ? granular.listClosedSessions({ environmentId }) : granular.listOpenSessions({ environmentId });
27170
+ async function listSessionsForEnvironment(granular, environmentId, options) {
27171
+ return granular.listSessions({
27172
+ environmentId,
27173
+ status: options.status,
27174
+ sessionScope: options.sessionScope,
27175
+ subjectId: options.subjectId,
27176
+ limit: options.limit,
27177
+ offset: options.offset
27178
+ });
26739
27179
  }
26740
27180
  async function connectRuntime(options) {
26741
27181
  if (options.sessionId) {
@@ -26848,6 +27288,19 @@ async function connectTestCommand(options) {
26848
27288
  }
26849
27289
 
26850
27290
  // src/cli/commands/session.ts
27291
+ function parseSessionListInteger(value, name) {
27292
+ const fallback2 = name === "limit" ? 25 : 0;
27293
+ const minimum = name === "limit" ? 1 : 0;
27294
+ const maximum = name === "limit" ? 500 : 1e5;
27295
+ if (value === void 0 || value === "") return fallback2;
27296
+ const parsed = typeof value === "number" ? value : Number.parseInt(value.trim(), 10);
27297
+ if (!Number.isInteger(parsed) || String(parsed) !== String(value).trim() || parsed < minimum || parsed > maximum) {
27298
+ throw new Error(
27299
+ `--${name} must be an integer between ${minimum} and ${maximum}.`
27300
+ );
27301
+ }
27302
+ return parsed;
27303
+ }
26851
27304
  function printValue(value, emitJson) {
26852
27305
  const json = JSON.stringify(value, null, 2);
26853
27306
  if (emitJson) {
@@ -26932,7 +27385,8 @@ async function sessionCreateCommand(options) {
26932
27385
  createIfMissing: true
26933
27386
  });
26934
27387
  const environment = await granular.createSession({
26935
- environmentId: envData.environmentId
27388
+ environmentId: envData.environmentId,
27389
+ sessionScope: options.sessionScope
26936
27390
  });
26937
27391
  try {
26938
27392
  const payload = {
@@ -26941,6 +27395,7 @@ async function sessionCreateCommand(options) {
26941
27395
  environmentId: environment.environmentId,
26942
27396
  environment: environment.tag || envData.tag?.name || envData.buildPolicy.tagName || requestedEnvironment,
26943
27397
  sessionId: environment.sessionId,
27398
+ sessionScope: options.sessionScope?.trim() || null,
26944
27399
  subjectId: environment.subjectId,
26945
27400
  versionId: environment.versionId
26946
27401
  };
@@ -26955,6 +27410,7 @@ async function sessionCreateCommand(options) {
26955
27410
  Environment: payload.environment,
26956
27411
  "Environment ID": payload.environmentId,
26957
27412
  "Session ID": payload.sessionId,
27413
+ "Session scope": payload.sessionScope || "unscoped",
26958
27414
  "Subject ID": payload.subjectId,
26959
27415
  "Version ID": payload.versionId
26960
27416
  });
@@ -26970,6 +27426,21 @@ async function sessionCreateCommand(options) {
26970
27426
  async function sessionListCommand(options) {
26971
27427
  const emitJson = options.json === true;
26972
27428
  const status = options.status ?? "active";
27429
+ const allowedStatuses = /* @__PURE__ */ new Set([
27430
+ "active",
27431
+ "closed",
27432
+ "expired",
27433
+ "failed",
27434
+ "timeout",
27435
+ "all"
27436
+ ]);
27437
+ if (!allowedStatuses.has(status)) {
27438
+ throw new Error(
27439
+ "--status must be one of active, closed, expired, failed, timeout, or all."
27440
+ );
27441
+ }
27442
+ const limit = parseSessionListInteger(options.limit, "limit");
27443
+ const offset = parseSessionListInteger(options.offset, "offset");
26973
27444
  const requestedEnvironment = options.environment ?? "dev";
26974
27445
  if (!emitJson) {
26975
27446
  printHeader();
@@ -26979,18 +27450,33 @@ async function sessionListCommand(options) {
26979
27450
  ontology: options.ontology,
26980
27451
  environment: requestedEnvironment,
26981
27452
  environmentId: options.environmentId,
27453
+ subjectId: options.subjectId,
26982
27454
  createIfMissing: false
26983
27455
  });
26984
27456
  const items = await listSessionsForEnvironment(
26985
27457
  granular,
26986
27458
  environmentData.environmentId,
26987
- status
27459
+ {
27460
+ status,
27461
+ sessionScope: options.sessionScope?.trim() || void 0,
27462
+ subjectId: options.subjectId?.trim() || void 0,
27463
+ limit,
27464
+ offset
27465
+ }
26988
27466
  );
26989
27467
  const payload = {
26990
27468
  ontologyId: environmentData.sandboxId,
26991
27469
  environmentId: environmentData.environmentId,
26992
27470
  environment: environmentData.tag?.name || environmentData.buildPolicy.tagName || requestedEnvironment,
26993
27471
  status,
27472
+ sessionScope: options.sessionScope?.trim() || null,
27473
+ page: {
27474
+ limit,
27475
+ offset,
27476
+ returned: items.length,
27477
+ mayHaveMore: items.length === limit,
27478
+ nextOffset: items.length === limit ? offset + items.length : null
27479
+ },
26994
27480
  items
26995
27481
  };
26996
27482
  if (emitJson) {
@@ -27013,6 +27499,9 @@ async function sessionListCommand(options) {
27013
27499
  item.lastSeenAt
27014
27500
  ])
27015
27501
  );
27502
+ info(
27503
+ `Showing ${items.length} session${items.length === 1 ? "" : "s"} from offset ${offset}.` + (items.length === limit ? ` Use --offset ${offset + items.length} for the next page.` : "")
27504
+ );
27016
27505
  console.log();
27017
27506
  }
27018
27507
 
@@ -27689,9 +28178,12 @@ program2.command("build").description(
27689
28178
  });
27690
28179
  program2.command("deploy").description(
27691
28180
  "Push the current revision to dev; use --prod to move prod there too"
27692
- ).option("--prod", "Also point prod to the current version").action(async (opts) => {
28181
+ ).option("--prod", "Also point prod to the current version").option(
28182
+ "--release-key <key>",
28183
+ "Version data/importer changes with this immutable release key"
28184
+ ).action(async (opts) => {
27693
28185
  try {
27694
- await deployCommand({ prod: opts.prod });
28186
+ await deployCommand({ prod: opts.prod, releaseKey: opts.releaseKey });
27695
28187
  } catch (err) {
27696
28188
  error(err.message);
27697
28189
  process.exit(1);
@@ -27919,6 +28411,9 @@ session.command("create").description(
27919
28411
  "--permissions <list>",
27920
28412
  "Comma-separated permission profile to ensure when a named environment needs to be created",
27921
28413
  "allow-all"
28414
+ ).option(
28415
+ "--session-scope <scope>",
28416
+ "Application-owned history scope to persist on the new session"
27922
28417
  ).option("--json", "Print machine-readable JSON").action(
27923
28418
  async (options) => {
27924
28419
  try {
@@ -27929,7 +28424,7 @@ session.command("create").description(
27929
28424
  }
27930
28425
  }
27931
28426
  );
27932
- session.command("list").description("List indexed sessions for an environment").option(
28427
+ session.command("list").description("List one bounded page of indexed sessions for an environment").option(
27933
28428
  "--ontology <ontologyId>",
27934
28429
  "Override the ontology id from .granularrc"
27935
28430
  ).option(
@@ -27939,11 +28434,17 @@ session.command("list").description("List indexed sessions for an environment").
27939
28434
  ).option(
27940
28435
  "--environment-id <environmentId>",
27941
28436
  "List sessions for this exact environment id"
28437
+ ).option(
28438
+ "--subject-id <subjectId>",
28439
+ "Resolve and constrain history to this internal Granular subject id"
28440
+ ).option(
28441
+ "--session-scope <scope>",
28442
+ "Return only sessions owned by this application history scope"
27942
28443
  ).option(
27943
28444
  "--status <status>",
27944
- "Session status filter: active|closed|all",
28445
+ "Session status: active|closed|expired|failed|timeout|all",
27945
28446
  "active"
27946
- ).option("--json", "Print machine-readable JSON").action(
28447
+ ).option("--limit <count>", "Rows to return (1-500)", "25").option("--offset <count>", "Rows to skip (0-100000)", "0").option("--json", "Print machine-readable JSON").action(
27947
28448
  async (options) => {
27948
28449
  try {
27949
28450
  await sessionListCommand(options);