@granular-software/sdk 0.4.50 → 0.4.51

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
  };
@@ -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\` |
@@ -19560,16 +19569,37 @@ var WSClient = class {
19560
19569
  tokenRefreshTimer = null;
19561
19570
  isExplicitlyDisconnected = false;
19562
19571
  reconnectAttempts = 0;
19572
+ connectPromise = null;
19573
+ connectionEpoch = 0;
19574
+ cancelConnectAttempt = null;
19563
19575
  options;
19564
19576
  constructor(options) {
19565
19577
  this.options = options;
19566
19578
  this.url = options.url;
19567
19579
  this.sessionId = options.sessionId;
19568
19580
  this.token = options.token;
19581
+ if (options.initialDocumentSnapshot) {
19582
+ this.seedDocumentSnapshot(options.initialDocumentSnapshot);
19583
+ }
19569
19584
  }
19570
19585
  get currentSessionId() {
19571
19586
  return this.sessionId;
19572
19587
  }
19588
+ seedDocumentSnapshot(document) {
19589
+ if (!document || typeof document !== "object" || Array.isArray(document)) {
19590
+ return;
19591
+ }
19592
+ try {
19593
+ this.doc = document instanceof Uint8Array ? Automerge__namespace.load(document) : Automerge__namespace.from(document);
19594
+ this.syncState = Automerge__namespace.initSyncState();
19595
+ this.emit("sync", this.doc);
19596
+ } catch (error2) {
19597
+ console.warn("[Granular] Failed to seed cached session document", error2);
19598
+ }
19599
+ }
19600
+ saveDocumentSnapshot() {
19601
+ return Automerge__namespace.save(this.doc);
19602
+ }
19573
19603
  clearTokenRefreshTimer() {
19574
19604
  if (this.tokenRefreshTimer) {
19575
19605
  clearTimeout(this.tokenRefreshTimer);
@@ -19679,8 +19709,23 @@ var WSClient = class {
19679
19709
  * Connect to the WebSocket server
19680
19710
  * @returns {Promise<void>} Resolves when connection is open
19681
19711
  */
19682
- async connect() {
19712
+ async connect(options = {}) {
19713
+ if (this.ws?.readyState === READY_STATE_OPEN) return;
19714
+ if (this.connectPromise) return this.connectPromise;
19715
+ const connectPromise = this.connectAttempt(options.signal);
19716
+ this.connectPromise = connectPromise;
19717
+ try {
19718
+ await connectPromise;
19719
+ } finally {
19720
+ if (this.connectPromise === connectPromise) {
19721
+ this.connectPromise = null;
19722
+ }
19723
+ }
19724
+ }
19725
+ async connectAttempt(signal) {
19726
+ if (signal?.aborted) throw new Error("WebSocket connect aborted");
19683
19727
  const token = await this.resolveTokenForConnect();
19728
+ if (signal?.aborted) throw new Error("WebSocket connect aborted");
19684
19729
  this.isExplicitlyDisconnected = false;
19685
19730
  this.scheduleTokenRefresh();
19686
19731
  if (this.reconnectTimer) {
@@ -19692,7 +19737,7 @@ var WSClient = class {
19692
19737
  try {
19693
19738
  const wsModule = await import('ws');
19694
19739
  WebSocketClass = wsModule.default || wsModule;
19695
- } catch (e) {
19740
+ } catch {
19696
19741
  }
19697
19742
  }
19698
19743
  if (!WebSocketClass) {
@@ -19700,83 +19745,97 @@ var WSClient = class {
19700
19745
  'No WebSocket implementation found. If using Node.js, please install "ws" and pass the constructor to the SDK options: { WebSocketCtor: WebSocket }.'
19701
19746
  );
19702
19747
  }
19748
+ const epoch = ++this.connectionEpoch;
19749
+ const wsUrl = new URL(this.url);
19750
+ wsUrl.searchParams.set("sessionId", this.sessionId);
19751
+ wsUrl.searchParams.set("token", token);
19752
+ const socket = new WebSocketClass(wsUrl.toString());
19753
+ this.ws = socket;
19703
19754
  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
- });
19755
+ let settled = false;
19756
+ const isCurrent = () => this.connectionEpoch === epoch && this.ws === socket;
19757
+ const finish = (error2) => {
19758
+ if (settled) return;
19759
+ settled = true;
19760
+ if (this.cancelConnectAttempt === handleAbort) {
19761
+ this.cancelConnectAttempt = null;
19762
+ }
19763
+ signal?.removeEventListener("abort", handleAbort);
19764
+ if (error2) {
19765
+ reject(error2 instanceof Error ? error2 : new Error(String(error2)));
19743
19766
  } 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
- };
19767
+ resolve2();
19777
19768
  }
19778
- } catch (error2) {
19779
- reject(error2);
19769
+ };
19770
+ const closeStaleSocket = () => {
19771
+ try {
19772
+ socket.close(1e3, "Stale connection attempt");
19773
+ } catch {
19774
+ }
19775
+ };
19776
+ const handleAbort = () => {
19777
+ if (isCurrent()) {
19778
+ this.connectionEpoch += 1;
19779
+ this.ws = null;
19780
+ }
19781
+ closeStaleSocket();
19782
+ finish(new Error("WebSocket connect aborted"));
19783
+ };
19784
+ this.cancelConnectAttempt = handleAbort;
19785
+ const handleOpen = () => {
19786
+ if (!isCurrent()) {
19787
+ closeStaleSocket();
19788
+ return;
19789
+ }
19790
+ this.reconnectAttempts = 0;
19791
+ this.emit("open", {});
19792
+ finish();
19793
+ };
19794
+ const handleMessage = (data) => {
19795
+ if (!isCurrent()) return;
19796
+ try {
19797
+ const text = typeof data === "string" ? data : data && typeof data === "object" && "toString" in data ? String(data.toString()) : "";
19798
+ this.handleMessage(JSON.parse(text));
19799
+ } catch (error2) {
19800
+ console.error("[Granular] Failed to parse message:", error2);
19801
+ }
19802
+ };
19803
+ const handleError = (error2) => {
19804
+ if (!isCurrent()) return;
19805
+ const typedError = error2 instanceof Error ? error2 : new Error("WebSocket error");
19806
+ this.emit("error", typedError);
19807
+ if (socket.readyState !== READY_STATE_OPEN) finish(typedError);
19808
+ };
19809
+ const handleClose = (close) => {
19810
+ if (!isCurrent()) return;
19811
+ if (!settled) {
19812
+ finish(
19813
+ new Error(
19814
+ `WebSocket closed before ready${close.code ? ` (code=${close.code})` : ""}`
19815
+ )
19816
+ );
19817
+ }
19818
+ this.handleDisconnect({
19819
+ code: close.code,
19820
+ reason: this.normalizeReason(close.reason),
19821
+ wasClean: close.wasClean
19822
+ });
19823
+ };
19824
+ signal?.addEventListener("abort", handleAbort, { once: true });
19825
+ const nodeSocket = socket;
19826
+ if (typeof nodeSocket.on === "function") {
19827
+ nodeSocket.on("open", handleOpen);
19828
+ nodeSocket.on("message", handleMessage);
19829
+ nodeSocket.on("error", handleError);
19830
+ nodeSocket.on(
19831
+ "close",
19832
+ (code, reason) => handleClose({ code, reason, wasClean: code === 1e3 })
19833
+ );
19834
+ } else {
19835
+ socket.onopen = handleOpen;
19836
+ socket.onmessage = (event) => handleMessage(event.data);
19837
+ socket.onerror = handleError;
19838
+ socket.onclose = (event) => handleClose(event);
19780
19839
  }
19781
19840
  });
19782
19841
  }
@@ -19794,9 +19853,58 @@ var WSClient = class {
19794
19853
  return void 0;
19795
19854
  }
19796
19855
  rejectPending(error2) {
19797
- this.messageQueue.forEach((pending) => pending.reject(error2));
19856
+ this.messageQueue.forEach((pending) => {
19857
+ clearTimeout(pending.timeout);
19858
+ pending.reject(error2);
19859
+ });
19798
19860
  this.messageQueue = [];
19799
19861
  }
19862
+ emitReconnectErrorMessage(error2) {
19863
+ const reconnectInfo = {
19864
+ error: error2,
19865
+ sessionId: this.sessionId,
19866
+ timestamp: Date.now()
19867
+ };
19868
+ this.emit("reconnect_error", reconnectInfo);
19869
+ if (this.options.onReconnectError) {
19870
+ try {
19871
+ this.options.onReconnectError(reconnectInfo);
19872
+ } catch (callbackError) {
19873
+ console.error(
19874
+ "[Granular] onReconnectError callback failed:",
19875
+ callbackError
19876
+ );
19877
+ }
19878
+ }
19879
+ }
19880
+ scheduleReconnectAttempt() {
19881
+ if (this.isExplicitlyDisconnected || this.reconnectTimer) return null;
19882
+ const baseReconnectDelayMs = typeof this.options.reconnectDelayMs === "number" && Number.isFinite(this.options.reconnectDelayMs) && this.options.reconnectDelayMs > 0 ? this.options.reconnectDelayMs : DEFAULT_RECONNECT_DELAY_MS;
19883
+ 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;
19884
+ if (this.reconnectAttempts >= maxReconnectAttempts) {
19885
+ this.emitReconnectErrorMessage(
19886
+ `WebSocket reconnect attempts exhausted after ${maxReconnectAttempts} attempt(s).`
19887
+ );
19888
+ return null;
19889
+ }
19890
+ this.reconnectAttempts += 1;
19891
+ const reconnectDelayMs = Math.min(
19892
+ 3e4,
19893
+ baseReconnectDelayMs * 2 ** Math.max(0, this.reconnectAttempts - 1)
19894
+ );
19895
+ this.reconnectTimer = setTimeout(() => {
19896
+ this.reconnectTimer = null;
19897
+ console.log("[Granular] Attempting reconnect...");
19898
+ this.connect().catch((error2) => {
19899
+ console.error("[Granular] Reconnect failed:", error2);
19900
+ this.emitReconnectErrorMessage(
19901
+ error2 instanceof Error ? error2.message : String(error2)
19902
+ );
19903
+ this.scheduleReconnectAttempt();
19904
+ });
19905
+ }, reconnectDelayMs);
19906
+ return reconnectDelayMs;
19907
+ }
19800
19908
  buildDisconnectError(info2) {
19801
19909
  const details = [
19802
19910
  info2.code !== void 0 ? `code=${info2.code}` : void 0,
@@ -19806,8 +19914,6 @@ var WSClient = class {
19806
19914
  return new Error(`WebSocket disconnected${suffix}`);
19807
19915
  }
19808
19916
  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
19917
  const unexpected = !this.isExplicitlyDisconnected;
19812
19918
  const info2 = {
19813
19919
  code: close.code,
@@ -19827,32 +19933,9 @@ var WSClient = class {
19827
19933
  const disconnectError = this.buildDisconnectError(info2);
19828
19934
  this.rejectPending(disconnectError);
19829
19935
  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;
19936
+ const reconnectDelayMs = this.scheduleReconnectAttempt();
19937
+ info2.reconnectScheduled = reconnectDelayMs !== null;
19938
+ if (reconnectDelayMs !== null) info2.reconnectDelayMs = reconnectDelayMs;
19856
19939
  if (this.options.onUnexpectedClose) {
19857
19940
  try {
19858
19941
  this.options.onUnexpectedClose(info2);
@@ -19863,28 +19946,6 @@ var WSClient = class {
19863
19946
  );
19864
19947
  }
19865
19948
  }
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
19949
  }
19889
19950
  }
19890
19951
  handleMessage(message) {
@@ -19995,6 +20056,7 @@ var WSClient = class {
19995
20056
  const response = message;
19996
20057
  const pending = this.messageQueue.find((q) => q.id === response.id);
19997
20058
  if (pending) {
20059
+ clearTimeout(pending.timeout);
19998
20060
  if (response.type === "rpc_error") {
19999
20061
  pending.reject(
20000
20062
  new Error(
@@ -20040,16 +20102,22 @@ var WSClient = class {
20040
20102
  id
20041
20103
  };
20042
20104
  return new Promise((resolve2, reject) => {
20043
- this.messageQueue.push({ resolve: resolve2, reject, id });
20044
- this.ws.send(JSON.stringify(request));
20045
20105
  const timeoutMs = rpcTimeoutMsForMethod(method);
20046
- setTimeout(() => {
20106
+ const timeout = setTimeout(() => {
20047
20107
  const pending = this.messageQueue.find((q) => q.id === id);
20048
20108
  if (pending) {
20049
20109
  this.messageQueue = this.messageQueue.filter((q) => q.id !== id);
20050
20110
  reject(new Error(`RPC timeout: ${method}`));
20051
20111
  }
20052
20112
  }, timeoutMs);
20113
+ this.messageQueue.push({ resolve: resolve2, reject, id, timeout });
20114
+ try {
20115
+ this.ws.send(JSON.stringify(request));
20116
+ } catch (error2) {
20117
+ clearTimeout(timeout);
20118
+ this.messageQueue = this.messageQueue.filter((q) => q.id !== id);
20119
+ reject(error2 instanceof Error ? error2 : new Error(String(error2)));
20120
+ }
20053
20121
  });
20054
20122
  }
20055
20123
  async handleIncomingRpc(request) {
@@ -20135,15 +20203,18 @@ var WSClient = class {
20135
20203
  /**
20136
20204
  * Disconnect the WebSocket and clear state
20137
20205
  */
20138
- disconnect() {
20206
+ disconnect(options = {}) {
20139
20207
  this.isExplicitlyDisconnected = true;
20208
+ this.cancelConnectAttempt?.();
20209
+ this.cancelConnectAttempt = null;
20210
+ this.connectionEpoch += 1;
20140
20211
  if (this.reconnectTimer) {
20141
20212
  clearTimeout(this.reconnectTimer);
20142
20213
  this.reconnectTimer = null;
20143
20214
  }
20144
20215
  this.clearTokenRefreshTimer();
20145
20216
  if (this.ws) {
20146
- this.ws.close(1e3, "Client disconnect");
20217
+ this.ws.close(1e3, options.reason || "Client disconnect");
20147
20218
  this.ws = null;
20148
20219
  }
20149
20220
  this.rejectPending(new Error("Client explicitly disconnected"));
@@ -20239,8 +20310,12 @@ function normalizePrompt(rawValue) {
20239
20310
  const source = promptRecord || raw;
20240
20311
  const id = typeof source.id === "string" ? source.id : typeof raw.id === "string" ? raw.id : typeof raw.promptId === "string" ? raw.promptId : "";
20241
20312
  if (!id) return null;
20313
+ const jobId = typeof source.jobId === "string" && source.jobId.trim() ? source.jobId.trim() : typeof raw.jobId === "string" && raw.jobId.trim() ? raw.jobId.trim() : void 0;
20314
+ 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
20315
  return {
20243
20316
  id,
20317
+ ...jobId ? { jobId } : {},
20318
+ ...turnId ? { turnId } : {},
20244
20319
  type: normalizePromptType(source === raw ? raw : { ...raw, ...source }),
20245
20320
  title: typeof source.title === "string" ? source.title : "Input required",
20246
20321
  message: typeof source.message === "string" ? source.message : "",
@@ -20327,6 +20402,9 @@ var Session = class {
20327
20402
  this.initialQuota = options.initialQuota || null;
20328
20403
  this.setupEventHandlers();
20329
20404
  this.setupToolInvokeHandler();
20405
+ this.currentDomainRevision = this.extractDomainRevisionFromDoc(
20406
+ this.client.doc
20407
+ );
20330
20408
  }
20331
20409
  extractDomainRevisionFromDoc(doc) {
20332
20410
  const domain = doc?.domain;
@@ -21232,6 +21310,7 @@ function normalizeJobAgentMessageEnvelope(data) {
21232
21310
  }
21233
21311
  return {
21234
21312
  jobId: d.jobId,
21313
+ ...typeof d.turnId === "string" && d.turnId.trim() ? { turnId: d.turnId.trim() } : {},
21235
21314
  message: {
21236
21315
  messageId: d.messageId,
21237
21316
  kind: d.kind === "artifacts" ? "artifacts" : "text",
@@ -21890,9 +21969,14 @@ function normalizeShowRefs(value) {
21890
21969
  variableNames: normalizeRefs(record.variableNames),
21891
21970
  fileIds: normalizeRefs(record.fileIds),
21892
21971
  sessionArtifactIds: normalizeRefs(record.sessionArtifactIds),
21893
- actionSuggestions: normalizeActionSuggestions(record.actionSuggestions)
21972
+ actionSuggestions: normalizeActionSuggestions(record.actionSuggestions),
21973
+ tables: Array.isArray(record.tables) ? record.tables.filter(
21974
+ (table2) => Boolean(
21975
+ table2 && typeof table2 === "object" && !Array.isArray(table2) && Array.isArray(table2.columns) && Array.isArray(table2.rows)
21976
+ )
21977
+ ) : void 0
21894
21978
  };
21895
- return show.entryPaths || show.listNames || show.variableNames || show.fileIds || show.sessionArtifactIds || show.actionSuggestions ? show : void 0;
21979
+ return show.entryPaths || show.listNames || show.variableNames || show.fileIds || show.sessionArtifactIds || show.actionSuggestions || show.tables ? show : void 0;
21896
21980
  }
21897
21981
  function normalizeActionSuggestions(value) {
21898
21982
  if (!Array.isArray(value)) return void 0;
@@ -21914,6 +21998,76 @@ function normalizeActionSuggestions(value) {
21914
21998
  }
21915
21999
  return suggestions.length ? suggestions : void 0;
21916
22000
  }
22001
+ var TRANSCRIPT_MESSAGE_PART_LIMIT = 128;
22002
+ var TRANSCRIPT_MESSAGE_PART_TEXT_LIMIT = 2e5;
22003
+ var TRANSCRIPT_MESSAGE_PART_ACTION_LABEL_LIMIT = 1e3;
22004
+ var TRANSCRIPT_MESSAGE_ACTION_LIMIT = 64;
22005
+ function normalizeConversationMessageActions(value) {
22006
+ if (!Array.isArray(value) || value.length === 0) return void 0;
22007
+ const actions = [];
22008
+ for (const item of value.slice(0, TRANSCRIPT_MESSAGE_ACTION_LIMIT)) {
22009
+ const record = asRecord3(item);
22010
+ const kind = record?.kind;
22011
+ const label = trimString(record?.label ?? record?.title);
22012
+ const status = record?.status;
22013
+ if (kind !== "frontend" && kind !== "backend" && kind !== "system" || !label || label.length > TRANSCRIPT_MESSAGE_PART_ACTION_LABEL_LIMIT) {
22014
+ continue;
22015
+ }
22016
+ actions.push({
22017
+ kind,
22018
+ label,
22019
+ ...status === "done" || status === "queued" || status === "failed" ? { status } : {}
22020
+ });
22021
+ }
22022
+ return actions.length ? actions : void 0;
22023
+ }
22024
+ function normalizeConversationMessageParts(value, canonicalContent, canonicalActions) {
22025
+ if (!Array.isArray(value) || value.length === 0 || value.length > TRANSCRIPT_MESSAGE_PART_LIMIT) {
22026
+ return void 0;
22027
+ }
22028
+ const parts = [];
22029
+ const canonicalActionsById = new Map(
22030
+ (canonicalActions || []).map((action) => [
22031
+ `${action.kind}:${action.label}`,
22032
+ action
22033
+ ])
22034
+ );
22035
+ const seenActionIds = /* @__PURE__ */ new Set();
22036
+ let textLength = 0;
22037
+ for (const item of value) {
22038
+ const record = asRecord3(item);
22039
+ if (!record) return void 0;
22040
+ if (record.type === "text") {
22041
+ if (typeof record.text !== "string" || record.text.length === 0) {
22042
+ return void 0;
22043
+ }
22044
+ textLength += record.text.length;
22045
+ if (textLength > TRANSCRIPT_MESSAGE_PART_TEXT_LIMIT) return void 0;
22046
+ parts.push({ type: "text", text: record.text });
22047
+ continue;
22048
+ }
22049
+ if (record.type !== "action") return void 0;
22050
+ const action = asRecord3(record.action);
22051
+ const kind = action?.kind;
22052
+ const label = trimString(action?.label);
22053
+ if (kind !== "frontend" && kind !== "backend" && kind !== "system" || !label || label.length > TRANSCRIPT_MESSAGE_PART_ACTION_LABEL_LIMIT) {
22054
+ return void 0;
22055
+ }
22056
+ const actionId = `${kind}:${label}`;
22057
+ const canonicalAction = canonicalActionsById.get(actionId);
22058
+ if (!canonicalAction) return void 0;
22059
+ if (seenActionIds.has(actionId)) continue;
22060
+ seenActionIds.add(actionId);
22061
+ parts.push({
22062
+ type: "action",
22063
+ action: canonicalAction
22064
+ });
22065
+ }
22066
+ const orderedText = parts.filter(
22067
+ (part) => part.type === "text"
22068
+ ).map((part) => part.text).join("");
22069
+ return orderedText === canonicalContent ? parts : void 0;
22070
+ }
21917
22071
  function stringifyTranscriptValue(value, fallback2 = "") {
21918
22072
  if (typeof value === "string") {
21919
22073
  return value.trim() || fallback2;
@@ -22072,10 +22226,12 @@ function normalizeConversationMessage(raw, artifactsById) {
22072
22226
  const content = trimString(
22073
22227
  record.content ?? record.reply ?? record.message ?? record.text
22074
22228
  );
22229
+ const actions = role === "assistant" ? normalizeConversationMessageActions(record.actions) : void 0;
22230
+ const parts = role === "assistant" ? normalizeConversationMessageParts(record.parts, content, actions) : void 0;
22075
22231
  const show = normalizeShowRefs(record.show);
22076
22232
  const id = asString(record.id) || crypto.randomUUID();
22077
22233
  const timestamp = asNumber(record.timestamp) || asNumber(record.ts) || 0;
22078
- if (!content && !show) return null;
22234
+ if (!content && !show && !actions?.length) return null;
22079
22235
  const artifactHistory = buildArtifactHistory(show, artifactsById);
22080
22236
  const historyContent = role === "assistant" ? content && artifactHistory ? `[Assistant reply]
22081
22237
  ${content}
@@ -22090,6 +22246,8 @@ ${content}` : artifactHistory : void 0;
22090
22246
  jobId: asString(record.jobId),
22091
22247
  promptId: asString(record.promptId),
22092
22248
  show,
22249
+ actions,
22250
+ parts,
22093
22251
  historyContent,
22094
22252
  source: "conversation"
22095
22253
  };
@@ -22738,7 +22896,12 @@ async function recordOpenAIUsageSpend(options) {
22738
22896
  const metadata = {
22739
22897
  ...options.metadata || {},
22740
22898
  ...options.usage.rawUsage !== void 0 ? { openaiUsage: options.usage.rawUsage } : {},
22741
- usageContext: context
22899
+ usageContext: context,
22900
+ pricingContextTier: options.usage.pricingContextTier,
22901
+ cacheWritePricePerMillionMicros: options.usage.cacheWritePricePerMillionMicros,
22902
+ cacheWriteTokens: options.usage.cacheWriteTokens,
22903
+ cacheWriteCostMicros: options.usage.cacheWriteCostMicros,
22904
+ longContextThresholdTokens: options.usage.longContextThresholdTokens
22742
22905
  };
22743
22906
  const response = await fetch(
22744
22907
  `${toGranularHttpBase(options.apiUrl)}/control/spend/events`,
@@ -22809,6 +22972,18 @@ function buildEffectMetamodelMutations(toolPath, spec) {
22809
22972
  }
22810
22973
 
22811
22974
  // src/client.ts
22975
+ var DEFAULT_CONVERSATION_SESSION_LIST_LIMIT = 100;
22976
+ var MAX_CONVERSATION_SESSION_LIST_LIMIT = 500;
22977
+ var MAX_CONVERSATION_SESSION_LIST_OFFSET = 1e5;
22978
+ function boundedSessionListInteger(value, name, fallback2, minimum, maximum) {
22979
+ if (value === void 0) return fallback2;
22980
+ if (!Number.isInteger(value) || value < minimum || value > maximum) {
22981
+ throw new RangeError(
22982
+ `Session list ${name} must be an integer between ${minimum} and ${maximum}.`
22983
+ );
22984
+ }
22985
+ return value;
22986
+ }
22812
22987
  var STANDARD_MODULES_OPERATIONS = [
22813
22988
  {
22814
22989
  create: "entity",
@@ -23147,7 +23322,7 @@ var Environment = class _Environment {
23147
23322
  }
23148
23323
  get sessions() {
23149
23324
  return {
23150
- list: async (options) => this.listSessions(options?.status || "active"),
23325
+ list: async (options = {}) => this.listSessions(options),
23151
23326
  create: async (options) => this.createSession(options),
23152
23327
  connect: async (sessionId, options) => this.connectSession(sessionId, options),
23153
23328
  reopen: async (sessionId, options) => this.reopenSession(sessionId, options),
@@ -23282,17 +23457,12 @@ var Environment = class _Environment {
23282
23457
  */
23283
23458
  async disconnect() {
23284
23459
  }
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 });
23460
+ async listSessions(optionsOrStatus = {}) {
23461
+ const options = typeof optionsOrStatus === "string" ? { status: optionsOrStatus } : optionsOrStatus;
23462
+ return this.granular.listSessions({
23463
+ ...options,
23464
+ environmentId: this.environmentId
23465
+ });
23296
23466
  }
23297
23467
  async getUserEnvironmentState(options = {}) {
23298
23468
  return this.granular.getUserEnvironmentState({
@@ -23310,6 +23480,7 @@ var Environment = class _Environment {
23310
23480
  return this.granular.createSession({
23311
23481
  environmentId: this.environmentId,
23312
23482
  clientId: options?.clientId,
23483
+ sessionScope: options?.sessionScope,
23313
23484
  initialHeap: options?.initialHeap
23314
23485
  });
23315
23486
  }
@@ -24872,7 +25043,7 @@ var EnvironmentSession = class extends Session {
24872
25043
  * Close only the socket transport without sending `client.goodbye`.
24873
25044
  */
24874
25045
  disconnectTransport() {
24875
- this.client.disconnect();
25046
+ this.client.disconnect({ reason: "Transport detach" });
24876
25047
  }
24877
25048
  /**
24878
25049
  * Backwards-compatible alias for `disconnect()`.
@@ -25267,16 +25438,71 @@ var Granular = class _Granular {
25267
25438
  };
25268
25439
  }
25269
25440
  /**
25270
- * List active (open) sessions for an environment each session is one agent conversation thread.
25441
+ * List indexed sessions using ownership filters and bounded pagination.
25442
+ */
25443
+ async listSessions(options) {
25444
+ const environmentId = options.environmentId?.trim();
25445
+ const sandboxId = options.sandboxId?.trim();
25446
+ const subjectId = options.subjectId?.trim();
25447
+ if (!environmentId && !sandboxId && !subjectId) {
25448
+ throw new Error(
25449
+ "listSessions() requires environmentId, sandboxId, or subjectId so history cannot be scanned accidentally."
25450
+ );
25451
+ }
25452
+ const status = options.status || "active";
25453
+ const allowedStatuses = /* @__PURE__ */ new Set([
25454
+ "active",
25455
+ "closed",
25456
+ "expired",
25457
+ "failed",
25458
+ "timeout",
25459
+ "all"
25460
+ ]);
25461
+ if (!allowedStatuses.has(status)) {
25462
+ throw new Error(`Unsupported session status: ${String(status)}`);
25463
+ }
25464
+ const limit = boundedSessionListInteger(
25465
+ options.limit,
25466
+ "limit",
25467
+ DEFAULT_CONVERSATION_SESSION_LIST_LIMIT,
25468
+ 1,
25469
+ MAX_CONVERSATION_SESSION_LIST_LIMIT
25470
+ );
25471
+ const offset = boundedSessionListInteger(
25472
+ options.offset,
25473
+ "offset",
25474
+ 0,
25475
+ 0,
25476
+ MAX_CONVERSATION_SESSION_LIST_OFFSET
25477
+ );
25478
+ const query = new URLSearchParams({
25479
+ limit: String(limit),
25480
+ offset: String(offset)
25481
+ });
25482
+ if (environmentId) query.set("environmentId", environmentId);
25483
+ if (sandboxId) query.set("sandboxId", sandboxId);
25484
+ if (subjectId) query.set("userId", subjectId);
25485
+ if (options.sessionScope?.trim()) {
25486
+ query.set("sessionScope", options.sessionScope.trim());
25487
+ }
25488
+ if (status !== "all") query.set("status", status);
25489
+ const res = await this.request(
25490
+ `/control/sessions?${query.toString()}`
25491
+ );
25492
+ const items = Array.isArray(res.items) ? res.items : [];
25493
+ return items.map((row) => this.normalizeConversationSession(row));
25494
+ }
25495
+ /**
25496
+ * List active (open) sessions for an environment.
25271
25497
  */
25272
25498
  async listOpenSessions(filters) {
25273
- return this.listSessionsForEnvironment(filters.environmentId, "active");
25499
+ return this.listSessions({ ...filters, status: "active" });
25274
25500
  }
25275
25501
  /**
25276
25502
  * List closed sessions for an environment (conversations that have disconnected).
25277
25503
  */
25278
25504
  async listClosedSessions(filters) {
25279
- return this.listSessionsForEnvironment(filters.environmentId, "closed");
25505
+ return this.listSessions({ ...filters, status: "closed" });
25280
25506
  }
25281
25507
  async getUserEnvironmentState(options) {
25282
25508
  const query = new URLSearchParams({
@@ -25311,14 +25537,6 @@ var Granular = class _Granular {
25311
25537
  });
25312
25538
  return result.readAtBySessionId || {};
25313
25539
  }
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
25540
  normalizeConversationSession(row) {
25323
25541
  const sessionId = String(row.sessionId ?? row.session_id ?? "");
25324
25542
  const environmentId = String(row.environmentId ?? row.environment_id ?? "");
@@ -25377,6 +25595,7 @@ var Granular = class _Granular {
25377
25595
  */
25378
25596
  async createSession(options) {
25379
25597
  const clientId = options.clientId || `client_${Date.now()}`;
25598
+ const sessionScope = options.sessionScope?.trim() || void 0;
25380
25599
  await this.activateEnvironment(options.environmentId);
25381
25600
  const envData = await this.environments.get(options.environmentId);
25382
25601
  const environment = this.bindEnvironmentHandle(envData);
@@ -25385,6 +25604,8 @@ var Granular = class _Granular {
25385
25604
  body: JSON.stringify({
25386
25605
  environmentId: options.environmentId,
25387
25606
  clientId,
25607
+ sessionScope,
25608
+ capabilities: sessionScope ? { sessionScope } : void 0,
25388
25609
  initialHeap: options.initialHeap
25389
25610
  })
25390
25611
  });
@@ -26706,15 +26927,22 @@ async function resolveEnvironmentData(granular, options) {
26706
26927
  const ontologyId = await resolveOntologyId(granular, options.ontology);
26707
26928
  const environmentName = options.environment ?? "dev";
26708
26929
  const environments = await granular.environments.list(ontologyId);
26709
- const existing = environments.find(
26710
- (environment) => matchesEnvironmentName(environment, environmentName)
26930
+ const matchingEnvironments = environments.filter(
26931
+ (environment) => matchesEnvironmentName(environment, environmentName) && (!options.subjectId || environment.subjectId === options.subjectId)
26711
26932
  );
26933
+ if (matchingEnvironments.length > 1) {
26934
+ throw new Error(
26935
+ `Environment slot \`${environmentName}\` matches ${matchingEnvironments.length} subject environments. Pass --environment-id or --subject-id so session history cannot resolve to an arbitrary user.`
26936
+ );
26937
+ }
26938
+ const existing = matchingEnvironments[0];
26712
26939
  if (existing) {
26713
26940
  return existing;
26714
26941
  }
26715
26942
  if (!options.createIfMissing) {
26943
+ const subjectSuffix = options.subjectId ? ` for subject \`${options.subjectId}\`` : "";
26716
26944
  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\`.`
26945
+ `No environment named \`${environmentName}\`${subjectSuffix} found for ontology \`${ontologyId}\`. Run \`granular connect test\` or \`granular session create\` first, or pass \`--environment-id\`.`
26718
26946
  );
26719
26947
  }
26720
26948
  const connection = await granular.connect({
@@ -26725,17 +26953,15 @@ async function resolveEnvironmentData(granular, options) {
26725
26953
  });
26726
26954
  return await granular.environments.get(connection.environmentId);
26727
26955
  }
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 });
26956
+ async function listSessionsForEnvironment(granular, environmentId, options) {
26957
+ return granular.listSessions({
26958
+ environmentId,
26959
+ status: options.status,
26960
+ sessionScope: options.sessionScope,
26961
+ subjectId: options.subjectId,
26962
+ limit: options.limit,
26963
+ offset: options.offset
26964
+ });
26739
26965
  }
26740
26966
  async function connectRuntime(options) {
26741
26967
  if (options.sessionId) {
@@ -26848,6 +27074,19 @@ async function connectTestCommand(options) {
26848
27074
  }
26849
27075
 
26850
27076
  // src/cli/commands/session.ts
27077
+ function parseSessionListInteger(value, name) {
27078
+ const fallback2 = name === "limit" ? 25 : 0;
27079
+ const minimum = name === "limit" ? 1 : 0;
27080
+ const maximum = name === "limit" ? 500 : 1e5;
27081
+ if (value === void 0 || value === "") return fallback2;
27082
+ const parsed = typeof value === "number" ? value : Number.parseInt(value.trim(), 10);
27083
+ if (!Number.isInteger(parsed) || String(parsed) !== String(value).trim() || parsed < minimum || parsed > maximum) {
27084
+ throw new Error(
27085
+ `--${name} must be an integer between ${minimum} and ${maximum}.`
27086
+ );
27087
+ }
27088
+ return parsed;
27089
+ }
26851
27090
  function printValue(value, emitJson) {
26852
27091
  const json = JSON.stringify(value, null, 2);
26853
27092
  if (emitJson) {
@@ -26932,7 +27171,8 @@ async function sessionCreateCommand(options) {
26932
27171
  createIfMissing: true
26933
27172
  });
26934
27173
  const environment = await granular.createSession({
26935
- environmentId: envData.environmentId
27174
+ environmentId: envData.environmentId,
27175
+ sessionScope: options.sessionScope
26936
27176
  });
26937
27177
  try {
26938
27178
  const payload = {
@@ -26941,6 +27181,7 @@ async function sessionCreateCommand(options) {
26941
27181
  environmentId: environment.environmentId,
26942
27182
  environment: environment.tag || envData.tag?.name || envData.buildPolicy.tagName || requestedEnvironment,
26943
27183
  sessionId: environment.sessionId,
27184
+ sessionScope: options.sessionScope?.trim() || null,
26944
27185
  subjectId: environment.subjectId,
26945
27186
  versionId: environment.versionId
26946
27187
  };
@@ -26955,6 +27196,7 @@ async function sessionCreateCommand(options) {
26955
27196
  Environment: payload.environment,
26956
27197
  "Environment ID": payload.environmentId,
26957
27198
  "Session ID": payload.sessionId,
27199
+ "Session scope": payload.sessionScope || "unscoped",
26958
27200
  "Subject ID": payload.subjectId,
26959
27201
  "Version ID": payload.versionId
26960
27202
  });
@@ -26970,6 +27212,21 @@ async function sessionCreateCommand(options) {
26970
27212
  async function sessionListCommand(options) {
26971
27213
  const emitJson = options.json === true;
26972
27214
  const status = options.status ?? "active";
27215
+ const allowedStatuses = /* @__PURE__ */ new Set([
27216
+ "active",
27217
+ "closed",
27218
+ "expired",
27219
+ "failed",
27220
+ "timeout",
27221
+ "all"
27222
+ ]);
27223
+ if (!allowedStatuses.has(status)) {
27224
+ throw new Error(
27225
+ "--status must be one of active, closed, expired, failed, timeout, or all."
27226
+ );
27227
+ }
27228
+ const limit = parseSessionListInteger(options.limit, "limit");
27229
+ const offset = parseSessionListInteger(options.offset, "offset");
26973
27230
  const requestedEnvironment = options.environment ?? "dev";
26974
27231
  if (!emitJson) {
26975
27232
  printHeader();
@@ -26979,18 +27236,33 @@ async function sessionListCommand(options) {
26979
27236
  ontology: options.ontology,
26980
27237
  environment: requestedEnvironment,
26981
27238
  environmentId: options.environmentId,
27239
+ subjectId: options.subjectId,
26982
27240
  createIfMissing: false
26983
27241
  });
26984
27242
  const items = await listSessionsForEnvironment(
26985
27243
  granular,
26986
27244
  environmentData.environmentId,
26987
- status
27245
+ {
27246
+ status,
27247
+ sessionScope: options.sessionScope?.trim() || void 0,
27248
+ subjectId: options.subjectId?.trim() || void 0,
27249
+ limit,
27250
+ offset
27251
+ }
26988
27252
  );
26989
27253
  const payload = {
26990
27254
  ontologyId: environmentData.sandboxId,
26991
27255
  environmentId: environmentData.environmentId,
26992
27256
  environment: environmentData.tag?.name || environmentData.buildPolicy.tagName || requestedEnvironment,
26993
27257
  status,
27258
+ sessionScope: options.sessionScope?.trim() || null,
27259
+ page: {
27260
+ limit,
27261
+ offset,
27262
+ returned: items.length,
27263
+ mayHaveMore: items.length === limit,
27264
+ nextOffset: items.length === limit ? offset + items.length : null
27265
+ },
26994
27266
  items
26995
27267
  };
26996
27268
  if (emitJson) {
@@ -27013,6 +27285,9 @@ async function sessionListCommand(options) {
27013
27285
  item.lastSeenAt
27014
27286
  ])
27015
27287
  );
27288
+ info(
27289
+ `Showing ${items.length} session${items.length === 1 ? "" : "s"} from offset ${offset}.` + (items.length === limit ? ` Use --offset ${offset + items.length} for the next page.` : "")
27290
+ );
27016
27291
  console.log();
27017
27292
  }
27018
27293
 
@@ -27919,6 +28194,9 @@ session.command("create").description(
27919
28194
  "--permissions <list>",
27920
28195
  "Comma-separated permission profile to ensure when a named environment needs to be created",
27921
28196
  "allow-all"
28197
+ ).option(
28198
+ "--session-scope <scope>",
28199
+ "Application-owned history scope to persist on the new session"
27922
28200
  ).option("--json", "Print machine-readable JSON").action(
27923
28201
  async (options) => {
27924
28202
  try {
@@ -27929,7 +28207,7 @@ session.command("create").description(
27929
28207
  }
27930
28208
  }
27931
28209
  );
27932
- session.command("list").description("List indexed sessions for an environment").option(
28210
+ session.command("list").description("List one bounded page of indexed sessions for an environment").option(
27933
28211
  "--ontology <ontologyId>",
27934
28212
  "Override the ontology id from .granularrc"
27935
28213
  ).option(
@@ -27939,11 +28217,17 @@ session.command("list").description("List indexed sessions for an environment").
27939
28217
  ).option(
27940
28218
  "--environment-id <environmentId>",
27941
28219
  "List sessions for this exact environment id"
28220
+ ).option(
28221
+ "--subject-id <subjectId>",
28222
+ "Resolve and constrain history to this internal Granular subject id"
28223
+ ).option(
28224
+ "--session-scope <scope>",
28225
+ "Return only sessions owned by this application history scope"
27942
28226
  ).option(
27943
28227
  "--status <status>",
27944
- "Session status filter: active|closed|all",
28228
+ "Session status: active|closed|expired|failed|timeout|all",
27945
28229
  "active"
27946
- ).option("--json", "Print machine-readable JSON").action(
28230
+ ).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
28231
  async (options) => {
27948
28232
  try {
27949
28233
  await sessionListCommand(options);