@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.
@@ -25,11 +25,11 @@ var __export = (target, all) => {
25
25
  for (var name in all)
26
26
  __defProp(target, name, { get: all[name], enumerable: true });
27
27
  };
28
- var __copyProps = (to, from, except, desc) => {
29
- if (from && typeof from === "object" || typeof from === "function") {
30
- for (let key of __getOwnPropNames(from))
28
+ var __copyProps = (to, from2, except, desc) => {
29
+ if (from2 && typeof from2 === "object" || typeof from2 === "function") {
30
+ for (let key of __getOwnPropNames(from2))
31
31
  if (!__hasOwnProp.call(to, key) && key !== except)
32
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
32
+ __defProp(to, key, { get: () => from2[key], enumerable: !(desc = __getOwnPropDesc(from2, key)) || desc.enumerable });
33
33
  }
34
34
  return to;
35
35
  };
@@ -4046,16 +4046,37 @@ var WSClient = class {
4046
4046
  tokenRefreshTimer = null;
4047
4047
  isExplicitlyDisconnected = false;
4048
4048
  reconnectAttempts = 0;
4049
+ connectPromise = null;
4050
+ connectionEpoch = 0;
4051
+ cancelConnectAttempt = null;
4049
4052
  options;
4050
4053
  constructor(options) {
4051
4054
  this.options = options;
4052
4055
  this.url = options.url;
4053
4056
  this.sessionId = options.sessionId;
4054
4057
  this.token = options.token;
4058
+ if (options.initialDocumentSnapshot) {
4059
+ this.seedDocumentSnapshot(options.initialDocumentSnapshot);
4060
+ }
4055
4061
  }
4056
4062
  get currentSessionId() {
4057
4063
  return this.sessionId;
4058
4064
  }
4065
+ seedDocumentSnapshot(document) {
4066
+ if (!document || typeof document !== "object" || Array.isArray(document)) {
4067
+ return;
4068
+ }
4069
+ try {
4070
+ this.doc = document instanceof Uint8Array ? Automerge.load(document) : Automerge.from(document);
4071
+ this.syncState = Automerge.initSyncState();
4072
+ this.emit("sync", this.doc);
4073
+ } catch (error) {
4074
+ console.warn("[Granular] Failed to seed cached session document", error);
4075
+ }
4076
+ }
4077
+ saveDocumentSnapshot() {
4078
+ return Automerge.save(this.doc);
4079
+ }
4059
4080
  clearTokenRefreshTimer() {
4060
4081
  if (this.tokenRefreshTimer) {
4061
4082
  clearTimeout(this.tokenRefreshTimer);
@@ -4165,8 +4186,23 @@ var WSClient = class {
4165
4186
  * Connect to the WebSocket server
4166
4187
  * @returns {Promise<void>} Resolves when connection is open
4167
4188
  */
4168
- async connect() {
4189
+ async connect(options = {}) {
4190
+ if (this.ws?.readyState === READY_STATE_OPEN) return;
4191
+ if (this.connectPromise) return this.connectPromise;
4192
+ const connectPromise = this.connectAttempt(options.signal);
4193
+ this.connectPromise = connectPromise;
4194
+ try {
4195
+ await connectPromise;
4196
+ } finally {
4197
+ if (this.connectPromise === connectPromise) {
4198
+ this.connectPromise = null;
4199
+ }
4200
+ }
4201
+ }
4202
+ async connectAttempt(signal) {
4203
+ if (signal?.aborted) throw new Error("WebSocket connect aborted");
4169
4204
  const token = await this.resolveTokenForConnect();
4205
+ if (signal?.aborted) throw new Error("WebSocket connect aborted");
4170
4206
  this.isExplicitlyDisconnected = false;
4171
4207
  this.scheduleTokenRefresh();
4172
4208
  if (this.reconnectTimer) {
@@ -4178,7 +4214,7 @@ var WSClient = class {
4178
4214
  try {
4179
4215
  const wsModule = await Promise.resolve().then(() => (init_wrapper(), wrapper_exports));
4180
4216
  WebSocketClass = wsModule.default || wsModule;
4181
- } catch (e) {
4217
+ } catch {
4182
4218
  }
4183
4219
  }
4184
4220
  if (!WebSocketClass) {
@@ -4186,83 +4222,97 @@ var WSClient = class {
4186
4222
  'No WebSocket implementation found. If using Node.js, please install "ws" and pass the constructor to the SDK options: { WebSocketCtor: WebSocket }.'
4187
4223
  );
4188
4224
  }
4225
+ const epoch = ++this.connectionEpoch;
4226
+ const wsUrl = new URL(this.url);
4227
+ wsUrl.searchParams.set("sessionId", this.sessionId);
4228
+ wsUrl.searchParams.set("token", token);
4229
+ const socket = new WebSocketClass(wsUrl.toString());
4230
+ this.ws = socket;
4189
4231
  return new Promise((resolve, reject) => {
4190
- try {
4191
- const wsUrl = new URL(this.url);
4192
- wsUrl.searchParams.set("sessionId", this.sessionId);
4193
- wsUrl.searchParams.set("token", token);
4194
- this.ws = new WebSocketClass(wsUrl.toString());
4195
- if (!this.ws) throw new Error("Failed to create WebSocket");
4196
- const socket = this.ws;
4197
- if (typeof socket.on === "function") {
4198
- socket.on("open", () => {
4199
- if (this.reconnectTimer) {
4200
- clearTimeout(this.reconnectTimer);
4201
- this.reconnectTimer = null;
4202
- }
4203
- this.reconnectAttempts = 0;
4204
- this.emit("open", {});
4205
- resolve();
4206
- });
4207
- socket.on("message", (data) => {
4208
- try {
4209
- const message = JSON.parse(data.toString());
4210
- this.handleMessage(message);
4211
- } catch (error) {
4212
- console.error("[Granular] Failed to parse message:", error);
4213
- }
4214
- });
4215
- socket.on("error", (error) => {
4216
- this.emit("error", error);
4217
- if (socket.readyState !== READY_STATE_OPEN) {
4218
- reject(error);
4219
- }
4220
- });
4221
- socket.on("close", (code, reason) => {
4222
- this.handleDisconnect({
4223
- code,
4224
- reason: this.normalizeReason(reason),
4225
- // ws does not provide wasClean on Node-style close callback
4226
- wasClean: code === 1e3
4227
- });
4228
- });
4232
+ let settled = false;
4233
+ const isCurrent = () => this.connectionEpoch === epoch && this.ws === socket;
4234
+ const finish = (error) => {
4235
+ if (settled) return;
4236
+ settled = true;
4237
+ if (this.cancelConnectAttempt === handleAbort) {
4238
+ this.cancelConnectAttempt = null;
4239
+ }
4240
+ signal?.removeEventListener("abort", handleAbort);
4241
+ if (error) {
4242
+ reject(error instanceof Error ? error : new Error(String(error)));
4229
4243
  } else {
4230
- this.ws.onopen = () => {
4231
- if (this.reconnectTimer) {
4232
- clearTimeout(this.reconnectTimer);
4233
- this.reconnectTimer = null;
4234
- }
4235
- this.reconnectAttempts = 0;
4236
- this.emit("open", {});
4237
- resolve();
4238
- };
4239
- this.ws.onmessage = (event) => {
4240
- try {
4241
- const data = event.data;
4242
- const message = JSON.parse(data.toString());
4243
- this.handleMessage(message);
4244
- } catch (error) {
4245
- console.error("[Granular] Failed to parse message:", error);
4246
- }
4247
- };
4248
- this.ws.onerror = (event) => {
4249
- const error = new Error("WebSocket error");
4250
- error.event = event;
4251
- this.emit("error", error);
4252
- if (this.ws?.readyState !== READY_STATE_OPEN) {
4253
- reject(error);
4254
- }
4255
- };
4256
- this.ws.onclose = (event) => {
4257
- this.handleDisconnect({
4258
- code: event.code,
4259
- reason: event.reason,
4260
- wasClean: event.wasClean
4261
- });
4262
- };
4244
+ resolve();
4263
4245
  }
4264
- } catch (error) {
4265
- reject(error);
4246
+ };
4247
+ const closeStaleSocket = () => {
4248
+ try {
4249
+ socket.close(1e3, "Stale connection attempt");
4250
+ } catch {
4251
+ }
4252
+ };
4253
+ const handleAbort = () => {
4254
+ if (isCurrent()) {
4255
+ this.connectionEpoch += 1;
4256
+ this.ws = null;
4257
+ }
4258
+ closeStaleSocket();
4259
+ finish(new Error("WebSocket connect aborted"));
4260
+ };
4261
+ this.cancelConnectAttempt = handleAbort;
4262
+ const handleOpen = () => {
4263
+ if (!isCurrent()) {
4264
+ closeStaleSocket();
4265
+ return;
4266
+ }
4267
+ this.reconnectAttempts = 0;
4268
+ this.emit("open", {});
4269
+ finish();
4270
+ };
4271
+ const handleMessage = (data) => {
4272
+ if (!isCurrent()) return;
4273
+ try {
4274
+ const text = typeof data === "string" ? data : data && typeof data === "object" && "toString" in data ? String(data.toString()) : "";
4275
+ this.handleMessage(JSON.parse(text));
4276
+ } catch (error) {
4277
+ console.error("[Granular] Failed to parse message:", error);
4278
+ }
4279
+ };
4280
+ const handleError = (error) => {
4281
+ if (!isCurrent()) return;
4282
+ const typedError = error instanceof Error ? error : new Error("WebSocket error");
4283
+ this.emit("error", typedError);
4284
+ if (socket.readyState !== READY_STATE_OPEN) finish(typedError);
4285
+ };
4286
+ const handleClose = (close) => {
4287
+ if (!isCurrent()) return;
4288
+ if (!settled) {
4289
+ finish(
4290
+ new Error(
4291
+ `WebSocket closed before ready${close.code ? ` (code=${close.code})` : ""}`
4292
+ )
4293
+ );
4294
+ }
4295
+ this.handleDisconnect({
4296
+ code: close.code,
4297
+ reason: this.normalizeReason(close.reason),
4298
+ wasClean: close.wasClean
4299
+ });
4300
+ };
4301
+ signal?.addEventListener("abort", handleAbort, { once: true });
4302
+ const nodeSocket = socket;
4303
+ if (typeof nodeSocket.on === "function") {
4304
+ nodeSocket.on("open", handleOpen);
4305
+ nodeSocket.on("message", handleMessage);
4306
+ nodeSocket.on("error", handleError);
4307
+ nodeSocket.on(
4308
+ "close",
4309
+ (code, reason) => handleClose({ code, reason, wasClean: code === 1e3 })
4310
+ );
4311
+ } else {
4312
+ socket.onopen = handleOpen;
4313
+ socket.onmessage = (event) => handleMessage(event.data);
4314
+ socket.onerror = handleError;
4315
+ socket.onclose = (event) => handleClose(event);
4266
4316
  }
4267
4317
  });
4268
4318
  }
@@ -4280,9 +4330,58 @@ var WSClient = class {
4280
4330
  return void 0;
4281
4331
  }
4282
4332
  rejectPending(error) {
4283
- this.messageQueue.forEach((pending) => pending.reject(error));
4333
+ this.messageQueue.forEach((pending) => {
4334
+ clearTimeout(pending.timeout);
4335
+ pending.reject(error);
4336
+ });
4284
4337
  this.messageQueue = [];
4285
4338
  }
4339
+ emitReconnectErrorMessage(error) {
4340
+ const reconnectInfo = {
4341
+ error,
4342
+ sessionId: this.sessionId,
4343
+ timestamp: Date.now()
4344
+ };
4345
+ this.emit("reconnect_error", reconnectInfo);
4346
+ if (this.options.onReconnectError) {
4347
+ try {
4348
+ this.options.onReconnectError(reconnectInfo);
4349
+ } catch (callbackError) {
4350
+ console.error(
4351
+ "[Granular] onReconnectError callback failed:",
4352
+ callbackError
4353
+ );
4354
+ }
4355
+ }
4356
+ }
4357
+ scheduleReconnectAttempt() {
4358
+ if (this.isExplicitlyDisconnected || this.reconnectTimer) return null;
4359
+ const baseReconnectDelayMs = typeof this.options.reconnectDelayMs === "number" && Number.isFinite(this.options.reconnectDelayMs) && this.options.reconnectDelayMs > 0 ? this.options.reconnectDelayMs : DEFAULT_RECONNECT_DELAY_MS;
4360
+ 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;
4361
+ if (this.reconnectAttempts >= maxReconnectAttempts) {
4362
+ this.emitReconnectErrorMessage(
4363
+ `WebSocket reconnect attempts exhausted after ${maxReconnectAttempts} attempt(s).`
4364
+ );
4365
+ return null;
4366
+ }
4367
+ this.reconnectAttempts += 1;
4368
+ const reconnectDelayMs = Math.min(
4369
+ 3e4,
4370
+ baseReconnectDelayMs * 2 ** Math.max(0, this.reconnectAttempts - 1)
4371
+ );
4372
+ this.reconnectTimer = setTimeout(() => {
4373
+ this.reconnectTimer = null;
4374
+ console.log("[Granular] Attempting reconnect...");
4375
+ this.connect().catch((error) => {
4376
+ console.error("[Granular] Reconnect failed:", error);
4377
+ this.emitReconnectErrorMessage(
4378
+ error instanceof Error ? error.message : String(error)
4379
+ );
4380
+ this.scheduleReconnectAttempt();
4381
+ });
4382
+ }, reconnectDelayMs);
4383
+ return reconnectDelayMs;
4384
+ }
4286
4385
  buildDisconnectError(info) {
4287
4386
  const details = [
4288
4387
  info.code !== void 0 ? `code=${info.code}` : void 0,
@@ -4292,8 +4391,6 @@ var WSClient = class {
4292
4391
  return new Error(`WebSocket disconnected${suffix}`);
4293
4392
  }
4294
4393
  handleDisconnect(close = {}) {
4295
- const baseReconnectDelayMs = typeof this.options.reconnectDelayMs === "number" && Number.isFinite(this.options.reconnectDelayMs) && this.options.reconnectDelayMs > 0 ? this.options.reconnectDelayMs : DEFAULT_RECONNECT_DELAY_MS;
4296
- 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;
4297
4394
  const unexpected = !this.isExplicitlyDisconnected;
4298
4395
  const info = {
4299
4396
  code: close.code,
@@ -4313,32 +4410,9 @@ var WSClient = class {
4313
4410
  const disconnectError = this.buildDisconnectError(info);
4314
4411
  this.rejectPending(disconnectError);
4315
4412
  this.emit("disconnect", info);
4316
- if (this.reconnectAttempts >= maxReconnectAttempts) {
4317
- const reconnectInfo = {
4318
- error: `WebSocket reconnect attempts exhausted after ${maxReconnectAttempts} attempt(s).`,
4319
- sessionId: this.sessionId,
4320
- timestamp: Date.now()
4321
- };
4322
- this.emit("reconnect_error", reconnectInfo);
4323
- if (this.options.onReconnectError) {
4324
- try {
4325
- this.options.onReconnectError(reconnectInfo);
4326
- } catch (callbackError) {
4327
- console.error(
4328
- "[Granular] onReconnectError callback failed:",
4329
- callbackError
4330
- );
4331
- }
4332
- }
4333
- return;
4334
- }
4335
- this.reconnectAttempts += 1;
4336
- const reconnectDelayMs = Math.min(
4337
- 3e4,
4338
- baseReconnectDelayMs * 2 ** Math.max(0, this.reconnectAttempts - 1)
4339
- );
4340
- info.reconnectScheduled = true;
4341
- info.reconnectDelayMs = reconnectDelayMs;
4413
+ const reconnectDelayMs = this.scheduleReconnectAttempt();
4414
+ info.reconnectScheduled = reconnectDelayMs !== null;
4415
+ if (reconnectDelayMs !== null) info.reconnectDelayMs = reconnectDelayMs;
4342
4416
  if (this.options.onUnexpectedClose) {
4343
4417
  try {
4344
4418
  this.options.onUnexpectedClose(info);
@@ -4349,28 +4423,6 @@ var WSClient = class {
4349
4423
  );
4350
4424
  }
4351
4425
  }
4352
- this.reconnectTimer = setTimeout(() => {
4353
- console.log("[Granular] Attempting reconnect...");
4354
- this.connect().catch((error) => {
4355
- console.error("[Granular] Reconnect failed:", error);
4356
- const reconnectInfo = {
4357
- error: error instanceof Error ? error.message : String(error),
4358
- sessionId: this.sessionId,
4359
- timestamp: Date.now()
4360
- };
4361
- this.emit("reconnect_error", reconnectInfo);
4362
- if (this.options.onReconnectError) {
4363
- try {
4364
- this.options.onReconnectError(reconnectInfo);
4365
- } catch (callbackError) {
4366
- console.error(
4367
- "[Granular] onReconnectError callback failed:",
4368
- callbackError
4369
- );
4370
- }
4371
- }
4372
- });
4373
- }, reconnectDelayMs);
4374
4426
  }
4375
4427
  }
4376
4428
  handleMessage(message) {
@@ -4481,6 +4533,7 @@ var WSClient = class {
4481
4533
  const response = message;
4482
4534
  const pending = this.messageQueue.find((q) => q.id === response.id);
4483
4535
  if (pending) {
4536
+ clearTimeout(pending.timeout);
4484
4537
  if (response.type === "rpc_error") {
4485
4538
  pending.reject(
4486
4539
  new Error(
@@ -4526,16 +4579,22 @@ var WSClient = class {
4526
4579
  id
4527
4580
  };
4528
4581
  return new Promise((resolve, reject) => {
4529
- this.messageQueue.push({ resolve, reject, id });
4530
- this.ws.send(JSON.stringify(request));
4531
4582
  const timeoutMs = rpcTimeoutMsForMethod(method);
4532
- setTimeout(() => {
4583
+ const timeout = setTimeout(() => {
4533
4584
  const pending = this.messageQueue.find((q) => q.id === id);
4534
4585
  if (pending) {
4535
4586
  this.messageQueue = this.messageQueue.filter((q) => q.id !== id);
4536
4587
  reject(new Error(`RPC timeout: ${method}`));
4537
4588
  }
4538
4589
  }, timeoutMs);
4590
+ this.messageQueue.push({ resolve, reject, id, timeout });
4591
+ try {
4592
+ this.ws.send(JSON.stringify(request));
4593
+ } catch (error) {
4594
+ clearTimeout(timeout);
4595
+ this.messageQueue = this.messageQueue.filter((q) => q.id !== id);
4596
+ reject(error instanceof Error ? error : new Error(String(error)));
4597
+ }
4539
4598
  });
4540
4599
  }
4541
4600
  async handleIncomingRpc(request) {
@@ -4621,15 +4680,18 @@ var WSClient = class {
4621
4680
  /**
4622
4681
  * Disconnect the WebSocket and clear state
4623
4682
  */
4624
- disconnect() {
4683
+ disconnect(options = {}) {
4625
4684
  this.isExplicitlyDisconnected = true;
4685
+ this.cancelConnectAttempt?.();
4686
+ this.cancelConnectAttempt = null;
4687
+ this.connectionEpoch += 1;
4626
4688
  if (this.reconnectTimer) {
4627
4689
  clearTimeout(this.reconnectTimer);
4628
4690
  this.reconnectTimer = null;
4629
4691
  }
4630
4692
  this.clearTokenRefreshTimer();
4631
4693
  if (this.ws) {
4632
- this.ws.close(1e3, "Client disconnect");
4694
+ this.ws.close(1e3, options.reason || "Client disconnect");
4633
4695
  this.ws = null;
4634
4696
  }
4635
4697
  this.rejectPending(new Error("Client explicitly disconnected"));
@@ -4725,8 +4787,12 @@ function normalizePrompt(rawValue) {
4725
4787
  const source = promptRecord || raw;
4726
4788
  const id = typeof source.id === "string" ? source.id : typeof raw.id === "string" ? raw.id : typeof raw.promptId === "string" ? raw.promptId : "";
4727
4789
  if (!id) return null;
4790
+ const jobId = typeof source.jobId === "string" && source.jobId.trim() ? source.jobId.trim() : typeof raw.jobId === "string" && raw.jobId.trim() ? raw.jobId.trim() : void 0;
4791
+ const turnId = typeof source.turnId === "string" && source.turnId.trim() ? source.turnId.trim() : typeof raw.turnId === "string" && raw.turnId.trim() ? raw.turnId.trim() : void 0;
4728
4792
  return {
4729
4793
  id,
4794
+ ...jobId ? { jobId } : {},
4795
+ ...turnId ? { turnId } : {},
4730
4796
  type: normalizePromptType(source === raw ? raw : { ...raw, ...source }),
4731
4797
  title: typeof source.title === "string" ? source.title : "Input required",
4732
4798
  message: typeof source.message === "string" ? source.message : "",
@@ -4813,6 +4879,9 @@ var Session = class {
4813
4879
  this.initialQuota = options.initialQuota || null;
4814
4880
  this.setupEventHandlers();
4815
4881
  this.setupToolInvokeHandler();
4882
+ this.currentDomainRevision = this.extractDomainRevisionFromDoc(
4883
+ this.client.doc
4884
+ );
4816
4885
  }
4817
4886
  extractDomainRevisionFromDoc(doc) {
4818
4887
  const domain = doc?.domain;
@@ -5718,6 +5787,7 @@ function normalizeJobAgentMessageEnvelope(data) {
5718
5787
  }
5719
5788
  return {
5720
5789
  jobId: d.jobId,
5790
+ ...typeof d.turnId === "string" && d.turnId.trim() ? { turnId: d.turnId.trim() } : {},
5721
5791
  message: {
5722
5792
  messageId: d.messageId,
5723
5793
  kind: d.kind === "artifacts" ? "artifacts" : "text",
@@ -6376,9 +6446,14 @@ function normalizeShowRefs(value) {
6376
6446
  variableNames: normalizeRefs(record.variableNames),
6377
6447
  fileIds: normalizeRefs(record.fileIds),
6378
6448
  sessionArtifactIds: normalizeRefs(record.sessionArtifactIds),
6379
- actionSuggestions: normalizeActionSuggestions(record.actionSuggestions)
6449
+ actionSuggestions: normalizeActionSuggestions(record.actionSuggestions),
6450
+ tables: Array.isArray(record.tables) ? record.tables.filter(
6451
+ (table) => Boolean(
6452
+ table && typeof table === "object" && !Array.isArray(table) && Array.isArray(table.columns) && Array.isArray(table.rows)
6453
+ )
6454
+ ) : void 0
6380
6455
  };
6381
- return show.entryPaths || show.listNames || show.variableNames || show.fileIds || show.sessionArtifactIds || show.actionSuggestions ? show : void 0;
6456
+ return show.entryPaths || show.listNames || show.variableNames || show.fileIds || show.sessionArtifactIds || show.actionSuggestions || show.tables ? show : void 0;
6382
6457
  }
6383
6458
  function normalizeActionSuggestions(value) {
6384
6459
  if (!Array.isArray(value)) return void 0;
@@ -6400,6 +6475,76 @@ function normalizeActionSuggestions(value) {
6400
6475
  }
6401
6476
  return suggestions.length ? suggestions : void 0;
6402
6477
  }
6478
+ var TRANSCRIPT_MESSAGE_PART_LIMIT = 128;
6479
+ var TRANSCRIPT_MESSAGE_PART_TEXT_LIMIT = 2e5;
6480
+ var TRANSCRIPT_MESSAGE_PART_ACTION_LABEL_LIMIT = 1e3;
6481
+ var TRANSCRIPT_MESSAGE_ACTION_LIMIT = 64;
6482
+ function normalizeConversationMessageActions(value) {
6483
+ if (!Array.isArray(value) || value.length === 0) return void 0;
6484
+ const actions = [];
6485
+ for (const item of value.slice(0, TRANSCRIPT_MESSAGE_ACTION_LIMIT)) {
6486
+ const record = asRecord3(item);
6487
+ const kind = record?.kind;
6488
+ const label = trimString(record?.label ?? record?.title);
6489
+ const status = record?.status;
6490
+ if (kind !== "frontend" && kind !== "backend" && kind !== "system" || !label || label.length > TRANSCRIPT_MESSAGE_PART_ACTION_LABEL_LIMIT) {
6491
+ continue;
6492
+ }
6493
+ actions.push({
6494
+ kind,
6495
+ label,
6496
+ ...status === "done" || status === "queued" || status === "failed" ? { status } : {}
6497
+ });
6498
+ }
6499
+ return actions.length ? actions : void 0;
6500
+ }
6501
+ function normalizeConversationMessageParts(value, canonicalContent, canonicalActions) {
6502
+ if (!Array.isArray(value) || value.length === 0 || value.length > TRANSCRIPT_MESSAGE_PART_LIMIT) {
6503
+ return void 0;
6504
+ }
6505
+ const parts = [];
6506
+ const canonicalActionsById = new Map(
6507
+ (canonicalActions || []).map((action) => [
6508
+ `${action.kind}:${action.label}`,
6509
+ action
6510
+ ])
6511
+ );
6512
+ const seenActionIds = /* @__PURE__ */ new Set();
6513
+ let textLength = 0;
6514
+ for (const item of value) {
6515
+ const record = asRecord3(item);
6516
+ if (!record) return void 0;
6517
+ if (record.type === "text") {
6518
+ if (typeof record.text !== "string" || record.text.length === 0) {
6519
+ return void 0;
6520
+ }
6521
+ textLength += record.text.length;
6522
+ if (textLength > TRANSCRIPT_MESSAGE_PART_TEXT_LIMIT) return void 0;
6523
+ parts.push({ type: "text", text: record.text });
6524
+ continue;
6525
+ }
6526
+ if (record.type !== "action") return void 0;
6527
+ const action = asRecord3(record.action);
6528
+ const kind = action?.kind;
6529
+ const label = trimString(action?.label);
6530
+ if (kind !== "frontend" && kind !== "backend" && kind !== "system" || !label || label.length > TRANSCRIPT_MESSAGE_PART_ACTION_LABEL_LIMIT) {
6531
+ return void 0;
6532
+ }
6533
+ const actionId = `${kind}:${label}`;
6534
+ const canonicalAction = canonicalActionsById.get(actionId);
6535
+ if (!canonicalAction) return void 0;
6536
+ if (seenActionIds.has(actionId)) continue;
6537
+ seenActionIds.add(actionId);
6538
+ parts.push({
6539
+ type: "action",
6540
+ action: canonicalAction
6541
+ });
6542
+ }
6543
+ const orderedText = parts.filter(
6544
+ (part) => part.type === "text"
6545
+ ).map((part) => part.text).join("");
6546
+ return orderedText === canonicalContent ? parts : void 0;
6547
+ }
6403
6548
  function stringifyTranscriptValue(value, fallback = "") {
6404
6549
  if (typeof value === "string") {
6405
6550
  return value.trim() || fallback;
@@ -6558,10 +6703,12 @@ function normalizeConversationMessage(raw, artifactsById) {
6558
6703
  const content = trimString(
6559
6704
  record.content ?? record.reply ?? record.message ?? record.text
6560
6705
  );
6706
+ const actions = role === "assistant" ? normalizeConversationMessageActions(record.actions) : void 0;
6707
+ const parts = role === "assistant" ? normalizeConversationMessageParts(record.parts, content, actions) : void 0;
6561
6708
  const show = normalizeShowRefs(record.show);
6562
6709
  const id = asString(record.id) || crypto.randomUUID();
6563
6710
  const timestamp = asNumber(record.timestamp) || asNumber(record.ts) || 0;
6564
- if (!content && !show) return null;
6711
+ if (!content && !show && !actions?.length) return null;
6565
6712
  const artifactHistory = buildArtifactHistory(show, artifactsById);
6566
6713
  const historyContent = role === "assistant" ? content && artifactHistory ? `[Assistant reply]
6567
6714
  ${content}
@@ -6576,6 +6723,8 @@ ${content}` : artifactHistory : void 0;
6576
6723
  jobId: asString(record.jobId),
6577
6724
  promptId: asString(record.promptId),
6578
6725
  show,
6726
+ actions,
6727
+ parts,
6579
6728
  historyContent,
6580
6729
  source: "conversation"
6581
6730
  };
@@ -12153,7 +12302,12 @@ async function recordOpenAIUsageSpend(options) {
12153
12302
  const metadata = {
12154
12303
  ...options.metadata || {},
12155
12304
  ...options.usage.rawUsage !== void 0 ? { openaiUsage: options.usage.rawUsage } : {},
12156
- usageContext: context
12305
+ usageContext: context,
12306
+ pricingContextTier: options.usage.pricingContextTier,
12307
+ cacheWritePricePerMillionMicros: options.usage.cacheWritePricePerMillionMicros,
12308
+ cacheWriteTokens: options.usage.cacheWriteTokens,
12309
+ cacheWriteCostMicros: options.usage.cacheWriteCostMicros,
12310
+ longContextThresholdTokens: options.usage.longContextThresholdTokens
12157
12311
  };
12158
12312
  const response = await fetch(
12159
12313
  `${toGranularHttpBase(options.apiUrl)}/control/spend/events`,
@@ -13081,11 +13235,20 @@ function buildStateMachineModelMutations(modelPath, machines) {
13081
13235
  return mutations;
13082
13236
  }
13083
13237
  function buildMachineTypes(classSummary, machine) {
13238
+ const stateGlossary = machine.states.map((state) => {
13239
+ const label = state.label && state.label !== state.name ? state.label : null;
13240
+ const meaning = [label, state.description].filter(Boolean).join(" \u2014 ");
13241
+ const finalMarker = state.isFinal ? " Final state." : "";
13242
+ return `${state.name}${meaning ? `: ${meaning}` : "."}${finalMarker}`;
13243
+ });
13084
13244
  return [
13085
13245
  {
13086
13246
  kind: "union",
13087
13247
  name: stateTypeName(classSummary.name, machine.name),
13088
- docs: [`Allowed states for ${classSummary.name}.${machine.name}.`],
13248
+ docs: [
13249
+ `Allowed states for ${classSummary.name}.${machine.name}.`,
13250
+ ...stateGlossary
13251
+ ],
13089
13252
  members: machine.states.map((state) => state.name)
13090
13253
  },
13091
13254
  {
@@ -13433,7 +13596,7 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
13433
13596
  },
13434
13597
  add_transition: async (value, {
13435
13598
  name,
13436
- from,
13599
+ from: from2,
13437
13600
  to,
13438
13601
  label,
13439
13602
  description,
@@ -13448,7 +13611,7 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
13448
13611
  value.target.add_state_machine_transition(
13449
13612
  value.name,
13450
13613
  name,
13451
- from,
13614
+ from2,
13452
13615
  to,
13453
13616
  {
13454
13617
  label,
@@ -13800,6 +13963,18 @@ function buildEffectMetamodelMutations(toolPath, spec) {
13800
13963
  }
13801
13964
 
13802
13965
  // src/client.ts
13966
+ var DEFAULT_CONVERSATION_SESSION_LIST_LIMIT = 100;
13967
+ var MAX_CONVERSATION_SESSION_LIST_LIMIT = 500;
13968
+ var MAX_CONVERSATION_SESSION_LIST_OFFSET = 1e5;
13969
+ function boundedSessionListInteger(value, name, fallback, minimum, maximum) {
13970
+ if (value === void 0) return fallback;
13971
+ if (!Number.isInteger(value) || value < minimum || value > maximum) {
13972
+ throw new RangeError(
13973
+ `Session list ${name} must be an integer between ${minimum} and ${maximum}.`
13974
+ );
13975
+ }
13976
+ return value;
13977
+ }
13803
13978
  var STANDARD_MODULES_OPERATIONS = [
13804
13979
  {
13805
13980
  create: "entity",
@@ -14017,7 +14192,8 @@ function normalizeEnvironmentSetupSummary(setup) {
14017
14192
  environmentId: String(setup.environmentId || ""),
14018
14193
  sandboxId: String(setup.sandboxId || ""),
14019
14194
  subjectId: String(setup.subjectId || ""),
14020
- triggerReason: setup.triggerReason === "fresh_after_version_update" ? "fresh_after_version_update" : "new_environment",
14195
+ triggerReason: setup.triggerReason === "fresh_after_version_update" ? "fresh_after_version_update" : setup.triggerReason === "explicit_reset" ? "explicit_reset" : "new_environment",
14196
+ operationKey: typeof setup.operationKey === "string" ? setup.operationKey : null,
14021
14197
  lifecycleStatus: setup.lifecycleStatus === "completed" || setup.lifecycleStatus === "failed" ? setup.lifecycleStatus : "running",
14022
14198
  stage: typeof setup.stage === "string" ? setup.stage : null,
14023
14199
  totalObjectsToImport: Number(setup.totalObjectsToImport || 0),
@@ -14138,7 +14314,7 @@ var Environment = class _Environment {
14138
14314
  }
14139
14315
  get sessions() {
14140
14316
  return {
14141
- list: async (options) => this.listSessions(options?.status || "active"),
14317
+ list: async (options = {}) => this.listSessions(options),
14142
14318
  create: async (options) => this.createSession(options),
14143
14319
  connect: async (sessionId, options) => this.connectSession(sessionId, options),
14144
14320
  reopen: async (sessionId, options) => this.reopenSession(sessionId, options),
@@ -14273,17 +14449,12 @@ var Environment = class _Environment {
14273
14449
  */
14274
14450
  async disconnect() {
14275
14451
  }
14276
- async listSessions(status = "active") {
14277
- if (status === "all") {
14278
- const [active, closed] = await Promise.all([
14279
- this.granular.listOpenSessions({ environmentId: this.environmentId }),
14280
- this.granular.listClosedSessions({ environmentId: this.environmentId })
14281
- ]);
14282
- return [...active, ...closed].sort(
14283
- (left, right) => Date.parse(right.lastSeenAt) - Date.parse(left.lastSeenAt)
14284
- );
14285
- }
14286
- return status === "closed" ? this.granular.listClosedSessions({ environmentId: this.environmentId }) : this.granular.listOpenSessions({ environmentId: this.environmentId });
14452
+ async listSessions(optionsOrStatus = {}) {
14453
+ const options = typeof optionsOrStatus === "string" ? { status: optionsOrStatus } : optionsOrStatus;
14454
+ return this.granular.listSessions({
14455
+ ...options,
14456
+ environmentId: this.environmentId
14457
+ });
14287
14458
  }
14288
14459
  async getUserEnvironmentState(options = {}) {
14289
14460
  return this.granular.getUserEnvironmentState({
@@ -14301,6 +14472,7 @@ var Environment = class _Environment {
14301
14472
  return this.granular.createSession({
14302
14473
  environmentId: this.environmentId,
14303
14474
  clientId: options?.clientId,
14475
+ sessionScope: options?.sessionScope,
14304
14476
  initialHeap: options?.initialHeap
14305
14477
  });
14306
14478
  }
@@ -15303,6 +15475,7 @@ var Environment = class _Environment {
15303
15475
  records: recordsToImport,
15304
15476
  batchSize: options.batchSize,
15305
15477
  setupRunId: options.setupRunId,
15478
+ operationKey: options.operationKey,
15306
15479
  writeMode: options.writeMode
15307
15480
  })
15308
15481
  }
@@ -15863,7 +16036,7 @@ var EnvironmentSession = class extends Session {
15863
16036
  * Close only the socket transport without sending `client.goodbye`.
15864
16037
  */
15865
16038
  disconnectTransport() {
15866
- this.client.disconnect();
16039
+ this.client.disconnect({ reason: "Transport detach" });
15867
16040
  }
15868
16041
  /**
15869
16042
  * Backwards-compatible alias for `disconnect()`.
@@ -16107,6 +16280,107 @@ var Granular = class _Granular {
16107
16280
  await this.maybeRunEnvironmentImporter(resolved, environment);
16108
16281
  return environment;
16109
16282
  }
16283
+ /**
16284
+ * Read the active environment selected by Granular for an already-recorded
16285
+ * external user. This is intentionally read-only: browser/login code must
16286
+ * not create subjects or environments as a side effect.
16287
+ */
16288
+ async getActiveEnvironmentForUser(options) {
16289
+ const sandboxId = options.sandboxId.trim();
16290
+ const tagName = options.tag.trim();
16291
+ const userId = options.userId.trim();
16292
+ if (!sandboxId || !tagName || !userId) {
16293
+ throw new Error(
16294
+ "getActiveEnvironmentForUser() requires sandboxId, tag, and userId."
16295
+ );
16296
+ }
16297
+ const subjects = await this.request(
16298
+ `/control/subjects?identityId=${encodeURIComponent(userId)}`
16299
+ );
16300
+ const subject = (subjects.items || []).find(
16301
+ (item) => item.identityId === userId || item.userId === userId
16302
+ );
16303
+ if (!subject?.subjectId && !subject?.granularId) {
16304
+ return null;
16305
+ }
16306
+ const subjectId = subject.subjectId || subject.granularId;
16307
+ const tags = await this.request(
16308
+ `/control/sandboxes/${encodeURIComponent(sandboxId)}/tags`
16309
+ );
16310
+ const tag = (tags.items || []).find(
16311
+ (item) => item?.name === tagName
16312
+ );
16313
+ if (!tag) return null;
16314
+ const query = new URLSearchParams({
16315
+ tagId: tag.tagId,
16316
+ slot: options.slot?.trim() || "default"
16317
+ });
16318
+ try {
16319
+ const payload = await this.request(
16320
+ `/control/sandboxes/${encodeURIComponent(sandboxId)}/subjects/${encodeURIComponent(subjectId)}/active-environment?${query.toString()}`
16321
+ );
16322
+ return payload.environment ? this.bindEnvironmentHandle(
16323
+ normalizeEnvironmentData(payload.environment)
16324
+ ) : null;
16325
+ } catch (error) {
16326
+ const message = error instanceof Error ? error.message : String(error);
16327
+ if (message.includes("404") || message.includes("not found")) {
16328
+ return null;
16329
+ }
16330
+ throw error;
16331
+ }
16332
+ }
16333
+ /**
16334
+ * Register one reviewed, pre-existing environment as the active workspace
16335
+ * for an external user. This is for a controlled migration only: it does
16336
+ * not create an environment and it does not run an importer.
16337
+ */
16338
+ async adoptEnvironmentForUser(options) {
16339
+ const sandboxId = options.sandboxId.trim();
16340
+ const tagName = options.tag.trim();
16341
+ const userId = options.userId.trim();
16342
+ const environmentId = options.environmentId.trim();
16343
+ if (!sandboxId || !tagName || !userId || !environmentId) {
16344
+ throw new Error(
16345
+ "adoptEnvironmentForUser() requires sandboxId, tag, userId, and environmentId."
16346
+ );
16347
+ }
16348
+ const subjects = await this.request(
16349
+ `/control/subjects?identityId=${encodeURIComponent(userId)}`
16350
+ );
16351
+ const subject = (subjects.items || []).find(
16352
+ (item) => item.identityId === userId || item.userId === userId
16353
+ );
16354
+ if (!subject?.subjectId && !subject?.granularId) {
16355
+ throw new Error(`No Granular subject exists for user ${userId}.`);
16356
+ }
16357
+ const tags = await this.request(`/control/sandboxes/${encodeURIComponent(sandboxId)}/tags`);
16358
+ const tag = (tags.items || []).find(
16359
+ (item) => item?.name === tagName
16360
+ );
16361
+ if (!tag) {
16362
+ throw new Error(`Tag ${tagName} was not found in sandbox ${sandboxId}.`);
16363
+ }
16364
+ const payload = await this.request(
16365
+ "/control/environment-activations/adopt",
16366
+ {
16367
+ method: "POST",
16368
+ body: JSON.stringify({
16369
+ environmentId,
16370
+ subjectId: subject.subjectId || subject.granularId,
16371
+ tagId: tag.tagId,
16372
+ slot: options.slot?.trim() || "default",
16373
+ confirmExistingData: true
16374
+ })
16375
+ }
16376
+ );
16377
+ if (!payload.environment) {
16378
+ throw new Error("Granular did not return the adopted environment.");
16379
+ }
16380
+ return this.bindEnvironmentHandle(
16381
+ normalizeEnvironmentData(payload.environment)
16382
+ );
16383
+ }
16110
16384
  /**
16111
16385
  * Deprecated compatibility alias for `openEnvironment()`.
16112
16386
  *
@@ -16138,7 +16412,9 @@ var Granular = class _Granular {
16138
16412
  requestedOntology,
16139
16413
  sandboxId: environmentData.sandboxId,
16140
16414
  subjectId: environmentData.subjectId,
16141
- setupTriggerReason: options.reason || "new_environment"
16415
+ externalUserId: environmentData.subjectId,
16416
+ setupTriggerReason: options.reason || "new_environment",
16417
+ setupOperationKey: options.operationKey
16142
16418
  },
16143
16419
  environment
16144
16420
  );
@@ -16150,20 +16426,17 @@ var Granular = class _Granular {
16150
16426
  }
16151
16427
  return tag;
16152
16428
  }
16153
- buildManagedEnvironmentName(tag, versionId) {
16154
- return `__sdk__${tag}__${versionId}__pinned`;
16429
+ buildManagedEnvironmentName(tag, versionId, resetKey) {
16430
+ if (!resetKey) {
16431
+ return `__sdk__${tag}__${versionId}__tracked`;
16432
+ }
16433
+ const safeResetKey = resetKey.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 80);
16434
+ return `__sdk__${tag}__${versionId}__reset__${safeResetKey}`;
16155
16435
  }
16156
16436
  isManagedEnvironmentName(environment, tagName) {
16157
16437
  const name = environment.environment || environment.envName || "";
16158
16438
  return name.startsWith(`__sdk__${tagName}__`);
16159
16439
  }
16160
- isPinnedToVersion(environment, versionId) {
16161
- return environment.buildPolicy.mode === "pinned" && (environment.versionId === versionId || environment.buildPolicy.versionId === versionId || environment.buildPolicy.buildId === versionId);
16162
- }
16163
- matchesTagTrackedEnvironment(environment, tagName, tagId) {
16164
- const environmentTagName = environment.tag?.name || environment.buildPolicy.tagName || null;
16165
- 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);
16166
- }
16167
16440
  sortEnvironmentsByRecency(environments) {
16168
16441
  return [...environments].sort(
16169
16442
  (left, right) => right.updatedAt - left.updatedAt
@@ -16213,61 +16486,187 @@ var Granular = class _Granular {
16213
16486
  `Tag "${tagName}" does not currently point to a build/version.`
16214
16487
  );
16215
16488
  }
16489
+ const slot = options.slot?.trim() || "default";
16490
+ const resetKey = options.resetKey?.trim() || void 0;
16491
+ const resolveActive = async (operationKey) => {
16492
+ const query = new URLSearchParams({ tagId: tag.tagId, slot });
16493
+ if (operationKey) query.set("operationKey", operationKey);
16494
+ try {
16495
+ const payload = await this.request(
16496
+ `/control/sandboxes/${encodeURIComponent(sandbox.sandboxId)}/subjects/${encodeURIComponent(user.granularId)}/active-environment?${query.toString()}`
16497
+ );
16498
+ return payload.environment ? normalizeEnvironmentData(payload.environment) : null;
16499
+ } catch (error) {
16500
+ const message = error instanceof Error ? error.message : String(error);
16501
+ if (message.includes("404") || message.includes("not found")) {
16502
+ return null;
16503
+ }
16504
+ throw error;
16505
+ }
16506
+ };
16507
+ const activate = async (environment2) => {
16508
+ const payload = await this.request(
16509
+ "/control/environment-activations",
16510
+ {
16511
+ method: "POST",
16512
+ body: JSON.stringify({
16513
+ environmentId: environment2.environmentId,
16514
+ tagId: tag.tagId,
16515
+ slot,
16516
+ operationKey: resetKey,
16517
+ operation: resetKey ? "explicit_reset" : void 0
16518
+ })
16519
+ }
16520
+ );
16521
+ return normalizeEnvironmentData(payload.environment);
16522
+ };
16523
+ if (resetKey) {
16524
+ const resetEnvironment = await resolveActive(resetKey);
16525
+ if (resetEnvironment) {
16526
+ return {
16527
+ environment: resetEnvironment,
16528
+ requestedOntology: ontology,
16529
+ sandboxId: sandbox.sandboxId,
16530
+ subjectId: user.granularId,
16531
+ externalUserId: user.userId,
16532
+ // Retrying an explicit reset must also resume its durable setup run.
16533
+ // Otherwise a Container crash after queue submission would leave a
16534
+ // valid environment permanently marked as "running".
16535
+ setupTriggerReason: "explicit_reset",
16536
+ setupOperationKey: resetKey
16537
+ };
16538
+ }
16539
+ } else {
16540
+ const active = await resolveActive();
16541
+ if (active && (active.versionId === targetVersionId || options.createFreshIfOutdated !== true)) {
16542
+ return {
16543
+ environment: active,
16544
+ requestedOntology: ontology,
16545
+ sandboxId: sandbox.sandboxId,
16546
+ subjectId: user.granularId,
16547
+ externalUserId: user.userId
16548
+ };
16549
+ }
16550
+ }
16216
16551
  const allEnvironments = await this.environments.list(sandbox.sandboxId);
16217
16552
  const userEnvironments = allEnvironments.filter(
16218
- (environment) => environment.subjectId === user.granularId
16553
+ (environment2) => environment2.subjectId === user.granularId
16219
16554
  );
16220
16555
  const currentMatches = this.sortEnvironmentsByRecency(
16221
16556
  userEnvironments.filter(
16222
- (environment) => this.matchesTagTrackedEnvironment(environment, tagName, tag.tagId) && environment.versionId === targetVersionId && (!this.isManagedEnvironmentName(environment, tagName) || this.isPinnedToVersion(environment, targetVersionId))
16557
+ (environment2) => environment2.tagId === tag.tagId && environment2.versionId === targetVersionId
16223
16558
  )
16224
16559
  );
16225
- if (currentMatches.length > 0) {
16560
+ if (!resetKey && currentMatches.length > 0) {
16561
+ const environment2 = await activate(currentMatches[0]);
16226
16562
  return {
16227
- environment: currentMatches[0],
16563
+ environment: environment2,
16228
16564
  requestedOntology: ontology,
16229
16565
  sandboxId: sandbox.sandboxId,
16230
- subjectId: user.granularId
16566
+ subjectId: user.granularId,
16567
+ externalUserId: user.userId
16231
16568
  };
16232
16569
  }
16233
16570
  const outdatedMatches = this.sortEnvironmentsByRecency(
16234
- userEnvironments.filter(
16235
- (environment) => this.matchesTagTrackedEnvironment(environment, tagName, tag.tagId)
16236
- )
16571
+ userEnvironments.filter((environment2) => environment2.tagId === tag.tagId)
16237
16572
  );
16238
16573
  if (outdatedMatches.length > 0 && options.createFreshIfOutdated !== true) {
16574
+ const environment2 = await activate(outdatedMatches[0]);
16239
16575
  return {
16240
- environment: outdatedMatches[0],
16576
+ environment: environment2,
16241
16577
  requestedOntology: ontology,
16242
16578
  sandboxId: sandbox.sandboxId,
16243
- subjectId: user.granularId
16579
+ subjectId: user.granularId,
16580
+ externalUserId: user.userId
16244
16581
  };
16245
16582
  }
16583
+ const created = await this.environments.create(sandbox.sandboxId, {
16584
+ subjectId: user.granularId,
16585
+ environment: this.buildManagedEnvironmentName(
16586
+ tagName,
16587
+ targetVersionId,
16588
+ resetKey
16589
+ ),
16590
+ tagId: tag.tagId,
16591
+ permissionProfileId: null
16592
+ });
16593
+ const environment = await activate(created);
16246
16594
  return {
16247
- environment: await this.environments.create(sandbox.sandboxId, {
16248
- subjectId: user.granularId,
16249
- environment: this.buildManagedEnvironmentName(tagName, targetVersionId),
16250
- tagId: tag.tagId,
16251
- versionId: targetVersionId,
16252
- permissionProfileId: null
16253
- }),
16595
+ environment,
16254
16596
  requestedOntology: ontology,
16255
16597
  sandboxId: sandbox.sandboxId,
16256
16598
  subjectId: user.granularId,
16257
- setupTriggerReason: outdatedMatches.length > 0 ? "fresh_after_version_update" : "new_environment"
16599
+ externalUserId: user.userId,
16600
+ setupTriggerReason: resetKey ? "explicit_reset" : outdatedMatches.length > 0 ? "fresh_after_version_update" : "new_environment",
16601
+ setupOperationKey: resetKey
16258
16602
  };
16259
16603
  }
16260
16604
  /**
16261
- * List active (open) sessions for an environment each session is one agent conversation thread.
16605
+ * List indexed sessions using ownership filters and bounded pagination.
16606
+ */
16607
+ async listSessions(options) {
16608
+ const environmentId = options.environmentId?.trim();
16609
+ const sandboxId = options.sandboxId?.trim();
16610
+ const subjectId = options.subjectId?.trim();
16611
+ if (!environmentId && !sandboxId && !subjectId) {
16612
+ throw new Error(
16613
+ "listSessions() requires environmentId, sandboxId, or subjectId so history cannot be scanned accidentally."
16614
+ );
16615
+ }
16616
+ const status = options.status || "active";
16617
+ const allowedStatuses = /* @__PURE__ */ new Set([
16618
+ "active",
16619
+ "closed",
16620
+ "expired",
16621
+ "failed",
16622
+ "timeout",
16623
+ "all"
16624
+ ]);
16625
+ if (!allowedStatuses.has(status)) {
16626
+ throw new Error(`Unsupported session status: ${String(status)}`);
16627
+ }
16628
+ const limit = boundedSessionListInteger(
16629
+ options.limit,
16630
+ "limit",
16631
+ DEFAULT_CONVERSATION_SESSION_LIST_LIMIT,
16632
+ 1,
16633
+ MAX_CONVERSATION_SESSION_LIST_LIMIT
16634
+ );
16635
+ const offset = boundedSessionListInteger(
16636
+ options.offset,
16637
+ "offset",
16638
+ 0,
16639
+ 0,
16640
+ MAX_CONVERSATION_SESSION_LIST_OFFSET
16641
+ );
16642
+ const query = new URLSearchParams({
16643
+ limit: String(limit),
16644
+ offset: String(offset)
16645
+ });
16646
+ if (environmentId) query.set("environmentId", environmentId);
16647
+ if (sandboxId) query.set("sandboxId", sandboxId);
16648
+ if (subjectId) query.set("userId", subjectId);
16649
+ if (options.sessionScope?.trim()) {
16650
+ query.set("sessionScope", options.sessionScope.trim());
16651
+ }
16652
+ if (status !== "all") query.set("status", status);
16653
+ const res = await this.request(
16654
+ `/control/sessions?${query.toString()}`
16655
+ );
16656
+ const items = Array.isArray(res.items) ? res.items : [];
16657
+ return items.map((row) => this.normalizeConversationSession(row));
16658
+ }
16659
+ /**
16660
+ * List active (open) sessions for an environment.
16262
16661
  */
16263
16662
  async listOpenSessions(filters) {
16264
- return this.listSessionsForEnvironment(filters.environmentId, "active");
16663
+ return this.listSessions({ ...filters, status: "active" });
16265
16664
  }
16266
16665
  /**
16267
16666
  * List closed sessions for an environment (conversations that have disconnected).
16268
16667
  */
16269
16668
  async listClosedSessions(filters) {
16270
- return this.listSessionsForEnvironment(filters.environmentId, "closed");
16669
+ return this.listSessions({ ...filters, status: "closed" });
16271
16670
  }
16272
16671
  async getUserEnvironmentState(options) {
16273
16672
  const query = new URLSearchParams({
@@ -16302,14 +16701,6 @@ var Granular = class _Granular {
16302
16701
  });
16303
16702
  return result.readAtBySessionId || {};
16304
16703
  }
16305
- async listSessionsForEnvironment(environmentId, status) {
16306
- const query = new URLSearchParams({ environmentId, status });
16307
- const res = await this.request(
16308
- `/control/sessions?${query.toString()}`
16309
- );
16310
- const items = Array.isArray(res.items) ? res.items : [];
16311
- return items.map((row) => this.normalizeConversationSession(row));
16312
- }
16313
16704
  normalizeConversationSession(row) {
16314
16705
  const sessionId = String(row.sessionId ?? row.session_id ?? "");
16315
16706
  const environmentId = String(row.environmentId ?? row.environment_id ?? "");
@@ -16368,6 +16759,7 @@ var Granular = class _Granular {
16368
16759
  */
16369
16760
  async createSession(options) {
16370
16761
  const clientId = options.clientId || `client_${Date.now()}`;
16762
+ const sessionScope = options.sessionScope?.trim() || void 0;
16371
16763
  await this.activateEnvironment(options.environmentId);
16372
16764
  const envData = await this.environments.get(options.environmentId);
16373
16765
  const environment = this.bindEnvironmentHandle(envData);
@@ -16376,6 +16768,8 @@ var Granular = class _Granular {
16376
16768
  body: JSON.stringify({
16377
16769
  environmentId: options.environmentId,
16378
16770
  clientId,
16771
+ sessionScope,
16772
+ capabilities: sessionScope ? { sessionScope } : void 0,
16379
16773
  initialHeap: options.initialHeap
16380
16774
  })
16381
16775
  });
@@ -16483,11 +16877,24 @@ var Granular = class _Granular {
16483
16877
  {
16484
16878
  method: "POST",
16485
16879
  body: JSON.stringify({
16486
- triggerReason: resolved.setupTriggerReason
16880
+ triggerReason: resolved.setupTriggerReason,
16881
+ operationKey: resolved.setupOperationKey
16487
16882
  })
16488
16883
  }
16489
16884
  );
16490
16885
  const setupRunId = setupRun.setupRunId;
16886
+ let claim = null;
16887
+ for (let attempt = 0; attempt < 3; attempt += 1) {
16888
+ claim = await this.request(
16889
+ `/control/environment-setup-runs/${setupRunId}/importer-claim`,
16890
+ { method: "POST", body: JSON.stringify({}) }
16891
+ );
16892
+ if (claim.action !== "busy") break;
16893
+ await sleep(Math.min(3e4, Math.max(250, claim.retryAfterMs || 1e3)));
16894
+ }
16895
+ if (!claim) {
16896
+ throw new Error(`Unable to claim environment setup run ${setupRunId}.`);
16897
+ }
16491
16898
  const updateSetupRun = async (patch) => {
16492
16899
  await this.request(
16493
16900
  `/control/environment-setup-runs/${setupRunId}`,
@@ -16497,10 +16904,26 @@ var Granular = class _Granular {
16497
16904
  }
16498
16905
  );
16499
16906
  };
16907
+ if (claim.action === "submitted") {
16908
+ const completedSetupRun = await this.request(
16909
+ `/control/environment-setup-runs/${setupRunId}`,
16910
+ { method: "PATCH", body: JSON.stringify({ markHookCompleted: true }) }
16911
+ );
16912
+ const refreshedEnvironment = await this.environments.get(
16913
+ environment.environmentId
16914
+ );
16915
+ environment.syncEnvironmentData(refreshedEnvironment);
16916
+ return completedSetupRun;
16917
+ }
16918
+ if (claim.action === "busy" || claim.action === "terminal") {
16919
+ return claim.summary;
16920
+ }
16921
+ let importSequence = 0;
16500
16922
  const importerContext = {
16501
16923
  environmentId: environment.environmentId,
16502
16924
  sandboxId: environment.sandboxId,
16503
16925
  subjectId: environment.subjectId,
16926
+ externalUserId: resolved.externalUserId,
16504
16927
  reason: resolved.setupTriggerReason,
16505
16928
  incrementTotalObjectsToImportCount: async (n) => {
16506
16929
  const safeIncrement = Math.max(0, Math.trunc(n));
@@ -16517,7 +16940,10 @@ var Granular = class _Granular {
16517
16940
  importRecords: async (records, options) => environment.enqueueRecordImport(records, {
16518
16941
  batchSize: options?.batchSize,
16519
16942
  writeMode: options?.writeMode,
16520
- setupRunId
16943
+ setupRunId,
16944
+ // Sequence is deterministic for a retry of one importer hook. It
16945
+ // prevents a Container restart from creating a second queue import.
16946
+ operationKey: `${setupRunId}:import:${importSequence++}`
16521
16947
  })
16522
16948
  };
16523
16949
  try {
@@ -19556,6 +19982,7 @@ function buildGranularAgentSystemPrompt(input) {
19556
19982
  - \`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(...)\`.
19557
19983
  - 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.
19558
19984
  - 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()\`.
19985
+ - 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.
19559
19986
  - 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.
19560
19987
  - 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.
19561
19988
  - 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.
@@ -19826,11 +20253,12 @@ ${actionIndex}
19826
20253
  - 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.
19827
20254
  - 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()\`.
19828
20255
  - 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.
19829
- - 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.
20256
+ - 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.
20257
+ - 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.
19830
20258
  - 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.
19831
20259
  - 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.
19832
20260
  - 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.
19833
- - 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.
20261
+ - 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.
19834
20262
  - 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.
19835
20263
  - 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.
19836
20264
  - 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.
@@ -20022,8 +20450,9 @@ function resolveHarnessTemplate(templateId = "stable", options) {
20022
20450
  }
20023
20451
 
20024
20452
  // src/openai-usage.ts
20025
- var OPENAI_PRICING_SOURCE_URL = "https://developers.openai.com/api/docs/models/gpt-5.4/";
20026
- var OPENAI_PRICING_EFFECTIVE_DATE = "2026-05-19";
20453
+ var OPENAI_GPT_5_4_PRICING_SOURCE_URL = "https://developers.openai.com/api/docs/models/gpt-5.4/";
20454
+ var OPENAI_GPT_5_6_PRICING_SOURCE_URL = "https://developers.openai.com/api/docs/pricing";
20455
+ var OPENAI_LONG_CONTEXT_THRESHOLD_TOKENS = 272e3;
20027
20456
  var OPENAI_MODEL_PRICING_USD_PER_MILLION = {
20028
20457
  "gpt-5.4": {
20029
20458
  provider: "openai",
@@ -20032,8 +20461,26 @@ var OPENAI_MODEL_PRICING_USD_PER_MILLION = {
20032
20461
  inputUsdPerMillion: 2.5,
20033
20462
  cachedInputUsdPerMillion: 0.25,
20034
20463
  outputUsdPerMillion: 15,
20035
- sourceUrl: OPENAI_PRICING_SOURCE_URL,
20036
- effectiveDate: OPENAI_PRICING_EFFECTIVE_DATE
20464
+ sourceUrl: OPENAI_GPT_5_4_PRICING_SOURCE_URL,
20465
+ effectiveDate: "2026-05-19"
20466
+ },
20467
+ "gpt-5.6-luna": {
20468
+ provider: "openai",
20469
+ model: "gpt-5.6-luna",
20470
+ currency: "USD",
20471
+ inputUsdPerMillion: 1,
20472
+ cachedInputUsdPerMillion: 0.1,
20473
+ cacheWriteUsdPerMillion: 1.25,
20474
+ outputUsdPerMillion: 6,
20475
+ sourceUrl: OPENAI_GPT_5_6_PRICING_SOURCE_URL,
20476
+ effectiveDate: "2026-07-11",
20477
+ longContextThresholdTokens: OPENAI_LONG_CONTEXT_THRESHOLD_TOKENS,
20478
+ longContextPricing: {
20479
+ inputUsdPerMillion: 2,
20480
+ cachedInputUsdPerMillion: 0.2,
20481
+ cacheWriteUsdPerMillion: 2.5,
20482
+ outputUsdPerMillion: 9
20483
+ }
20037
20484
  }
20038
20485
  };
20039
20486
  function asRecord5(value) {
@@ -20046,8 +20493,18 @@ function numberField(record, key) {
20046
20493
  function microsPerMillion(usdPerMillion) {
20047
20494
  return Math.round(usdPerMillion * 1e6);
20048
20495
  }
20049
- function getOpenAIModelPricing(model) {
20050
- return OPENAI_MODEL_PRICING_USD_PER_MILLION[model] || null;
20496
+ function getOpenAIModelPricing(model, inputTokens = 0) {
20497
+ const pricing = OPENAI_MODEL_PRICING_USD_PER_MILLION[model];
20498
+ if (!pricing) return null;
20499
+ const threshold = pricing.longContextThresholdTokens ?? null;
20500
+ if (pricing.longContextPricing && typeof threshold === "number" && inputTokens > threshold) {
20501
+ return {
20502
+ ...pricing,
20503
+ ...pricing.longContextPricing,
20504
+ contextTier: "long"
20505
+ };
20506
+ }
20507
+ return { ...pricing, contextTier: "short" };
20051
20508
  }
20052
20509
  function normalizeOpenAIUsage(rawUsage) {
20053
20510
  const usage = asRecord5(rawUsage);
@@ -20055,6 +20512,7 @@ function normalizeOpenAIUsage(rawUsage) {
20055
20512
  return {
20056
20513
  inputTokens: 0,
20057
20514
  cachedInputTokens: 0,
20515
+ cacheWriteTokens: 0,
20058
20516
  uncachedInputTokens: 0,
20059
20517
  outputTokens: 0,
20060
20518
  reasoningTokens: 0,
@@ -20070,20 +20528,28 @@ function normalizeOpenAIUsage(rawUsage) {
20070
20528
  inputTokens,
20071
20529
  numberField(inputDetails, "cached_tokens") || numberField(inputDetails, "cached_input_tokens")
20072
20530
  );
20531
+ const cacheWriteTokens = Math.min(
20532
+ Math.max(inputTokens - cachedInputTokens, 0),
20533
+ numberField(inputDetails, "cache_write_tokens")
20534
+ );
20073
20535
  const reasoningTokens = numberField(outputDetails, "reasoning_tokens") || numberField(outputDetails, "reasoning_output_tokens");
20074
20536
  return {
20075
20537
  inputTokens,
20076
20538
  cachedInputTokens,
20077
- uncachedInputTokens: Math.max(inputTokens - cachedInputTokens, 0),
20539
+ cacheWriteTokens,
20540
+ uncachedInputTokens: Math.max(
20541
+ inputTokens - cachedInputTokens - cacheWriteTokens,
20542
+ 0
20543
+ ),
20078
20544
  outputTokens,
20079
20545
  reasoningTokens,
20080
20546
  totalTokens
20081
20547
  };
20082
20548
  }
20083
20549
  function calculateOpenAITokenSpend(model, rawUsage) {
20084
- const pricing = getOpenAIModelPricing(model);
20085
- if (!pricing) return null;
20086
20550
  const usage = normalizeOpenAIUsage(rawUsage);
20551
+ const pricing = getOpenAIModelPricing(model, usage.inputTokens);
20552
+ if (!pricing) return null;
20087
20553
  const inputPricePerMillionMicros = microsPerMillion(
20088
20554
  pricing.inputUsdPerMillion
20089
20555
  );
@@ -20093,14 +20559,19 @@ function calculateOpenAITokenSpend(model, rawUsage) {
20093
20559
  const outputPricePerMillionMicros = microsPerMillion(
20094
20560
  pricing.outputUsdPerMillion
20095
20561
  );
20562
+ const cacheWritePricePerMillionMicros = typeof pricing.cacheWriteUsdPerMillion === "number" ? microsPerMillion(pricing.cacheWriteUsdPerMillion) : null;
20563
+ const cacheWriteCostMicros = Math.round(
20564
+ usage.cacheWriteTokens * (cacheWritePricePerMillionMicros ?? inputPricePerMillionMicros) / 1e6
20565
+ );
20096
20566
  const amountMicros = Math.round(
20097
- (usage.uncachedInputTokens * inputPricePerMillionMicros + usage.cachedInputTokens * cachedInputPricePerMillionMicros + usage.outputTokens * outputPricePerMillionMicros) / 1e6
20567
+ (usage.uncachedInputTokens * inputPricePerMillionMicros + usage.cachedInputTokens * cachedInputPricePerMillionMicros + usage.cacheWriteTokens * (cacheWritePricePerMillionMicros ?? inputPricePerMillionMicros) + usage.outputTokens * outputPricePerMillionMicros) / 1e6
20098
20568
  );
20099
20569
  return {
20100
20570
  provider: "openai",
20101
20571
  model,
20102
20572
  inputTokens: usage.inputTokens,
20103
20573
  cachedInputTokens: usage.cachedInputTokens,
20574
+ cacheWriteTokens: usage.cacheWriteTokens,
20104
20575
  uncachedInputTokens: usage.uncachedInputTokens,
20105
20576
  outputTokens: usage.outputTokens,
20106
20577
  reasoningTokens: usage.reasoningTokens,
@@ -20109,7 +20580,11 @@ function calculateOpenAITokenSpend(model, rawUsage) {
20109
20580
  currency: "USD",
20110
20581
  inputPricePerMillionMicros,
20111
20582
  cachedInputPricePerMillionMicros,
20583
+ cacheWritePricePerMillionMicros,
20584
+ cacheWriteCostMicros,
20112
20585
  outputPricePerMillionMicros,
20586
+ pricingContextTier: pricing.contextTier || "short",
20587
+ longContextThresholdTokens: pricing.longContextThresholdTokens ?? null,
20113
20588
  pricingSource: pricing.sourceUrl,
20114
20589
  pricingEffectiveAt: pricing.effectiveDate,
20115
20590
  usage