@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/index.mjs CHANGED
@@ -22,11 +22,11 @@ var __export = (target, all) => {
22
22
  for (var name in all)
23
23
  __defProp(target, name, { get: all[name], enumerable: true });
24
24
  };
25
- var __copyProps = (to, from, except, desc) => {
26
- if (from && typeof from === "object" || typeof from === "function") {
27
- for (let key of __getOwnPropNames(from))
25
+ var __copyProps = (to, from2, except, desc) => {
26
+ if (from2 && typeof from2 === "object" || typeof from2 === "function") {
27
+ for (let key of __getOwnPropNames(from2))
28
28
  if (!__hasOwnProp.call(to, key) && key !== except)
29
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
29
+ __defProp(to, key, { get: () => from2[key], enumerable: !(desc = __getOwnPropDesc(from2, key)) || desc.enumerable });
30
30
  }
31
31
  return to;
32
32
  };
@@ -4043,16 +4043,37 @@ var WSClient = class {
4043
4043
  tokenRefreshTimer = null;
4044
4044
  isExplicitlyDisconnected = false;
4045
4045
  reconnectAttempts = 0;
4046
+ connectPromise = null;
4047
+ connectionEpoch = 0;
4048
+ cancelConnectAttempt = null;
4046
4049
  options;
4047
4050
  constructor(options) {
4048
4051
  this.options = options;
4049
4052
  this.url = options.url;
4050
4053
  this.sessionId = options.sessionId;
4051
4054
  this.token = options.token;
4055
+ if (options.initialDocumentSnapshot) {
4056
+ this.seedDocumentSnapshot(options.initialDocumentSnapshot);
4057
+ }
4052
4058
  }
4053
4059
  get currentSessionId() {
4054
4060
  return this.sessionId;
4055
4061
  }
4062
+ seedDocumentSnapshot(document) {
4063
+ if (!document || typeof document !== "object" || Array.isArray(document)) {
4064
+ return;
4065
+ }
4066
+ try {
4067
+ this.doc = document instanceof Uint8Array ? Automerge.load(document) : Automerge.from(document);
4068
+ this.syncState = Automerge.initSyncState();
4069
+ this.emit("sync", this.doc);
4070
+ } catch (error) {
4071
+ console.warn("[Granular] Failed to seed cached session document", error);
4072
+ }
4073
+ }
4074
+ saveDocumentSnapshot() {
4075
+ return Automerge.save(this.doc);
4076
+ }
4056
4077
  clearTokenRefreshTimer() {
4057
4078
  if (this.tokenRefreshTimer) {
4058
4079
  clearTimeout(this.tokenRefreshTimer);
@@ -4162,8 +4183,23 @@ var WSClient = class {
4162
4183
  * Connect to the WebSocket server
4163
4184
  * @returns {Promise<void>} Resolves when connection is open
4164
4185
  */
4165
- async connect() {
4186
+ async connect(options = {}) {
4187
+ if (this.ws?.readyState === READY_STATE_OPEN) return;
4188
+ if (this.connectPromise) return this.connectPromise;
4189
+ const connectPromise = this.connectAttempt(options.signal);
4190
+ this.connectPromise = connectPromise;
4191
+ try {
4192
+ await connectPromise;
4193
+ } finally {
4194
+ if (this.connectPromise === connectPromise) {
4195
+ this.connectPromise = null;
4196
+ }
4197
+ }
4198
+ }
4199
+ async connectAttempt(signal) {
4200
+ if (signal?.aborted) throw new Error("WebSocket connect aborted");
4166
4201
  const token = await this.resolveTokenForConnect();
4202
+ if (signal?.aborted) throw new Error("WebSocket connect aborted");
4167
4203
  this.isExplicitlyDisconnected = false;
4168
4204
  this.scheduleTokenRefresh();
4169
4205
  if (this.reconnectTimer) {
@@ -4175,7 +4211,7 @@ var WSClient = class {
4175
4211
  try {
4176
4212
  const wsModule = await Promise.resolve().then(() => (init_wrapper(), wrapper_exports));
4177
4213
  WebSocketClass = wsModule.default || wsModule;
4178
- } catch (e) {
4214
+ } catch {
4179
4215
  }
4180
4216
  }
4181
4217
  if (!WebSocketClass) {
@@ -4183,83 +4219,97 @@ var WSClient = class {
4183
4219
  'No WebSocket implementation found. If using Node.js, please install "ws" and pass the constructor to the SDK options: { WebSocketCtor: WebSocket }.'
4184
4220
  );
4185
4221
  }
4222
+ const epoch = ++this.connectionEpoch;
4223
+ const wsUrl = new URL(this.url);
4224
+ wsUrl.searchParams.set("sessionId", this.sessionId);
4225
+ wsUrl.searchParams.set("token", token);
4226
+ const socket = new WebSocketClass(wsUrl.toString());
4227
+ this.ws = socket;
4186
4228
  return new Promise((resolve, reject) => {
4187
- try {
4188
- const wsUrl = new URL(this.url);
4189
- wsUrl.searchParams.set("sessionId", this.sessionId);
4190
- wsUrl.searchParams.set("token", token);
4191
- this.ws = new WebSocketClass(wsUrl.toString());
4192
- if (!this.ws) throw new Error("Failed to create WebSocket");
4193
- const socket = this.ws;
4194
- if (typeof socket.on === "function") {
4195
- socket.on("open", () => {
4196
- if (this.reconnectTimer) {
4197
- clearTimeout(this.reconnectTimer);
4198
- this.reconnectTimer = null;
4199
- }
4200
- this.reconnectAttempts = 0;
4201
- this.emit("open", {});
4202
- resolve();
4203
- });
4204
- socket.on("message", (data) => {
4205
- try {
4206
- const message = JSON.parse(data.toString());
4207
- this.handleMessage(message);
4208
- } catch (error) {
4209
- console.error("[Granular] Failed to parse message:", error);
4210
- }
4211
- });
4212
- socket.on("error", (error) => {
4213
- this.emit("error", error);
4214
- if (socket.readyState !== READY_STATE_OPEN) {
4215
- reject(error);
4216
- }
4217
- });
4218
- socket.on("close", (code, reason) => {
4219
- this.handleDisconnect({
4220
- code,
4221
- reason: this.normalizeReason(reason),
4222
- // ws does not provide wasClean on Node-style close callback
4223
- wasClean: code === 1e3
4224
- });
4225
- });
4229
+ let settled = false;
4230
+ const isCurrent = () => this.connectionEpoch === epoch && this.ws === socket;
4231
+ const finish = (error) => {
4232
+ if (settled) return;
4233
+ settled = true;
4234
+ if (this.cancelConnectAttempt === handleAbort) {
4235
+ this.cancelConnectAttempt = null;
4236
+ }
4237
+ signal?.removeEventListener("abort", handleAbort);
4238
+ if (error) {
4239
+ reject(error instanceof Error ? error : new Error(String(error)));
4226
4240
  } else {
4227
- this.ws.onopen = () => {
4228
- if (this.reconnectTimer) {
4229
- clearTimeout(this.reconnectTimer);
4230
- this.reconnectTimer = null;
4231
- }
4232
- this.reconnectAttempts = 0;
4233
- this.emit("open", {});
4234
- resolve();
4235
- };
4236
- this.ws.onmessage = (event) => {
4237
- try {
4238
- const data = event.data;
4239
- const message = JSON.parse(data.toString());
4240
- this.handleMessage(message);
4241
- } catch (error) {
4242
- console.error("[Granular] Failed to parse message:", error);
4243
- }
4244
- };
4245
- this.ws.onerror = (event) => {
4246
- const error = new Error("WebSocket error");
4247
- error.event = event;
4248
- this.emit("error", error);
4249
- if (this.ws?.readyState !== READY_STATE_OPEN) {
4250
- reject(error);
4251
- }
4252
- };
4253
- this.ws.onclose = (event) => {
4254
- this.handleDisconnect({
4255
- code: event.code,
4256
- reason: event.reason,
4257
- wasClean: event.wasClean
4258
- });
4259
- };
4241
+ resolve();
4260
4242
  }
4261
- } catch (error) {
4262
- reject(error);
4243
+ };
4244
+ const closeStaleSocket = () => {
4245
+ try {
4246
+ socket.close(1e3, "Stale connection attempt");
4247
+ } catch {
4248
+ }
4249
+ };
4250
+ const handleAbort = () => {
4251
+ if (isCurrent()) {
4252
+ this.connectionEpoch += 1;
4253
+ this.ws = null;
4254
+ }
4255
+ closeStaleSocket();
4256
+ finish(new Error("WebSocket connect aborted"));
4257
+ };
4258
+ this.cancelConnectAttempt = handleAbort;
4259
+ const handleOpen = () => {
4260
+ if (!isCurrent()) {
4261
+ closeStaleSocket();
4262
+ return;
4263
+ }
4264
+ this.reconnectAttempts = 0;
4265
+ this.emit("open", {});
4266
+ finish();
4267
+ };
4268
+ const handleMessage = (data) => {
4269
+ if (!isCurrent()) return;
4270
+ try {
4271
+ const text = typeof data === "string" ? data : data && typeof data === "object" && "toString" in data ? String(data.toString()) : "";
4272
+ this.handleMessage(JSON.parse(text));
4273
+ } catch (error) {
4274
+ console.error("[Granular] Failed to parse message:", error);
4275
+ }
4276
+ };
4277
+ const handleError = (error) => {
4278
+ if (!isCurrent()) return;
4279
+ const typedError = error instanceof Error ? error : new Error("WebSocket error");
4280
+ this.emit("error", typedError);
4281
+ if (socket.readyState !== READY_STATE_OPEN) finish(typedError);
4282
+ };
4283
+ const handleClose = (close) => {
4284
+ if (!isCurrent()) return;
4285
+ if (!settled) {
4286
+ finish(
4287
+ new Error(
4288
+ `WebSocket closed before ready${close.code ? ` (code=${close.code})` : ""}`
4289
+ )
4290
+ );
4291
+ }
4292
+ this.handleDisconnect({
4293
+ code: close.code,
4294
+ reason: this.normalizeReason(close.reason),
4295
+ wasClean: close.wasClean
4296
+ });
4297
+ };
4298
+ signal?.addEventListener("abort", handleAbort, { once: true });
4299
+ const nodeSocket = socket;
4300
+ if (typeof nodeSocket.on === "function") {
4301
+ nodeSocket.on("open", handleOpen);
4302
+ nodeSocket.on("message", handleMessage);
4303
+ nodeSocket.on("error", handleError);
4304
+ nodeSocket.on(
4305
+ "close",
4306
+ (code, reason) => handleClose({ code, reason, wasClean: code === 1e3 })
4307
+ );
4308
+ } else {
4309
+ socket.onopen = handleOpen;
4310
+ socket.onmessage = (event) => handleMessage(event.data);
4311
+ socket.onerror = handleError;
4312
+ socket.onclose = (event) => handleClose(event);
4263
4313
  }
4264
4314
  });
4265
4315
  }
@@ -4277,9 +4327,58 @@ var WSClient = class {
4277
4327
  return void 0;
4278
4328
  }
4279
4329
  rejectPending(error) {
4280
- this.messageQueue.forEach((pending) => pending.reject(error));
4330
+ this.messageQueue.forEach((pending) => {
4331
+ clearTimeout(pending.timeout);
4332
+ pending.reject(error);
4333
+ });
4281
4334
  this.messageQueue = [];
4282
4335
  }
4336
+ emitReconnectErrorMessage(error) {
4337
+ const reconnectInfo = {
4338
+ error,
4339
+ sessionId: this.sessionId,
4340
+ timestamp: Date.now()
4341
+ };
4342
+ this.emit("reconnect_error", reconnectInfo);
4343
+ if (this.options.onReconnectError) {
4344
+ try {
4345
+ this.options.onReconnectError(reconnectInfo);
4346
+ } catch (callbackError) {
4347
+ console.error(
4348
+ "[Granular] onReconnectError callback failed:",
4349
+ callbackError
4350
+ );
4351
+ }
4352
+ }
4353
+ }
4354
+ scheduleReconnectAttempt() {
4355
+ if (this.isExplicitlyDisconnected || this.reconnectTimer) return null;
4356
+ const baseReconnectDelayMs = typeof this.options.reconnectDelayMs === "number" && Number.isFinite(this.options.reconnectDelayMs) && this.options.reconnectDelayMs > 0 ? this.options.reconnectDelayMs : DEFAULT_RECONNECT_DELAY_MS;
4357
+ 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;
4358
+ if (this.reconnectAttempts >= maxReconnectAttempts) {
4359
+ this.emitReconnectErrorMessage(
4360
+ `WebSocket reconnect attempts exhausted after ${maxReconnectAttempts} attempt(s).`
4361
+ );
4362
+ return null;
4363
+ }
4364
+ this.reconnectAttempts += 1;
4365
+ const reconnectDelayMs = Math.min(
4366
+ 3e4,
4367
+ baseReconnectDelayMs * 2 ** Math.max(0, this.reconnectAttempts - 1)
4368
+ );
4369
+ this.reconnectTimer = setTimeout(() => {
4370
+ this.reconnectTimer = null;
4371
+ console.log("[Granular] Attempting reconnect...");
4372
+ this.connect().catch((error) => {
4373
+ console.error("[Granular] Reconnect failed:", error);
4374
+ this.emitReconnectErrorMessage(
4375
+ error instanceof Error ? error.message : String(error)
4376
+ );
4377
+ this.scheduleReconnectAttempt();
4378
+ });
4379
+ }, reconnectDelayMs);
4380
+ return reconnectDelayMs;
4381
+ }
4283
4382
  buildDisconnectError(info) {
4284
4383
  const details = [
4285
4384
  info.code !== void 0 ? `code=${info.code}` : void 0,
@@ -4289,8 +4388,6 @@ var WSClient = class {
4289
4388
  return new Error(`WebSocket disconnected${suffix}`);
4290
4389
  }
4291
4390
  handleDisconnect(close = {}) {
4292
- const baseReconnectDelayMs = typeof this.options.reconnectDelayMs === "number" && Number.isFinite(this.options.reconnectDelayMs) && this.options.reconnectDelayMs > 0 ? this.options.reconnectDelayMs : DEFAULT_RECONNECT_DELAY_MS;
4293
- 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;
4294
4391
  const unexpected = !this.isExplicitlyDisconnected;
4295
4392
  const info = {
4296
4393
  code: close.code,
@@ -4310,32 +4407,9 @@ var WSClient = class {
4310
4407
  const disconnectError = this.buildDisconnectError(info);
4311
4408
  this.rejectPending(disconnectError);
4312
4409
  this.emit("disconnect", info);
4313
- if (this.reconnectAttempts >= maxReconnectAttempts) {
4314
- const reconnectInfo = {
4315
- error: `WebSocket reconnect attempts exhausted after ${maxReconnectAttempts} attempt(s).`,
4316
- sessionId: this.sessionId,
4317
- timestamp: Date.now()
4318
- };
4319
- this.emit("reconnect_error", reconnectInfo);
4320
- if (this.options.onReconnectError) {
4321
- try {
4322
- this.options.onReconnectError(reconnectInfo);
4323
- } catch (callbackError) {
4324
- console.error(
4325
- "[Granular] onReconnectError callback failed:",
4326
- callbackError
4327
- );
4328
- }
4329
- }
4330
- return;
4331
- }
4332
- this.reconnectAttempts += 1;
4333
- const reconnectDelayMs = Math.min(
4334
- 3e4,
4335
- baseReconnectDelayMs * 2 ** Math.max(0, this.reconnectAttempts - 1)
4336
- );
4337
- info.reconnectScheduled = true;
4338
- info.reconnectDelayMs = reconnectDelayMs;
4410
+ const reconnectDelayMs = this.scheduleReconnectAttempt();
4411
+ info.reconnectScheduled = reconnectDelayMs !== null;
4412
+ if (reconnectDelayMs !== null) info.reconnectDelayMs = reconnectDelayMs;
4339
4413
  if (this.options.onUnexpectedClose) {
4340
4414
  try {
4341
4415
  this.options.onUnexpectedClose(info);
@@ -4346,28 +4420,6 @@ var WSClient = class {
4346
4420
  );
4347
4421
  }
4348
4422
  }
4349
- this.reconnectTimer = setTimeout(() => {
4350
- console.log("[Granular] Attempting reconnect...");
4351
- this.connect().catch((error) => {
4352
- console.error("[Granular] Reconnect failed:", error);
4353
- const reconnectInfo = {
4354
- error: error instanceof Error ? error.message : String(error),
4355
- sessionId: this.sessionId,
4356
- timestamp: Date.now()
4357
- };
4358
- this.emit("reconnect_error", reconnectInfo);
4359
- if (this.options.onReconnectError) {
4360
- try {
4361
- this.options.onReconnectError(reconnectInfo);
4362
- } catch (callbackError) {
4363
- console.error(
4364
- "[Granular] onReconnectError callback failed:",
4365
- callbackError
4366
- );
4367
- }
4368
- }
4369
- });
4370
- }, reconnectDelayMs);
4371
4423
  }
4372
4424
  }
4373
4425
  handleMessage(message) {
@@ -4478,6 +4530,7 @@ var WSClient = class {
4478
4530
  const response = message;
4479
4531
  const pending = this.messageQueue.find((q) => q.id === response.id);
4480
4532
  if (pending) {
4533
+ clearTimeout(pending.timeout);
4481
4534
  if (response.type === "rpc_error") {
4482
4535
  pending.reject(
4483
4536
  new Error(
@@ -4523,16 +4576,22 @@ var WSClient = class {
4523
4576
  id
4524
4577
  };
4525
4578
  return new Promise((resolve, reject) => {
4526
- this.messageQueue.push({ resolve, reject, id });
4527
- this.ws.send(JSON.stringify(request));
4528
4579
  const timeoutMs = rpcTimeoutMsForMethod(method);
4529
- setTimeout(() => {
4580
+ const timeout = setTimeout(() => {
4530
4581
  const pending = this.messageQueue.find((q) => q.id === id);
4531
4582
  if (pending) {
4532
4583
  this.messageQueue = this.messageQueue.filter((q) => q.id !== id);
4533
4584
  reject(new Error(`RPC timeout: ${method}`));
4534
4585
  }
4535
4586
  }, timeoutMs);
4587
+ this.messageQueue.push({ resolve, reject, id, timeout });
4588
+ try {
4589
+ this.ws.send(JSON.stringify(request));
4590
+ } catch (error) {
4591
+ clearTimeout(timeout);
4592
+ this.messageQueue = this.messageQueue.filter((q) => q.id !== id);
4593
+ reject(error instanceof Error ? error : new Error(String(error)));
4594
+ }
4536
4595
  });
4537
4596
  }
4538
4597
  async handleIncomingRpc(request) {
@@ -4618,15 +4677,18 @@ var WSClient = class {
4618
4677
  /**
4619
4678
  * Disconnect the WebSocket and clear state
4620
4679
  */
4621
- disconnect() {
4680
+ disconnect(options = {}) {
4622
4681
  this.isExplicitlyDisconnected = true;
4682
+ this.cancelConnectAttempt?.();
4683
+ this.cancelConnectAttempt = null;
4684
+ this.connectionEpoch += 1;
4623
4685
  if (this.reconnectTimer) {
4624
4686
  clearTimeout(this.reconnectTimer);
4625
4687
  this.reconnectTimer = null;
4626
4688
  }
4627
4689
  this.clearTokenRefreshTimer();
4628
4690
  if (this.ws) {
4629
- this.ws.close(1e3, "Client disconnect");
4691
+ this.ws.close(1e3, options.reason || "Client disconnect");
4630
4692
  this.ws = null;
4631
4693
  }
4632
4694
  this.rejectPending(new Error("Client explicitly disconnected"));
@@ -4722,8 +4784,12 @@ function normalizePrompt(rawValue) {
4722
4784
  const source = promptRecord || raw;
4723
4785
  const id = typeof source.id === "string" ? source.id : typeof raw.id === "string" ? raw.id : typeof raw.promptId === "string" ? raw.promptId : "";
4724
4786
  if (!id) return null;
4787
+ const jobId = typeof source.jobId === "string" && source.jobId.trim() ? source.jobId.trim() : typeof raw.jobId === "string" && raw.jobId.trim() ? raw.jobId.trim() : void 0;
4788
+ const turnId = typeof source.turnId === "string" && source.turnId.trim() ? source.turnId.trim() : typeof raw.turnId === "string" && raw.turnId.trim() ? raw.turnId.trim() : void 0;
4725
4789
  return {
4726
4790
  id,
4791
+ ...jobId ? { jobId } : {},
4792
+ ...turnId ? { turnId } : {},
4727
4793
  type: normalizePromptType(source === raw ? raw : { ...raw, ...source }),
4728
4794
  title: typeof source.title === "string" ? source.title : "Input required",
4729
4795
  message: typeof source.message === "string" ? source.message : "",
@@ -4810,6 +4876,9 @@ var Session = class {
4810
4876
  this.initialQuota = options.initialQuota || null;
4811
4877
  this.setupEventHandlers();
4812
4878
  this.setupToolInvokeHandler();
4879
+ this.currentDomainRevision = this.extractDomainRevisionFromDoc(
4880
+ this.client.doc
4881
+ );
4813
4882
  }
4814
4883
  extractDomainRevisionFromDoc(doc) {
4815
4884
  const domain = doc?.domain;
@@ -5715,6 +5784,7 @@ function normalizeJobAgentMessageEnvelope(data) {
5715
5784
  }
5716
5785
  return {
5717
5786
  jobId: d.jobId,
5787
+ ...typeof d.turnId === "string" && d.turnId.trim() ? { turnId: d.turnId.trim() } : {},
5718
5788
  message: {
5719
5789
  messageId: d.messageId,
5720
5790
  kind: d.kind === "artifacts" ? "artifacts" : "text",
@@ -6373,9 +6443,14 @@ function normalizeShowRefs(value) {
6373
6443
  variableNames: normalizeRefs(record.variableNames),
6374
6444
  fileIds: normalizeRefs(record.fileIds),
6375
6445
  sessionArtifactIds: normalizeRefs(record.sessionArtifactIds),
6376
- actionSuggestions: normalizeActionSuggestions(record.actionSuggestions)
6446
+ actionSuggestions: normalizeActionSuggestions(record.actionSuggestions),
6447
+ tables: Array.isArray(record.tables) ? record.tables.filter(
6448
+ (table) => Boolean(
6449
+ table && typeof table === "object" && !Array.isArray(table) && Array.isArray(table.columns) && Array.isArray(table.rows)
6450
+ )
6451
+ ) : void 0
6377
6452
  };
6378
- return show.entryPaths || show.listNames || show.variableNames || show.fileIds || show.sessionArtifactIds || show.actionSuggestions ? show : void 0;
6453
+ return show.entryPaths || show.listNames || show.variableNames || show.fileIds || show.sessionArtifactIds || show.actionSuggestions || show.tables ? show : void 0;
6379
6454
  }
6380
6455
  function normalizeActionSuggestions(value) {
6381
6456
  if (!Array.isArray(value)) return void 0;
@@ -6397,6 +6472,76 @@ function normalizeActionSuggestions(value) {
6397
6472
  }
6398
6473
  return suggestions.length ? suggestions : void 0;
6399
6474
  }
6475
+ var TRANSCRIPT_MESSAGE_PART_LIMIT = 128;
6476
+ var TRANSCRIPT_MESSAGE_PART_TEXT_LIMIT = 2e5;
6477
+ var TRANSCRIPT_MESSAGE_PART_ACTION_LABEL_LIMIT = 1e3;
6478
+ var TRANSCRIPT_MESSAGE_ACTION_LIMIT = 64;
6479
+ function normalizeConversationMessageActions(value) {
6480
+ if (!Array.isArray(value) || value.length === 0) return void 0;
6481
+ const actions = [];
6482
+ for (const item of value.slice(0, TRANSCRIPT_MESSAGE_ACTION_LIMIT)) {
6483
+ const record = asRecord3(item);
6484
+ const kind = record?.kind;
6485
+ const label = trimString(record?.label ?? record?.title);
6486
+ const status = record?.status;
6487
+ if (kind !== "frontend" && kind !== "backend" && kind !== "system" || !label || label.length > TRANSCRIPT_MESSAGE_PART_ACTION_LABEL_LIMIT) {
6488
+ continue;
6489
+ }
6490
+ actions.push({
6491
+ kind,
6492
+ label,
6493
+ ...status === "done" || status === "queued" || status === "failed" ? { status } : {}
6494
+ });
6495
+ }
6496
+ return actions.length ? actions : void 0;
6497
+ }
6498
+ function normalizeConversationMessageParts(value, canonicalContent, canonicalActions) {
6499
+ if (!Array.isArray(value) || value.length === 0 || value.length > TRANSCRIPT_MESSAGE_PART_LIMIT) {
6500
+ return void 0;
6501
+ }
6502
+ const parts = [];
6503
+ const canonicalActionsById = new Map(
6504
+ (canonicalActions || []).map((action) => [
6505
+ `${action.kind}:${action.label}`,
6506
+ action
6507
+ ])
6508
+ );
6509
+ const seenActionIds = /* @__PURE__ */ new Set();
6510
+ let textLength = 0;
6511
+ for (const item of value) {
6512
+ const record = asRecord3(item);
6513
+ if (!record) return void 0;
6514
+ if (record.type === "text") {
6515
+ if (typeof record.text !== "string" || record.text.length === 0) {
6516
+ return void 0;
6517
+ }
6518
+ textLength += record.text.length;
6519
+ if (textLength > TRANSCRIPT_MESSAGE_PART_TEXT_LIMIT) return void 0;
6520
+ parts.push({ type: "text", text: record.text });
6521
+ continue;
6522
+ }
6523
+ if (record.type !== "action") return void 0;
6524
+ const action = asRecord3(record.action);
6525
+ const kind = action?.kind;
6526
+ const label = trimString(action?.label);
6527
+ if (kind !== "frontend" && kind !== "backend" && kind !== "system" || !label || label.length > TRANSCRIPT_MESSAGE_PART_ACTION_LABEL_LIMIT) {
6528
+ return void 0;
6529
+ }
6530
+ const actionId = `${kind}:${label}`;
6531
+ const canonicalAction = canonicalActionsById.get(actionId);
6532
+ if (!canonicalAction) return void 0;
6533
+ if (seenActionIds.has(actionId)) continue;
6534
+ seenActionIds.add(actionId);
6535
+ parts.push({
6536
+ type: "action",
6537
+ action: canonicalAction
6538
+ });
6539
+ }
6540
+ const orderedText = parts.filter(
6541
+ (part) => part.type === "text"
6542
+ ).map((part) => part.text).join("");
6543
+ return orderedText === canonicalContent ? parts : void 0;
6544
+ }
6400
6545
  function stringifyTranscriptValue(value, fallback = "") {
6401
6546
  if (typeof value === "string") {
6402
6547
  return value.trim() || fallback;
@@ -6555,10 +6700,12 @@ function normalizeConversationMessage(raw, artifactsById) {
6555
6700
  const content = trimString(
6556
6701
  record.content ?? record.reply ?? record.message ?? record.text
6557
6702
  );
6703
+ const actions = role === "assistant" ? normalizeConversationMessageActions(record.actions) : void 0;
6704
+ const parts = role === "assistant" ? normalizeConversationMessageParts(record.parts, content, actions) : void 0;
6558
6705
  const show = normalizeShowRefs(record.show);
6559
6706
  const id = asString(record.id) || crypto.randomUUID();
6560
6707
  const timestamp = asNumber(record.timestamp) || asNumber(record.ts) || 0;
6561
- if (!content && !show) return null;
6708
+ if (!content && !show && !actions?.length) return null;
6562
6709
  const artifactHistory = buildArtifactHistory(show, artifactsById);
6563
6710
  const historyContent = role === "assistant" ? content && artifactHistory ? `[Assistant reply]
6564
6711
  ${content}
@@ -6573,6 +6720,8 @@ ${content}` : artifactHistory : void 0;
6573
6720
  jobId: asString(record.jobId),
6574
6721
  promptId: asString(record.promptId),
6575
6722
  show,
6723
+ actions,
6724
+ parts,
6576
6725
  historyContent,
6577
6726
  source: "conversation"
6578
6727
  };
@@ -12150,7 +12299,12 @@ async function recordOpenAIUsageSpend(options) {
12150
12299
  const metadata = {
12151
12300
  ...options.metadata || {},
12152
12301
  ...options.usage.rawUsage !== void 0 ? { openaiUsage: options.usage.rawUsage } : {},
12153
- usageContext: context
12302
+ usageContext: context,
12303
+ pricingContextTier: options.usage.pricingContextTier,
12304
+ cacheWritePricePerMillionMicros: options.usage.cacheWritePricePerMillionMicros,
12305
+ cacheWriteTokens: options.usage.cacheWriteTokens,
12306
+ cacheWriteCostMicros: options.usage.cacheWriteCostMicros,
12307
+ longContextThresholdTokens: options.usage.longContextThresholdTokens
12154
12308
  };
12155
12309
  const response = await fetch(
12156
12310
  `${toGranularHttpBase(options.apiUrl)}/control/spend/events`,
@@ -13078,11 +13232,20 @@ function buildStateMachineModelMutations(modelPath, machines) {
13078
13232
  return mutations;
13079
13233
  }
13080
13234
  function buildMachineTypes(classSummary, machine) {
13235
+ const stateGlossary = machine.states.map((state) => {
13236
+ const label = state.label && state.label !== state.name ? state.label : null;
13237
+ const meaning = [label, state.description].filter(Boolean).join(" \u2014 ");
13238
+ const finalMarker = state.isFinal ? " Final state." : "";
13239
+ return `${state.name}${meaning ? `: ${meaning}` : "."}${finalMarker}`;
13240
+ });
13081
13241
  return [
13082
13242
  {
13083
13243
  kind: "union",
13084
13244
  name: stateTypeName(classSummary.name, machine.name),
13085
- docs: [`Allowed states for ${classSummary.name}.${machine.name}.`],
13245
+ docs: [
13246
+ `Allowed states for ${classSummary.name}.${machine.name}.`,
13247
+ ...stateGlossary
13248
+ ],
13086
13249
  members: machine.states.map((state) => state.name)
13087
13250
  },
13088
13251
  {
@@ -13430,7 +13593,7 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
13430
13593
  },
13431
13594
  add_transition: async (value, {
13432
13595
  name,
13433
- from,
13596
+ from: from2,
13434
13597
  to,
13435
13598
  label,
13436
13599
  description,
@@ -13445,7 +13608,7 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
13445
13608
  value.target.add_state_machine_transition(
13446
13609
  value.name,
13447
13610
  name,
13448
- from,
13611
+ from2,
13449
13612
  to,
13450
13613
  {
13451
13614
  label,
@@ -13895,6 +14058,18 @@ function buildEffectMetamodelMutations(toolPath, spec) {
13895
14058
  }
13896
14059
 
13897
14060
  // src/client.ts
14061
+ var DEFAULT_CONVERSATION_SESSION_LIST_LIMIT = 100;
14062
+ var MAX_CONVERSATION_SESSION_LIST_LIMIT = 500;
14063
+ var MAX_CONVERSATION_SESSION_LIST_OFFSET = 1e5;
14064
+ function boundedSessionListInteger(value, name, fallback, minimum, maximum) {
14065
+ if (value === void 0) return fallback;
14066
+ if (!Number.isInteger(value) || value < minimum || value > maximum) {
14067
+ throw new RangeError(
14068
+ `Session list ${name} must be an integer between ${minimum} and ${maximum}.`
14069
+ );
14070
+ }
14071
+ return value;
14072
+ }
13898
14073
  var STANDARD_MODULES_OPERATIONS = [
13899
14074
  {
13900
14075
  create: "entity",
@@ -14112,7 +14287,8 @@ function normalizeEnvironmentSetupSummary(setup) {
14112
14287
  environmentId: String(setup.environmentId || ""),
14113
14288
  sandboxId: String(setup.sandboxId || ""),
14114
14289
  subjectId: String(setup.subjectId || ""),
14115
- triggerReason: setup.triggerReason === "fresh_after_version_update" ? "fresh_after_version_update" : "new_environment",
14290
+ triggerReason: setup.triggerReason === "fresh_after_version_update" ? "fresh_after_version_update" : setup.triggerReason === "explicit_reset" ? "explicit_reset" : "new_environment",
14291
+ operationKey: typeof setup.operationKey === "string" ? setup.operationKey : null,
14116
14292
  lifecycleStatus: setup.lifecycleStatus === "completed" || setup.lifecycleStatus === "failed" ? setup.lifecycleStatus : "running",
14117
14293
  stage: typeof setup.stage === "string" ? setup.stage : null,
14118
14294
  totalObjectsToImport: Number(setup.totalObjectsToImport || 0),
@@ -14233,7 +14409,7 @@ var Environment = class _Environment {
14233
14409
  }
14234
14410
  get sessions() {
14235
14411
  return {
14236
- list: async (options) => this.listSessions(options?.status || "active"),
14412
+ list: async (options = {}) => this.listSessions(options),
14237
14413
  create: async (options) => this.createSession(options),
14238
14414
  connect: async (sessionId, options) => this.connectSession(sessionId, options),
14239
14415
  reopen: async (sessionId, options) => this.reopenSession(sessionId, options),
@@ -14368,17 +14544,12 @@ var Environment = class _Environment {
14368
14544
  */
14369
14545
  async disconnect() {
14370
14546
  }
14371
- async listSessions(status = "active") {
14372
- if (status === "all") {
14373
- const [active, closed] = await Promise.all([
14374
- this.granular.listOpenSessions({ environmentId: this.environmentId }),
14375
- this.granular.listClosedSessions({ environmentId: this.environmentId })
14376
- ]);
14377
- return [...active, ...closed].sort(
14378
- (left, right) => Date.parse(right.lastSeenAt) - Date.parse(left.lastSeenAt)
14379
- );
14380
- }
14381
- return status === "closed" ? this.granular.listClosedSessions({ environmentId: this.environmentId }) : this.granular.listOpenSessions({ environmentId: this.environmentId });
14547
+ async listSessions(optionsOrStatus = {}) {
14548
+ const options = typeof optionsOrStatus === "string" ? { status: optionsOrStatus } : optionsOrStatus;
14549
+ return this.granular.listSessions({
14550
+ ...options,
14551
+ environmentId: this.environmentId
14552
+ });
14382
14553
  }
14383
14554
  async getUserEnvironmentState(options = {}) {
14384
14555
  return this.granular.getUserEnvironmentState({
@@ -14396,6 +14567,7 @@ var Environment = class _Environment {
14396
14567
  return this.granular.createSession({
14397
14568
  environmentId: this.environmentId,
14398
14569
  clientId: options?.clientId,
14570
+ sessionScope: options?.sessionScope,
14399
14571
  initialHeap: options?.initialHeap
14400
14572
  });
14401
14573
  }
@@ -15398,6 +15570,7 @@ var Environment = class _Environment {
15398
15570
  records: recordsToImport,
15399
15571
  batchSize: options.batchSize,
15400
15572
  setupRunId: options.setupRunId,
15573
+ operationKey: options.operationKey,
15401
15574
  writeMode: options.writeMode
15402
15575
  })
15403
15576
  }
@@ -15958,7 +16131,7 @@ var EnvironmentSession = class extends Session {
15958
16131
  * Close only the socket transport without sending `client.goodbye`.
15959
16132
  */
15960
16133
  disconnectTransport() {
15961
- this.client.disconnect();
16134
+ this.client.disconnect({ reason: "Transport detach" });
15962
16135
  }
15963
16136
  /**
15964
16137
  * Backwards-compatible alias for `disconnect()`.
@@ -16202,6 +16375,107 @@ var Granular = class _Granular {
16202
16375
  await this.maybeRunEnvironmentImporter(resolved, environment);
16203
16376
  return environment;
16204
16377
  }
16378
+ /**
16379
+ * Read the active environment selected by Granular for an already-recorded
16380
+ * external user. This is intentionally read-only: browser/login code must
16381
+ * not create subjects or environments as a side effect.
16382
+ */
16383
+ async getActiveEnvironmentForUser(options) {
16384
+ const sandboxId = options.sandboxId.trim();
16385
+ const tagName = options.tag.trim();
16386
+ const userId = options.userId.trim();
16387
+ if (!sandboxId || !tagName || !userId) {
16388
+ throw new Error(
16389
+ "getActiveEnvironmentForUser() requires sandboxId, tag, and userId."
16390
+ );
16391
+ }
16392
+ const subjects = await this.request(
16393
+ `/control/subjects?identityId=${encodeURIComponent(userId)}`
16394
+ );
16395
+ const subject = (subjects.items || []).find(
16396
+ (item) => item.identityId === userId || item.userId === userId
16397
+ );
16398
+ if (!subject?.subjectId && !subject?.granularId) {
16399
+ return null;
16400
+ }
16401
+ const subjectId = subject.subjectId || subject.granularId;
16402
+ const tags = await this.request(
16403
+ `/control/sandboxes/${encodeURIComponent(sandboxId)}/tags`
16404
+ );
16405
+ const tag = (tags.items || []).find(
16406
+ (item) => item?.name === tagName
16407
+ );
16408
+ if (!tag) return null;
16409
+ const query = new URLSearchParams({
16410
+ tagId: tag.tagId,
16411
+ slot: options.slot?.trim() || "default"
16412
+ });
16413
+ try {
16414
+ const payload = await this.request(
16415
+ `/control/sandboxes/${encodeURIComponent(sandboxId)}/subjects/${encodeURIComponent(subjectId)}/active-environment?${query.toString()}`
16416
+ );
16417
+ return payload.environment ? this.bindEnvironmentHandle(
16418
+ normalizeEnvironmentData(payload.environment)
16419
+ ) : null;
16420
+ } catch (error) {
16421
+ const message = error instanceof Error ? error.message : String(error);
16422
+ if (message.includes("404") || message.includes("not found")) {
16423
+ return null;
16424
+ }
16425
+ throw error;
16426
+ }
16427
+ }
16428
+ /**
16429
+ * Register one reviewed, pre-existing environment as the active workspace
16430
+ * for an external user. This is for a controlled migration only: it does
16431
+ * not create an environment and it does not run an importer.
16432
+ */
16433
+ async adoptEnvironmentForUser(options) {
16434
+ const sandboxId = options.sandboxId.trim();
16435
+ const tagName = options.tag.trim();
16436
+ const userId = options.userId.trim();
16437
+ const environmentId = options.environmentId.trim();
16438
+ if (!sandboxId || !tagName || !userId || !environmentId) {
16439
+ throw new Error(
16440
+ "adoptEnvironmentForUser() requires sandboxId, tag, userId, and environmentId."
16441
+ );
16442
+ }
16443
+ const subjects = await this.request(
16444
+ `/control/subjects?identityId=${encodeURIComponent(userId)}`
16445
+ );
16446
+ const subject = (subjects.items || []).find(
16447
+ (item) => item.identityId === userId || item.userId === userId
16448
+ );
16449
+ if (!subject?.subjectId && !subject?.granularId) {
16450
+ throw new Error(`No Granular subject exists for user ${userId}.`);
16451
+ }
16452
+ const tags = await this.request(`/control/sandboxes/${encodeURIComponent(sandboxId)}/tags`);
16453
+ const tag = (tags.items || []).find(
16454
+ (item) => item?.name === tagName
16455
+ );
16456
+ if (!tag) {
16457
+ throw new Error(`Tag ${tagName} was not found in sandbox ${sandboxId}.`);
16458
+ }
16459
+ const payload = await this.request(
16460
+ "/control/environment-activations/adopt",
16461
+ {
16462
+ method: "POST",
16463
+ body: JSON.stringify({
16464
+ environmentId,
16465
+ subjectId: subject.subjectId || subject.granularId,
16466
+ tagId: tag.tagId,
16467
+ slot: options.slot?.trim() || "default",
16468
+ confirmExistingData: true
16469
+ })
16470
+ }
16471
+ );
16472
+ if (!payload.environment) {
16473
+ throw new Error("Granular did not return the adopted environment.");
16474
+ }
16475
+ return this.bindEnvironmentHandle(
16476
+ normalizeEnvironmentData(payload.environment)
16477
+ );
16478
+ }
16205
16479
  /**
16206
16480
  * Deprecated compatibility alias for `openEnvironment()`.
16207
16481
  *
@@ -16233,7 +16507,9 @@ var Granular = class _Granular {
16233
16507
  requestedOntology,
16234
16508
  sandboxId: environmentData.sandboxId,
16235
16509
  subjectId: environmentData.subjectId,
16236
- setupTriggerReason: options.reason || "new_environment"
16510
+ externalUserId: environmentData.subjectId,
16511
+ setupTriggerReason: options.reason || "new_environment",
16512
+ setupOperationKey: options.operationKey
16237
16513
  },
16238
16514
  environment
16239
16515
  );
@@ -16245,20 +16521,17 @@ var Granular = class _Granular {
16245
16521
  }
16246
16522
  return tag;
16247
16523
  }
16248
- buildManagedEnvironmentName(tag, versionId) {
16249
- return `__sdk__${tag}__${versionId}__pinned`;
16524
+ buildManagedEnvironmentName(tag, versionId, resetKey) {
16525
+ if (!resetKey) {
16526
+ return `__sdk__${tag}__${versionId}__tracked`;
16527
+ }
16528
+ const safeResetKey = resetKey.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 80);
16529
+ return `__sdk__${tag}__${versionId}__reset__${safeResetKey}`;
16250
16530
  }
16251
16531
  isManagedEnvironmentName(environment, tagName) {
16252
16532
  const name = environment.environment || environment.envName || "";
16253
16533
  return name.startsWith(`__sdk__${tagName}__`);
16254
16534
  }
16255
- isPinnedToVersion(environment, versionId) {
16256
- return environment.buildPolicy.mode === "pinned" && (environment.versionId === versionId || environment.buildPolicy.versionId === versionId || environment.buildPolicy.buildId === versionId);
16257
- }
16258
- matchesTagTrackedEnvironment(environment, tagName, tagId) {
16259
- const environmentTagName = environment.tag?.name || environment.buildPolicy.tagName || null;
16260
- 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);
16261
- }
16262
16535
  sortEnvironmentsByRecency(environments) {
16263
16536
  return [...environments].sort(
16264
16537
  (left, right) => right.updatedAt - left.updatedAt
@@ -16308,61 +16581,187 @@ var Granular = class _Granular {
16308
16581
  `Tag "${tagName}" does not currently point to a build/version.`
16309
16582
  );
16310
16583
  }
16584
+ const slot = options.slot?.trim() || "default";
16585
+ const resetKey = options.resetKey?.trim() || void 0;
16586
+ const resolveActive = async (operationKey) => {
16587
+ const query = new URLSearchParams({ tagId: tag.tagId, slot });
16588
+ if (operationKey) query.set("operationKey", operationKey);
16589
+ try {
16590
+ const payload = await this.request(
16591
+ `/control/sandboxes/${encodeURIComponent(sandbox.sandboxId)}/subjects/${encodeURIComponent(user.granularId)}/active-environment?${query.toString()}`
16592
+ );
16593
+ return payload.environment ? normalizeEnvironmentData(payload.environment) : null;
16594
+ } catch (error) {
16595
+ const message = error instanceof Error ? error.message : String(error);
16596
+ if (message.includes("404") || message.includes("not found")) {
16597
+ return null;
16598
+ }
16599
+ throw error;
16600
+ }
16601
+ };
16602
+ const activate = async (environment2) => {
16603
+ const payload = await this.request(
16604
+ "/control/environment-activations",
16605
+ {
16606
+ method: "POST",
16607
+ body: JSON.stringify({
16608
+ environmentId: environment2.environmentId,
16609
+ tagId: tag.tagId,
16610
+ slot,
16611
+ operationKey: resetKey,
16612
+ operation: resetKey ? "explicit_reset" : void 0
16613
+ })
16614
+ }
16615
+ );
16616
+ return normalizeEnvironmentData(payload.environment);
16617
+ };
16618
+ if (resetKey) {
16619
+ const resetEnvironment = await resolveActive(resetKey);
16620
+ if (resetEnvironment) {
16621
+ return {
16622
+ environment: resetEnvironment,
16623
+ requestedOntology: ontology,
16624
+ sandboxId: sandbox.sandboxId,
16625
+ subjectId: user.granularId,
16626
+ externalUserId: user.userId,
16627
+ // Retrying an explicit reset must also resume its durable setup run.
16628
+ // Otherwise a Container crash after queue submission would leave a
16629
+ // valid environment permanently marked as "running".
16630
+ setupTriggerReason: "explicit_reset",
16631
+ setupOperationKey: resetKey
16632
+ };
16633
+ }
16634
+ } else {
16635
+ const active = await resolveActive();
16636
+ if (active && (active.versionId === targetVersionId || options.createFreshIfOutdated !== true)) {
16637
+ return {
16638
+ environment: active,
16639
+ requestedOntology: ontology,
16640
+ sandboxId: sandbox.sandboxId,
16641
+ subjectId: user.granularId,
16642
+ externalUserId: user.userId
16643
+ };
16644
+ }
16645
+ }
16311
16646
  const allEnvironments = await this.environments.list(sandbox.sandboxId);
16312
16647
  const userEnvironments = allEnvironments.filter(
16313
- (environment) => environment.subjectId === user.granularId
16648
+ (environment2) => environment2.subjectId === user.granularId
16314
16649
  );
16315
16650
  const currentMatches = this.sortEnvironmentsByRecency(
16316
16651
  userEnvironments.filter(
16317
- (environment) => this.matchesTagTrackedEnvironment(environment, tagName, tag.tagId) && environment.versionId === targetVersionId && (!this.isManagedEnvironmentName(environment, tagName) || this.isPinnedToVersion(environment, targetVersionId))
16652
+ (environment2) => environment2.tagId === tag.tagId && environment2.versionId === targetVersionId
16318
16653
  )
16319
16654
  );
16320
- if (currentMatches.length > 0) {
16655
+ if (!resetKey && currentMatches.length > 0) {
16656
+ const environment2 = await activate(currentMatches[0]);
16321
16657
  return {
16322
- environment: currentMatches[0],
16658
+ environment: environment2,
16323
16659
  requestedOntology: ontology,
16324
16660
  sandboxId: sandbox.sandboxId,
16325
- subjectId: user.granularId
16661
+ subjectId: user.granularId,
16662
+ externalUserId: user.userId
16326
16663
  };
16327
16664
  }
16328
16665
  const outdatedMatches = this.sortEnvironmentsByRecency(
16329
- userEnvironments.filter(
16330
- (environment) => this.matchesTagTrackedEnvironment(environment, tagName, tag.tagId)
16331
- )
16666
+ userEnvironments.filter((environment2) => environment2.tagId === tag.tagId)
16332
16667
  );
16333
16668
  if (outdatedMatches.length > 0 && options.createFreshIfOutdated !== true) {
16669
+ const environment2 = await activate(outdatedMatches[0]);
16334
16670
  return {
16335
- environment: outdatedMatches[0],
16671
+ environment: environment2,
16336
16672
  requestedOntology: ontology,
16337
16673
  sandboxId: sandbox.sandboxId,
16338
- subjectId: user.granularId
16674
+ subjectId: user.granularId,
16675
+ externalUserId: user.userId
16339
16676
  };
16340
16677
  }
16678
+ const created = await this.environments.create(sandbox.sandboxId, {
16679
+ subjectId: user.granularId,
16680
+ environment: this.buildManagedEnvironmentName(
16681
+ tagName,
16682
+ targetVersionId,
16683
+ resetKey
16684
+ ),
16685
+ tagId: tag.tagId,
16686
+ permissionProfileId: null
16687
+ });
16688
+ const environment = await activate(created);
16341
16689
  return {
16342
- environment: await this.environments.create(sandbox.sandboxId, {
16343
- subjectId: user.granularId,
16344
- environment: this.buildManagedEnvironmentName(tagName, targetVersionId),
16345
- tagId: tag.tagId,
16346
- versionId: targetVersionId,
16347
- permissionProfileId: null
16348
- }),
16690
+ environment,
16349
16691
  requestedOntology: ontology,
16350
16692
  sandboxId: sandbox.sandboxId,
16351
16693
  subjectId: user.granularId,
16352
- setupTriggerReason: outdatedMatches.length > 0 ? "fresh_after_version_update" : "new_environment"
16694
+ externalUserId: user.userId,
16695
+ setupTriggerReason: resetKey ? "explicit_reset" : outdatedMatches.length > 0 ? "fresh_after_version_update" : "new_environment",
16696
+ setupOperationKey: resetKey
16353
16697
  };
16354
16698
  }
16355
16699
  /**
16356
- * List active (open) sessions for an environment each session is one agent conversation thread.
16700
+ * List indexed sessions using ownership filters and bounded pagination.
16701
+ */
16702
+ async listSessions(options) {
16703
+ const environmentId = options.environmentId?.trim();
16704
+ const sandboxId = options.sandboxId?.trim();
16705
+ const subjectId = options.subjectId?.trim();
16706
+ if (!environmentId && !sandboxId && !subjectId) {
16707
+ throw new Error(
16708
+ "listSessions() requires environmentId, sandboxId, or subjectId so history cannot be scanned accidentally."
16709
+ );
16710
+ }
16711
+ const status = options.status || "active";
16712
+ const allowedStatuses = /* @__PURE__ */ new Set([
16713
+ "active",
16714
+ "closed",
16715
+ "expired",
16716
+ "failed",
16717
+ "timeout",
16718
+ "all"
16719
+ ]);
16720
+ if (!allowedStatuses.has(status)) {
16721
+ throw new Error(`Unsupported session status: ${String(status)}`);
16722
+ }
16723
+ const limit = boundedSessionListInteger(
16724
+ options.limit,
16725
+ "limit",
16726
+ DEFAULT_CONVERSATION_SESSION_LIST_LIMIT,
16727
+ 1,
16728
+ MAX_CONVERSATION_SESSION_LIST_LIMIT
16729
+ );
16730
+ const offset = boundedSessionListInteger(
16731
+ options.offset,
16732
+ "offset",
16733
+ 0,
16734
+ 0,
16735
+ MAX_CONVERSATION_SESSION_LIST_OFFSET
16736
+ );
16737
+ const query = new URLSearchParams({
16738
+ limit: String(limit),
16739
+ offset: String(offset)
16740
+ });
16741
+ if (environmentId) query.set("environmentId", environmentId);
16742
+ if (sandboxId) query.set("sandboxId", sandboxId);
16743
+ if (subjectId) query.set("userId", subjectId);
16744
+ if (options.sessionScope?.trim()) {
16745
+ query.set("sessionScope", options.sessionScope.trim());
16746
+ }
16747
+ if (status !== "all") query.set("status", status);
16748
+ const res = await this.request(
16749
+ `/control/sessions?${query.toString()}`
16750
+ );
16751
+ const items = Array.isArray(res.items) ? res.items : [];
16752
+ return items.map((row) => this.normalizeConversationSession(row));
16753
+ }
16754
+ /**
16755
+ * List active (open) sessions for an environment.
16357
16756
  */
16358
16757
  async listOpenSessions(filters) {
16359
- return this.listSessionsForEnvironment(filters.environmentId, "active");
16758
+ return this.listSessions({ ...filters, status: "active" });
16360
16759
  }
16361
16760
  /**
16362
16761
  * List closed sessions for an environment (conversations that have disconnected).
16363
16762
  */
16364
16763
  async listClosedSessions(filters) {
16365
- return this.listSessionsForEnvironment(filters.environmentId, "closed");
16764
+ return this.listSessions({ ...filters, status: "closed" });
16366
16765
  }
16367
16766
  async getUserEnvironmentState(options) {
16368
16767
  const query = new URLSearchParams({
@@ -16397,14 +16796,6 @@ var Granular = class _Granular {
16397
16796
  });
16398
16797
  return result.readAtBySessionId || {};
16399
16798
  }
16400
- async listSessionsForEnvironment(environmentId, status) {
16401
- const query = new URLSearchParams({ environmentId, status });
16402
- const res = await this.request(
16403
- `/control/sessions?${query.toString()}`
16404
- );
16405
- const items = Array.isArray(res.items) ? res.items : [];
16406
- return items.map((row) => this.normalizeConversationSession(row));
16407
- }
16408
16799
  normalizeConversationSession(row) {
16409
16800
  const sessionId = String(row.sessionId ?? row.session_id ?? "");
16410
16801
  const environmentId = String(row.environmentId ?? row.environment_id ?? "");
@@ -16463,6 +16854,7 @@ var Granular = class _Granular {
16463
16854
  */
16464
16855
  async createSession(options) {
16465
16856
  const clientId = options.clientId || `client_${Date.now()}`;
16857
+ const sessionScope = options.sessionScope?.trim() || void 0;
16466
16858
  await this.activateEnvironment(options.environmentId);
16467
16859
  const envData = await this.environments.get(options.environmentId);
16468
16860
  const environment = this.bindEnvironmentHandle(envData);
@@ -16471,6 +16863,8 @@ var Granular = class _Granular {
16471
16863
  body: JSON.stringify({
16472
16864
  environmentId: options.environmentId,
16473
16865
  clientId,
16866
+ sessionScope,
16867
+ capabilities: sessionScope ? { sessionScope } : void 0,
16474
16868
  initialHeap: options.initialHeap
16475
16869
  })
16476
16870
  });
@@ -16578,11 +16972,24 @@ var Granular = class _Granular {
16578
16972
  {
16579
16973
  method: "POST",
16580
16974
  body: JSON.stringify({
16581
- triggerReason: resolved.setupTriggerReason
16975
+ triggerReason: resolved.setupTriggerReason,
16976
+ operationKey: resolved.setupOperationKey
16582
16977
  })
16583
16978
  }
16584
16979
  );
16585
16980
  const setupRunId = setupRun.setupRunId;
16981
+ let claim = null;
16982
+ for (let attempt = 0; attempt < 3; attempt += 1) {
16983
+ claim = await this.request(
16984
+ `/control/environment-setup-runs/${setupRunId}/importer-claim`,
16985
+ { method: "POST", body: JSON.stringify({}) }
16986
+ );
16987
+ if (claim.action !== "busy") break;
16988
+ await sleep(Math.min(3e4, Math.max(250, claim.retryAfterMs || 1e3)));
16989
+ }
16990
+ if (!claim) {
16991
+ throw new Error(`Unable to claim environment setup run ${setupRunId}.`);
16992
+ }
16586
16993
  const updateSetupRun = async (patch) => {
16587
16994
  await this.request(
16588
16995
  `/control/environment-setup-runs/${setupRunId}`,
@@ -16592,10 +16999,26 @@ var Granular = class _Granular {
16592
16999
  }
16593
17000
  );
16594
17001
  };
17002
+ if (claim.action === "submitted") {
17003
+ const completedSetupRun = await this.request(
17004
+ `/control/environment-setup-runs/${setupRunId}`,
17005
+ { method: "PATCH", body: JSON.stringify({ markHookCompleted: true }) }
17006
+ );
17007
+ const refreshedEnvironment = await this.environments.get(
17008
+ environment.environmentId
17009
+ );
17010
+ environment.syncEnvironmentData(refreshedEnvironment);
17011
+ return completedSetupRun;
17012
+ }
17013
+ if (claim.action === "busy" || claim.action === "terminal") {
17014
+ return claim.summary;
17015
+ }
17016
+ let importSequence = 0;
16595
17017
  const importerContext = {
16596
17018
  environmentId: environment.environmentId,
16597
17019
  sandboxId: environment.sandboxId,
16598
17020
  subjectId: environment.subjectId,
17021
+ externalUserId: resolved.externalUserId,
16599
17022
  reason: resolved.setupTriggerReason,
16600
17023
  incrementTotalObjectsToImportCount: async (n) => {
16601
17024
  const safeIncrement = Math.max(0, Math.trunc(n));
@@ -16612,7 +17035,10 @@ var Granular = class _Granular {
16612
17035
  importRecords: async (records, options) => environment.enqueueRecordImport(records, {
16613
17036
  batchSize: options?.batchSize,
16614
17037
  writeMode: options?.writeMode,
16615
- setupRunId
17038
+ setupRunId,
17039
+ // Sequence is deterministic for a retry of one importer hook. It
17040
+ // prevents a Container restart from creating a second queue import.
17041
+ operationKey: `${setupRunId}:import:${importSequence++}`
16616
17042
  })
16617
17043
  };
16618
17044
  try {
@@ -19662,6 +20088,7 @@ function buildGranularAgentSystemPrompt(input) {
19662
20088
  - \`groundedObjects.save(...)\` only accepts scalar values, session files, runtime records/sandbox instances, or arrays of runtime records/sandbox instances from one class. Do not save plain action/effect result objects; fetch affected records first or answer from summaries with \`replyToUser(...)\`.
19663
20089
  - Do not use \`showObjects({ entries: [...] })\` or \`showObjects({ saveAs, entries })\`. Save ordered pages, queues, search results, or ranked lists with \`groundedObjects.save(...)\`, then call \`showObjects({ variableNames: [...] })\` once.
19664
20090
  - Use \`showAgentResponse({ reply, show: [record, action] })\` when one assistant message should combine text, grounded records, files, prepared actions, or action suggestions. The \`action\` can be a state handle such as \`record.lifecycle.approved\` or an action handle such as \`record.lifecycle.approved.reach()\`.
20091
+ - Pass grounded records directly in \`show\` when the default record presentation answers the request. When the user asks for particular columns, comparisons, or computed values, import \`table\` (and \`relativeTime\` when useful) from \`@granular/agent\` and call \`showAgentResponse({ reply, show: table(records, [{ label: "Object", value: record => record.label }, { label: "When", value: record => relativeTime(record.timestamp) }]) })\`. Column callbacks must be synchronous and return a scalar, \`Date\`, or \`relativeTime(...)\`; they run inside the job and only resolved cells are persisted.
19665
20092
  - Do not use deprecated side-channel helpers such as \`agent_text_message(...)\`, \`agent_heap_objects(...)\`, or \`agent_message(...)\` unless the generated types expose no Harness v3 helper alternative.
19666
20093
  - When the user asks to show, list, display, open, or "show them" for records you found, call \`showObjects(...)\`; do not answer only with a count or text summary.
19667
20094
  - For count-only questions such as "how many", "how many X do I have", or "what is the total number of X", call the entity \`.count(...)\` or use page \`totalCount\` only when a page is already needed for other reasons. Answer with \`replyToUser(...)\` only. Do not call \`showObjects(...)\`, \`saveAs\`, or \`groundedObjects.save(...)\` unless the user also asked to see records or a later requested action needs a reusable record selection.
@@ -19932,11 +20359,12 @@ ${actionIndex}
19932
20359
  - Use typed plan fields and helpers directly. For visible blocker text, use the declared blocker-formatting helper when available instead of hand-written object casts.
19933
20360
  - A suggestion is a recommendation button: \`await stateHandle.suggest(message)\`. A prepared action is the editable form the user can review/run: \`const action = stateHandle.reach(); const prepared = await action.open()\`.
19934
20361
  - Use suggestions when the user asks "what can I do next?", asks for options, or gives an unclear intent. Prefer 2 to 5 concrete suggestions and keep text short.
19935
- - Open a prepared action when the user asks for one clear action. Prefill only values grounded in the user request, conversation, files, selected records, or fresh reads. Leave unknown fields empty; do not invent them.
20362
+ - Opening a prepared action does not execute the underlying mutation. Use \`.open()\` only when the user asks to prepare, review, show, or edit an action before running it, when a new record must be prepared, or when a reviewable form must collect missing editable inputs. Prefill only grounded values and leave unknown fields empty.
20363
+ - When the user explicitly commands an existing-record mutation such as approve, reject, block, route, send, or update, and one visible domain action uniquely matches, call that domain action directly. Do not substitute a state-handle \`.open()\` artifact for execution. If runtime policy requires confirmation, invoke the action once and let the runtime pause and resume that same invocation.
19936
20364
  - When a grounded related/context record can satisfy the prepared action through declared relationships, use those relationships to fill required relationship inputs before opening or updating the action. If a required relationship remains empty, continue through declared relationship chains from the grounded object when the next hop can fill that slot. Do not only save the context record in memory while leaving derivable relationship slots blank.
19937
20365
  - A derived intermediate relationship is not enough when another required relationship is still reachable from it. For example, if a team gives a cost center and the action also requires a budget, traverse the cost center's declared budget relationship before opening the action.
19938
20366
  - If the user says they have a document/work item/event but no matching record is found, check for a declared class-level new-record state/action handle or importable backend create/preparation action for that named class before giving up. Use grounded required fields to open the prepared action or call the create/preparation action; if required values are still missing and no prepared action can collect them, ask only for those values. Do not claim the record already exists.
19939
- - Do not ask for confirmation before opening a prepared action. The prepared action is itself reviewable. Use \`userInteraction.askConfirmation\` only when the user explicitly asks for yes/no approval, the selected action is ambiguous after grounding, or the domain/runtime asks for confirmation.
20367
+ - Do not ask for confirmation before opening a prepared action requested for review; the artifact is itself reviewable. For a direct mutation command, do not open an artifact merely to obtain confirmation. Use \`userInteraction.askConfirmation\` only when the user explicitly asks for a separate yes/no step, material ambiguity remains after grounding, or policy requires confirmation outside the invoked action runtime.
19940
20368
  - If the action cannot continue because of missing input, stale state, missing relationships, related-state requirements, or permissions, keep/show the prepared action at that blocker and explain the next needed person, record, or value. Do not skip workflow steps or target a later state.
19941
20369
  - Reuse an already-open prepared action for the same target/action when available: update it, show it again, or explain what is still needed instead of creating a duplicate.
19942
20370
  - If an open prepared action needs edits, prefer the returned record helper: \`const prepared = await action.open(); await prepared.updateInputs({ inputValues, relationships });\`. Use \`artifacts.updateInputs(id, patch)\` only when you only have an id.
@@ -20166,8 +20594,9 @@ function buildContinuationInstructionFromTemplate(resultPreview, options) {
20166
20594
  }
20167
20595
 
20168
20596
  // src/openai-usage.ts
20169
- var OPENAI_PRICING_SOURCE_URL = "https://developers.openai.com/api/docs/models/gpt-5.4/";
20170
- var OPENAI_PRICING_EFFECTIVE_DATE = "2026-05-19";
20597
+ var OPENAI_GPT_5_4_PRICING_SOURCE_URL = "https://developers.openai.com/api/docs/models/gpt-5.4/";
20598
+ var OPENAI_GPT_5_6_PRICING_SOURCE_URL = "https://developers.openai.com/api/docs/pricing";
20599
+ var OPENAI_LONG_CONTEXT_THRESHOLD_TOKENS = 272e3;
20171
20600
  var OPENAI_MODEL_PRICING_USD_PER_MILLION = {
20172
20601
  "gpt-5.4": {
20173
20602
  provider: "openai",
@@ -20176,8 +20605,26 @@ var OPENAI_MODEL_PRICING_USD_PER_MILLION = {
20176
20605
  inputUsdPerMillion: 2.5,
20177
20606
  cachedInputUsdPerMillion: 0.25,
20178
20607
  outputUsdPerMillion: 15,
20179
- sourceUrl: OPENAI_PRICING_SOURCE_URL,
20180
- effectiveDate: OPENAI_PRICING_EFFECTIVE_DATE
20608
+ sourceUrl: OPENAI_GPT_5_4_PRICING_SOURCE_URL,
20609
+ effectiveDate: "2026-05-19"
20610
+ },
20611
+ "gpt-5.6-luna": {
20612
+ provider: "openai",
20613
+ model: "gpt-5.6-luna",
20614
+ currency: "USD",
20615
+ inputUsdPerMillion: 1,
20616
+ cachedInputUsdPerMillion: 0.1,
20617
+ cacheWriteUsdPerMillion: 1.25,
20618
+ outputUsdPerMillion: 6,
20619
+ sourceUrl: OPENAI_GPT_5_6_PRICING_SOURCE_URL,
20620
+ effectiveDate: "2026-07-11",
20621
+ longContextThresholdTokens: OPENAI_LONG_CONTEXT_THRESHOLD_TOKENS,
20622
+ longContextPricing: {
20623
+ inputUsdPerMillion: 2,
20624
+ cachedInputUsdPerMillion: 0.2,
20625
+ cacheWriteUsdPerMillion: 2.5,
20626
+ outputUsdPerMillion: 9
20627
+ }
20181
20628
  }
20182
20629
  };
20183
20630
  function asRecord5(value) {
@@ -20190,8 +20637,18 @@ function numberField(record, key) {
20190
20637
  function microsPerMillion(usdPerMillion) {
20191
20638
  return Math.round(usdPerMillion * 1e6);
20192
20639
  }
20193
- function getOpenAIModelPricing(model) {
20194
- return OPENAI_MODEL_PRICING_USD_PER_MILLION[model] || null;
20640
+ function getOpenAIModelPricing(model, inputTokens = 0) {
20641
+ const pricing = OPENAI_MODEL_PRICING_USD_PER_MILLION[model];
20642
+ if (!pricing) return null;
20643
+ const threshold = pricing.longContextThresholdTokens ?? null;
20644
+ if (pricing.longContextPricing && typeof threshold === "number" && inputTokens > threshold) {
20645
+ return {
20646
+ ...pricing,
20647
+ ...pricing.longContextPricing,
20648
+ contextTier: "long"
20649
+ };
20650
+ }
20651
+ return { ...pricing, contextTier: "short" };
20195
20652
  }
20196
20653
  function normalizeOpenAIUsage(rawUsage) {
20197
20654
  const usage = asRecord5(rawUsage);
@@ -20199,6 +20656,7 @@ function normalizeOpenAIUsage(rawUsage) {
20199
20656
  return {
20200
20657
  inputTokens: 0,
20201
20658
  cachedInputTokens: 0,
20659
+ cacheWriteTokens: 0,
20202
20660
  uncachedInputTokens: 0,
20203
20661
  outputTokens: 0,
20204
20662
  reasoningTokens: 0,
@@ -20214,20 +20672,28 @@ function normalizeOpenAIUsage(rawUsage) {
20214
20672
  inputTokens,
20215
20673
  numberField(inputDetails, "cached_tokens") || numberField(inputDetails, "cached_input_tokens")
20216
20674
  );
20675
+ const cacheWriteTokens = Math.min(
20676
+ Math.max(inputTokens - cachedInputTokens, 0),
20677
+ numberField(inputDetails, "cache_write_tokens")
20678
+ );
20217
20679
  const reasoningTokens = numberField(outputDetails, "reasoning_tokens") || numberField(outputDetails, "reasoning_output_tokens");
20218
20680
  return {
20219
20681
  inputTokens,
20220
20682
  cachedInputTokens,
20221
- uncachedInputTokens: Math.max(inputTokens - cachedInputTokens, 0),
20683
+ cacheWriteTokens,
20684
+ uncachedInputTokens: Math.max(
20685
+ inputTokens - cachedInputTokens - cacheWriteTokens,
20686
+ 0
20687
+ ),
20222
20688
  outputTokens,
20223
20689
  reasoningTokens,
20224
20690
  totalTokens
20225
20691
  };
20226
20692
  }
20227
20693
  function calculateOpenAITokenSpend(model, rawUsage) {
20228
- const pricing = getOpenAIModelPricing(model);
20229
- if (!pricing) return null;
20230
20694
  const usage = normalizeOpenAIUsage(rawUsage);
20695
+ const pricing = getOpenAIModelPricing(model, usage.inputTokens);
20696
+ if (!pricing) return null;
20231
20697
  const inputPricePerMillionMicros = microsPerMillion(
20232
20698
  pricing.inputUsdPerMillion
20233
20699
  );
@@ -20237,14 +20703,19 @@ function calculateOpenAITokenSpend(model, rawUsage) {
20237
20703
  const outputPricePerMillionMicros = microsPerMillion(
20238
20704
  pricing.outputUsdPerMillion
20239
20705
  );
20706
+ const cacheWritePricePerMillionMicros = typeof pricing.cacheWriteUsdPerMillion === "number" ? microsPerMillion(pricing.cacheWriteUsdPerMillion) : null;
20707
+ const cacheWriteCostMicros = Math.round(
20708
+ usage.cacheWriteTokens * (cacheWritePricePerMillionMicros ?? inputPricePerMillionMicros) / 1e6
20709
+ );
20240
20710
  const amountMicros = Math.round(
20241
- (usage.uncachedInputTokens * inputPricePerMillionMicros + usage.cachedInputTokens * cachedInputPricePerMillionMicros + usage.outputTokens * outputPricePerMillionMicros) / 1e6
20711
+ (usage.uncachedInputTokens * inputPricePerMillionMicros + usage.cachedInputTokens * cachedInputPricePerMillionMicros + usage.cacheWriteTokens * (cacheWritePricePerMillionMicros ?? inputPricePerMillionMicros) + usage.outputTokens * outputPricePerMillionMicros) / 1e6
20242
20712
  );
20243
20713
  return {
20244
20714
  provider: "openai",
20245
20715
  model,
20246
20716
  inputTokens: usage.inputTokens,
20247
20717
  cachedInputTokens: usage.cachedInputTokens,
20718
+ cacheWriteTokens: usage.cacheWriteTokens,
20248
20719
  uncachedInputTokens: usage.uncachedInputTokens,
20249
20720
  outputTokens: usage.outputTokens,
20250
20721
  reasoningTokens: usage.reasoningTokens,
@@ -20253,7 +20724,11 @@ function calculateOpenAITokenSpend(model, rawUsage) {
20253
20724
  currency: "USD",
20254
20725
  inputPricePerMillionMicros,
20255
20726
  cachedInputPricePerMillionMicros,
20727
+ cacheWritePricePerMillionMicros,
20728
+ cacheWriteCostMicros,
20256
20729
  outputPricePerMillionMicros,
20730
+ pricingContextTier: pricing.contextTier || "short",
20731
+ longContextThresholdTokens: pricing.longContextThresholdTokens ?? null,
20257
20732
  pricingSource: pricing.sourceUrl,
20258
20733
  pricingEffectiveAt: pricing.effectiveDate,
20259
20734
  usage