@ferricstore/ferricstore 0.12.0 → 0.12.1

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.
Files changed (52) hide show
  1. package/README.md +3 -0
  2. package/dist/langgraph.cjs +395 -125
  3. package/dist/langgraph.cjs.map +1 -1
  4. package/dist/langgraph.d.cts +17 -7
  5. package/dist/langgraph.d.ts +17 -7
  6. package/dist/langgraph.js +396 -126
  7. package/dist/langgraph.js.map +1 -1
  8. package/dist/openai-agents.cjs +161 -24
  9. package/dist/openai-agents.cjs.map +1 -1
  10. package/dist/openai-agents.d.cts +10 -4
  11. package/dist/openai-agents.d.ts +10 -4
  12. package/dist/openai-agents.js +161 -24
  13. package/dist/openai-agents.js.map +1 -1
  14. package/docs/agent-api/assets/hierarchy.js +1 -0
  15. package/docs/agent-api/assets/highlight.css +92 -0
  16. package/docs/agent-api/assets/icons.js +18 -0
  17. package/docs/agent-api/assets/icons.svg +1 -0
  18. package/docs/agent-api/assets/main.js +60 -0
  19. package/docs/agent-api/assets/navigation.js +1 -0
  20. package/docs/agent-api/assets/search.js +1 -0
  21. package/docs/agent-api/assets/style.css +1648 -0
  22. package/docs/agent-api/classes/langgraph.FerricStoreSaver.html +297 -0
  23. package/docs/agent-api/classes/langgraph.FerricStoreStore.html +298 -0
  24. package/docs/agent-api/classes/langgraph.LangGraphFlow.html +190 -0
  25. package/docs/agent-api/classes/langgraph.LangGraphFlowContext.html +158 -0
  26. package/docs/agent-api/classes/langgraph.LangGraphFlowRun.html +133 -0
  27. package/docs/agent-api/classes/openai-agents.FerricStoreSession.html +241 -0
  28. package/docs/agent-api/hierarchy.html +44 -0
  29. package/docs/agent-api/index.html +142 -0
  30. package/docs/agent-api/interfaces/langgraph.FerricFlowHandlerContext.html +94 -0
  31. package/docs/agent-api/interfaces/langgraph.FerricStoreCommandClient.html +77 -0
  32. package/docs/agent-api/interfaces/langgraph.FerricStoreLockOptions.html +81 -0
  33. package/docs/agent-api/interfaces/langgraph.FerricStoreSaverOptions.html +106 -0
  34. package/docs/agent-api/interfaces/langgraph.FerricStoreStoreOptions.html +98 -0
  35. package/docs/agent-api/interfaces/langgraph.InvokableLangGraph.html +83 -0
  36. package/docs/agent-api/interfaces/langgraph.LangGraphFlowOptions.html +116 -0
  37. package/docs/agent-api/interfaces/openai-agents.FerricStoreSessionOptions.html +114 -0
  38. package/docs/agent-api/interfaces/openai-agents.Session.html +208 -0
  39. package/docs/agent-api/interfaces/openai-agents.SessionHistoryRewriteAwareSession.html +230 -0
  40. package/docs/agent-api/interfaces/openai-agents.SessionHistoryTransactionAwareSession.html +237 -0
  41. package/docs/agent-api/modules/langgraph.html +44 -0
  42. package/docs/agent-api/modules/openai-agents.html +44 -0
  43. package/docs/agent-api/modules.html +35 -0
  44. package/docs/agent-api/types/langgraph.LangGraphChannelVersions.html +33 -0
  45. package/docs/agent-api/types/langgraph.LangGraphInvocationConfig.html +33 -0
  46. package/docs/agent-api/types/langgraph.LangGraphOutcomeMapper.html +50 -0
  47. package/docs/agent-api/types/langgraph.LangGraphPendingWrite.html +33 -0
  48. package/docs/agent-api/types/openai-agents.AgentInputItem.html +35 -0
  49. package/docs/agent-frameworks.md +25 -8
  50. package/docs/api/index.html +4 -1
  51. package/docs/api/media/agent-frameworks.md +25 -8
  52. package/package.json +3 -3
@@ -10,14 +10,17 @@ interface FerricStoreSessionOptions extends FerricStoreLockOptions {
10
10
  initialItems?: AgentInputItem[];
11
11
  /** FerricStore key prefix. Defaults to `openai:agents:session`. */
12
12
  keyPrefix?: string;
13
+ /** Previous worker locales to accept when migrating unversioned operation receipts. */
14
+ legacyReceiptLocales?: string[];
13
15
  }
14
16
  /**
15
17
  * Durable OpenAI Agents SDK conversation history backed by FerricStore.
16
18
  *
17
- * Every mutation is serialized by an ownership-checked, renewable FerricStore
18
- * lock. History transactions and their operation receipts are persisted in one
19
- * atomic hash-field write, implementing the SDK's retry-safe transaction
20
- * capability in addition to its base Session contract.
19
+ * Renewable locks reduce contention, while compare-and-swap makes every state
20
+ * commit safe even if a writer's lease expires in flight. History transactions
21
+ * and their operation receipts are persisted in one atomic value, implementing
22
+ * the SDK's retry-safe transaction capability in addition to its base Session
23
+ * contract.
21
24
  */
22
25
  declare class FerricStoreSession implements Session, SessionHistoryRewriteAwareSession, SessionHistoryTransactionAwareSession {
23
26
  readonly client: FerricStoreCommandClient;
@@ -25,7 +28,9 @@ declare class FerricStoreSession implements Session, SessionHistoryRewriteAwareS
25
28
  readonly keyPrefix: string;
26
29
  private readonly initialItems;
27
30
  private readonly lockOptions;
31
+ private readonly legacyReceiptLocales;
28
32
  private readonly sessionKey;
33
+ private readonly stateKey;
29
34
  private readonly lockKey;
30
35
  constructor(client: FerricStoreCommandClient, options?: FerricStoreSessionOptions);
31
36
  getSessionId(): Promise<string>;
@@ -38,6 +43,7 @@ declare class FerricStoreSession implements Session, SessionHistoryRewriteAwareS
38
43
  applyHistoryTransaction(args: SessionHistoryTransactionArgs): Promise<void>;
39
44
  private mutate;
40
45
  private readState;
46
+ private readMutationState;
41
47
  private emptyState;
42
48
  }
43
49
 
@@ -10,14 +10,17 @@ interface FerricStoreSessionOptions extends FerricStoreLockOptions {
10
10
  initialItems?: AgentInputItem[];
11
11
  /** FerricStore key prefix. Defaults to `openai:agents:session`. */
12
12
  keyPrefix?: string;
13
+ /** Previous worker locales to accept when migrating unversioned operation receipts. */
14
+ legacyReceiptLocales?: string[];
13
15
  }
14
16
  /**
15
17
  * Durable OpenAI Agents SDK conversation history backed by FerricStore.
16
18
  *
17
- * Every mutation is serialized by an ownership-checked, renewable FerricStore
18
- * lock. History transactions and their operation receipts are persisted in one
19
- * atomic hash-field write, implementing the SDK's retry-safe transaction
20
- * capability in addition to its base Session contract.
19
+ * Renewable locks reduce contention, while compare-and-swap makes every state
20
+ * commit safe even if a writer's lease expires in flight. History transactions
21
+ * and their operation receipts are persisted in one atomic value, implementing
22
+ * the SDK's retry-safe transaction capability in addition to its base Session
23
+ * contract.
21
24
  */
22
25
  declare class FerricStoreSession implements Session, SessionHistoryRewriteAwareSession, SessionHistoryTransactionAwareSession {
23
26
  readonly client: FerricStoreCommandClient;
@@ -25,7 +28,9 @@ declare class FerricStoreSession implements Session, SessionHistoryRewriteAwareS
25
28
  readonly keyPrefix: string;
26
29
  private readonly initialItems;
27
30
  private readonly lockOptions;
31
+ private readonly legacyReceiptLocales;
28
32
  private readonly sessionKey;
33
+ private readonly stateKey;
29
34
  private readonly lockKey;
30
35
  constructor(client: FerricStoreCommandClient, options?: FerricStoreSessionOptions);
31
36
  getSessionId(): Promise<string>;
@@ -38,6 +43,7 @@ declare class FerricStoreSession implements Session, SessionHistoryRewriteAwareS
38
43
  applyHistoryTransaction(args: SessionHistoryTransactionArgs): Promise<void>;
39
44
  private mutate;
40
45
  private readState;
46
+ private readMutationState;
41
47
  private emptyState;
42
48
  }
43
49
 
@@ -104,14 +104,44 @@ function integerResponse(value, name) {
104
104
  if (!Number.isSafeInteger(parsed)) throw new TypeError(`FerricStore returned an invalid ${name}`);
105
105
  return parsed;
106
106
  }
107
+ async function readAtomicValue(client, key, name) {
108
+ const value = await client.command("GET", key);
109
+ if (value == null) return void 0;
110
+ if (typeof value === "string") return Buffer.from(value, "utf8");
111
+ if (Buffer.isBuffer(value) || value instanceof Uint8Array) return Buffer.from(value);
112
+ throw new TypeError(`FerricStore returned a non-binary ${name}`);
113
+ }
114
+ async function compareAndSetAtomicValue(client, key, expected, value) {
115
+ if (expected == null) {
116
+ const response2 = await client.command("SET", key, value, "NX");
117
+ if (response2 == null || response2 === false) return false;
118
+ if (response2 === true) return true;
119
+ return textResponse(response2, "SET NX response").toUpperCase() === "OK";
120
+ }
121
+ const response = await client.command("CAS", key, expected, value);
122
+ if (response == null || response === false) return false;
123
+ if (response === true) return true;
124
+ return integerResponse(response, "CAS response") === 1;
125
+ }
107
126
  async function withMutationLocks(client, keys, operation, options = {}) {
108
127
  const orderedKeys = [...new Set(keys)].sort();
109
- if (orderedKeys.length === 0) return await operation();
128
+ if (orderedKeys.length === 0) {
129
+ const signal = new AbortController().signal;
130
+ return await operation({
131
+ signal,
132
+ assertOwned: () => void 0,
133
+ publish: async (...args) => await additiveCommand(client, args),
134
+ compareAndSet: async (key, expected, value) => await compareAndSetAtomicValue(client, key, expected, value)
135
+ });
136
+ }
110
137
  const normalized = {
111
138
  lockRetryMs: positiveInteger(options.lockRetryMs, DEFAULT_LOCK_OPTIONS.lockRetryMs, "lockRetryMs"),
112
139
  lockTtlMs: positiveInteger(options.lockTtlMs, DEFAULT_LOCK_OPTIONS.lockTtlMs, "lockTtlMs"),
113
140
  lockWaitMs: nonNegativeInteger(options.lockWaitMs, DEFAULT_LOCK_OPTIONS.lockWaitMs, "lockWaitMs")
114
141
  };
142
+ if (normalized.lockRetryMs >= normalized.lockTtlMs) {
143
+ throw new TypeError("lockRetryMs must be less than lockTtlMs");
144
+ }
115
145
  const owner = randomUUID();
116
146
  const acquired = [];
117
147
  const deadline = performance.now() + normalized.lockWaitMs;
@@ -120,29 +150,81 @@ async function withMutationLocks(client, keys, operation, options = {}) {
120
150
  let releaseError;
121
151
  let result;
122
152
  let operationCompleted = false;
153
+ let conditionalCommitCompleted = false;
123
154
  const heartbeatAbort = new AbortController();
155
+ const ownershipAbort = new AbortController();
156
+ const lastExtended = /* @__PURE__ */ new Map();
157
+ const loseOwnership = (error) => {
158
+ const normalizedError = errorObject(error);
159
+ heartbeatError ??= normalizedError;
160
+ if (!ownershipAbort.signal.aborted) ownershipAbort.abort(normalizedError);
161
+ return normalizedError;
162
+ };
163
+ const assertOwned = () => {
164
+ if (heartbeatError != null) throw errorObject(heartbeatError);
165
+ if (ownershipAbort.signal.aborted) throw errorObject(ownershipAbort.signal.reason);
166
+ };
167
+ const renewOwned = async () => {
168
+ assertOwned();
169
+ for (const key of acquired) {
170
+ try {
171
+ const response = await client.command("EXTEND", key, owner, normalized.lockTtlMs);
172
+ if (integerResponse(response, "EXTEND response") !== 1) {
173
+ throw new Error(`lost FerricStore lock ${JSON.stringify(key)} while mutating data`);
174
+ }
175
+ lastExtended.set(key, performance.now());
176
+ } catch (error) {
177
+ throw loseOwnership(new Error(
178
+ `could not validate FerricStore lock ${JSON.stringify(key)} before mutating data`,
179
+ { cause: error }
180
+ ));
181
+ }
182
+ }
183
+ assertOwned();
184
+ };
185
+ const lease = {
186
+ signal: ownershipAbort.signal,
187
+ assertOwned,
188
+ publish: async (...args) => {
189
+ await renewOwned();
190
+ const response = await additiveCommand(client, args);
191
+ assertOwned();
192
+ return response;
193
+ },
194
+ compareAndSet: async (key, expected, value) => {
195
+ await renewOwned();
196
+ const committed = await compareAndSetAtomicValue(client, key, expected, value);
197
+ if (committed) conditionalCommitCompleted = true;
198
+ else assertOwned();
199
+ return committed;
200
+ }
201
+ };
124
202
  try {
125
203
  for (const key of orderedKeys) {
126
204
  while (!await tryAcquireLock(client, key, owner, normalized.lockTtlMs)) {
127
205
  if (performance.now() >= deadline) {
128
206
  throw new Error(`timed out acquiring FerricStore lock ${JSON.stringify(key)}`);
129
207
  }
208
+ await extendAcquiredLocks(client, acquired, owner, normalized.lockTtlMs);
130
209
  await delay(normalized.lockRetryMs);
131
210
  }
132
211
  acquired.push(key);
133
212
  }
213
+ await extendAcquiredLocks(client, acquired, owner, normalized.lockTtlMs);
214
+ for (const key of acquired) lastExtended.set(key, performance.now());
134
215
  const heartbeat = renewLocks(
135
216
  client,
136
217
  acquired,
137
218
  owner,
138
219
  normalized.lockTtlMs,
220
+ lastExtended,
139
221
  heartbeatAbort.signal,
140
222
  (error) => {
141
- heartbeatError ??= error;
223
+ loseOwnership(error);
142
224
  }
143
225
  );
144
226
  try {
145
- result = await operation();
227
+ result = await operation(lease);
146
228
  operationCompleted = true;
147
229
  } catch (error) {
148
230
  primaryError = error;
@@ -163,11 +245,32 @@ async function withMutationLocks(client, keys, operation, options = {}) {
163
245
  }
164
246
  }
165
247
  if (primaryError != null) throw errorObject(primaryError);
166
- if (heartbeatError != null) throw errorObject(heartbeatError);
167
- if (releaseError != null) throw errorObject(releaseError);
248
+ if (heartbeatError != null && !conditionalCommitCompleted) throw errorObject(heartbeatError);
249
+ if (releaseError != null && !(heartbeatError != null && conditionalCommitCompleted)) {
250
+ throw errorObject(releaseError);
251
+ }
168
252
  if (!operationCompleted) throw new Error("FerricStore mutation did not complete");
169
253
  return result;
170
254
  }
255
+ async function additiveCommand(client, args) {
256
+ const rawName = args[0];
257
+ const name = typeof rawName === "string" ? rawName.toUpperCase() : Buffer.isBuffer(rawName) || rawName instanceof Uint8Array ? Buffer.from(rawName).toString("utf8").toUpperCase() : "";
258
+ if (name !== "SADD" && name !== "ZADD") {
259
+ throw new TypeError("FerricStore mutation leases only publish add-only SADD or ZADD indexes");
260
+ }
261
+ if (name === "ZADD" && (args.length < 4 || args.length % 2 !== 0 || args.slice(2).some((value, index) => index % 2 === 0 && Number(value) !== 0))) {
262
+ throw new TypeError("FerricStore mutation leases only publish zero-score ZADD indexes");
263
+ }
264
+ return await client.command(...args);
265
+ }
266
+ async function extendAcquiredLocks(client, keys, owner, ttlMs) {
267
+ for (const key of keys) {
268
+ const response = await client.command("EXTEND", key, owner, ttlMs);
269
+ if (integerResponse(response, "EXTEND response") !== 1) {
270
+ throw new Error(`lost FerricStore lock ${JSON.stringify(key)} before mutating data`);
271
+ }
272
+ }
273
+ }
171
274
  async function tryAcquireLock(client, key, owner, ttlMs) {
172
275
  try {
173
276
  const response = await client.command("LOCK", key, owner, ttlMs);
@@ -177,10 +280,9 @@ async function tryAcquireLock(client, key, owner, ttlMs) {
177
280
  throw error;
178
281
  }
179
282
  }
180
- async function renewLocks(client, keys, owner, ttlMs, signal, onError) {
181
- const intervalMs = Math.max(Math.floor(ttlMs / 3), 10);
182
- const retryMs = Math.min(Math.max(Math.floor(intervalMs / 10), 10), 1e3);
183
- const lastExtended = new Map(keys.map((key) => [key, performance.now()]));
283
+ async function renewLocks(client, keys, owner, ttlMs, lastExtended, signal, onError) {
284
+ const intervalMs = Math.max(Math.floor(ttlMs / 3), 1);
285
+ const retryMs = Math.min(Math.max(Math.floor(intervalMs / 10), 1), 1e3);
184
286
  let waitMs = intervalMs;
185
287
  while (!signal.aborted) {
186
288
  try {
@@ -218,7 +320,7 @@ function errorObject(value) {
218
320
  // src/agent-persistence/snapshot.ts
219
321
  import { createHash } from "crypto";
220
322
  function encodeSnapshot(value) {
221
- return Buffer.from(JSON.stringify(snapshot(value, /* @__PURE__ */ new WeakSet())), "utf8");
323
+ return encodeSnapshotWith(value, defaultKeyComparator);
222
324
  }
223
325
  function decodeSnapshot(value, name) {
224
326
  const bytes = typeof value === "string" ? Buffer.from(value, "utf8") : Buffer.from(asBytes(value, name));
@@ -233,13 +335,22 @@ function decodeSnapshot(value, name) {
233
335
  function snapshotDigest(value) {
234
336
  return createHash("sha256").update(encodeSnapshot(value)).digest("hex");
235
337
  }
338
+ function legacySnapshotDigest(value, locale) {
339
+ return createHash("sha256").update(encodeSnapshotWith(value, (left, right) => left.localeCompare(right, locale))).digest("hex");
340
+ }
236
341
  function cloneSnapshot(value) {
237
342
  return decodeSnapshot(encodeSnapshot(value), "snapshot");
238
343
  }
239
344
  function snapshotsEqual(left, right) {
240
345
  return encodeSnapshot(left).equals(encodeSnapshot(right));
241
346
  }
242
- function snapshot(value, ancestors) {
347
+ function encodeSnapshotWith(value, compareKeys) {
348
+ return Buffer.from(JSON.stringify(snapshot(value, /* @__PURE__ */ new WeakSet(), compareKeys)), "utf8");
349
+ }
350
+ function defaultKeyComparator(left, right) {
351
+ return left < right ? -1 : left > right ? 1 : 0;
352
+ }
353
+ function snapshot(value, ancestors, compareKeys) {
243
354
  if (value === null) return ["null"];
244
355
  if (value === void 0) return ["undefined"];
245
356
  if (typeof value === "string") return ["string", value];
@@ -273,7 +384,7 @@ function snapshot(value, ancestors) {
273
384
  if (descriptor == null || !descriptor.enumerable || !("value" in descriptor)) {
274
385
  throw new TypeError("session history contains an unsupported array item");
275
386
  }
276
- items.push(snapshot(descriptor.value, ancestors));
387
+ items.push(snapshot(descriptor.value, ancestors, compareKeys));
277
388
  }
278
389
  return ["array", items];
279
390
  }
@@ -286,12 +397,12 @@ function snapshot(value, ancestors) {
286
397
  throw new TypeError("session history contains a symbol property");
287
398
  }
288
399
  const entries = [];
289
- for (const key of keys.sort((left, right) => left.localeCompare(right))) {
400
+ for (const key of keys.sort(compareKeys)) {
290
401
  const descriptor = Object.getOwnPropertyDescriptor(value, key);
291
402
  if (descriptor == null || !descriptor.enumerable || !("value" in descriptor)) {
292
403
  throw new TypeError("session history contains an unsupported property");
293
404
  }
294
- entries.push([key, snapshot(descriptor.value, ancestors)]);
405
+ entries.push([key, snapshot(descriptor.value, ancestors, compareKeys)]);
295
406
  }
296
407
  return ["object", entries];
297
408
  } finally {
@@ -362,13 +473,16 @@ function isArrayIndex(key, length) {
362
473
  // src/openai-agents.ts
363
474
  var SESSION_FORMAT_VERSION = 1;
364
475
  var SESSION_STATE_FIELD = "state";
476
+ var RECEIPT_DIGEST_VERSION = "v2:";
365
477
  var FerricStoreSession = class {
366
478
  client;
367
479
  sessionId;
368
480
  keyPrefix;
369
481
  initialItems;
370
482
  lockOptions;
483
+ legacyReceiptLocales;
371
484
  sessionKey;
485
+ stateKey;
372
486
  lockKey;
373
487
  constructor(client, options = {}) {
374
488
  this.client = client;
@@ -383,8 +497,17 @@ var FerricStoreSession = class {
383
497
  lockTtlMs: options.lockTtlMs,
384
498
  lockWaitMs: options.lockWaitMs
385
499
  };
500
+ if (options.legacyReceiptLocales != null && !Array.isArray(options.legacyReceiptLocales)) {
501
+ throw new TypeError("legacyReceiptLocales must be an array");
502
+ }
503
+ try {
504
+ this.legacyReceiptLocales = Intl.getCanonicalLocales(options.legacyReceiptLocales ?? []);
505
+ } catch (error) {
506
+ throw new TypeError("legacyReceiptLocales contains an invalid locale", { cause: error });
507
+ }
386
508
  const digest = createHash2("sha256").update(this.sessionId, "utf8").digest("hex");
387
509
  this.sessionKey = `${this.keyPrefix}:{oais:${digest}}:session`;
510
+ this.stateKey = `${this.sessionKey}:atomic-state`;
388
511
  this.lockKey = `${this.keyPrefix}:{oais:${digest}}:mutation-lock`;
389
512
  }
390
513
  async getSessionId() {
@@ -455,15 +578,21 @@ var FerricStoreSession = class {
455
578
  }
456
579
  async applyHistoryTransaction(args) {
457
580
  const { operationId, transaction } = snapshotTransactionArgs(args);
458
- const digest = snapshotDigest(transaction);
581
+ const digest = `${RECEIPT_DIGEST_VERSION}${snapshotDigest(transaction)}`;
582
+ const legacyDigests = /* @__PURE__ */ new Set([
583
+ snapshotDigest(transaction),
584
+ legacySnapshotDigest(transaction),
585
+ ...this.legacyReceiptLocales.map((locale) => legacySnapshotDigest(transaction, locale))
586
+ ]);
459
587
  await this.mutate(async (state) => {
460
588
  const existing = Object.getOwnPropertyDescriptor(state.operations, operationId)?.value;
461
589
  if (existing != null) {
462
590
  if (typeof existing !== "string") throw new Error("corrupt session history operation receipt");
463
- if (existing !== digest) {
591
+ if (existing === digest) return state;
592
+ if (!legacyDigests.has(existing)) {
464
593
  throw new Error("session history operation was already applied with a different transaction");
465
594
  }
466
- return state;
595
+ return { ...state, operations: { ...state.operations, [operationId]: digest } };
467
596
  }
468
597
  let items;
469
598
  if (transaction.type === "append_items") {
@@ -484,20 +613,28 @@ var FerricStoreSession = class {
484
613
  });
485
614
  }
486
615
  async mutate(operation) {
487
- await withMutationLocks(this.client, [this.lockKey], async () => {
488
- const current = await this.readState();
489
- const next = await operation(current);
490
- await this.client.command("HSET", this.sessionKey, SESSION_STATE_FIELD, encodeSnapshot(next));
616
+ await withMutationLocks(this.client, [this.lockKey], async (lease) => {
617
+ for (let attempt = 0; attempt < 8; attempt += 1) {
618
+ lease.assertOwned();
619
+ const snapshot2 = await this.readMutationState();
620
+ const next = await operation(snapshot2.state);
621
+ if (await lease.compareAndSet(this.stateKey, snapshot2.expected, encodeSnapshot(next))) return;
622
+ }
623
+ throw new Error("concurrent FerricStore OpenAI Agents session mutation did not converge");
491
624
  }, this.lockOptions);
492
625
  }
493
626
  async readState() {
494
- const value = await this.client.command("HGET", this.sessionKey, SESSION_STATE_FIELD);
495
- if (value == null) return this.emptyState();
627
+ return (await this.readMutationState()).state;
628
+ }
629
+ async readMutationState() {
630
+ const expected = await readAtomicValue(this.client, this.stateKey, "OpenAI Agents atomic session state");
631
+ const value = expected ?? await this.client.command("HGET", this.sessionKey, SESSION_STATE_FIELD);
632
+ if (value == null) return { expected, state: this.emptyState() };
496
633
  const state = decodeSnapshot(value, "OpenAI Agents session state");
497
634
  if (state == null || typeof state !== "object" || state.formatVersion !== SESSION_FORMAT_VERSION || state.sessionId !== this.sessionId || !Array.isArray(state.items) || state.operations == null || typeof state.operations !== "object" || Array.isArray(state.operations) || Object.values(state.operations).some((digest) => typeof digest !== "string")) {
498
635
  throw new Error("unsupported or corrupt FerricStore OpenAI Agents session state");
499
636
  }
500
- return state;
637
+ return { expected, state };
501
638
  }
502
639
  emptyState() {
503
640
  return {