@axlsdk/axl 0.27.0 → 0.27.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.
package/dist/index.cjs CHANGED
@@ -602,7 +602,7 @@ var sharedGuard = typeof installedGuard === "object" && installedGuard !== null
602
602
  var buildPath = typeof __AXL_BUILD_PATH__ === "string" ? __AXL_BUILD_PATH__ : "<source>";
603
603
  var thisCopy = {
604
604
  path: buildPath.startsWith("file:") ? (0, import_node_url.fileURLToPath)(buildPath) : buildPath,
605
- version: true ? "0.27.0" : "unknown"
605
+ version: true ? "0.27.1" : "unknown"
606
606
  };
607
607
  function warnCrossCopy(other, joined) {
608
608
  if (other === thisCopy) return;
@@ -17280,7 +17280,7 @@ ${appendedSchemaText}`;
17280
17280
  `ctx.checkpoint name "${name}" is reserved \u2014 names starting with "__auto/" (case-insensitive) are used by the runtime for ask/spawn/race/parallel/map auto-checkpointing.`
17281
17281
  );
17282
17282
  }
17283
- return this._checkpoint(name, fn);
17283
+ return this._checkpoint(name, fn, { saveRequired: true });
17284
17284
  }
17285
17285
  /**
17286
17286
  * Internal checkpoint implementation shared by both the public
@@ -17310,7 +17310,17 @@ ${appendedSchemaText}`;
17310
17310
  return saved;
17311
17311
  }
17312
17312
  const result = await fn();
17313
- await this.stateStore.saveCheckpoint(this.executionId, name, result);
17313
+ if (!options?.saveRequired && this.autoCheckpointCounters.saveFailed) return result;
17314
+ try {
17315
+ await this.stateStore.saveCheckpoint(this.executionId, name, result);
17316
+ } catch (err) {
17317
+ if (options?.saveRequired) throw err;
17318
+ this.autoCheckpointCounters.saveFailed = true;
17319
+ console.error(
17320
+ `[axl] Failed to save checkpoint "${name}" for executionId=${this.executionId}; a replay would run this step and later ones again: ${err instanceof Error ? err.message : String(err)}`
17321
+ );
17322
+ return result;
17323
+ }
17314
17324
  this.emitEvent({
17315
17325
  type: "checkpoint_save",
17316
17326
  ...agentStamp,
@@ -20231,11 +20241,18 @@ var Session = class _Session {
20231
20241
  try {
20232
20242
  const completion = axlStream.promise.then(
20233
20243
  (result) => this.commitHistory(history, result).catch((err) => {
20234
- this.runtime.emit("error", {
20235
- type: "session_history_save_failed",
20236
- sessionId: this.sessionId,
20237
- error: err instanceof Error ? err.message : String(err)
20238
- });
20244
+ const error = err instanceof Error ? err.message : String(err);
20245
+ try {
20246
+ this.runtime.emit("error", {
20247
+ type: "session_history_save_failed",
20248
+ sessionId: this.sessionId,
20249
+ error
20250
+ });
20251
+ } catch {
20252
+ console.error(
20253
+ `[axl] Failed to save session history for sessionId=${this.sessionId}: ${error}`
20254
+ );
20255
+ }
20239
20256
  }),
20240
20257
  () => void 0
20241
20258
  );
@@ -22302,7 +22319,11 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
22302
22319
  observation
22303
22320
  });
22304
22321
  if (this.stateStore.deleteCheckpoints) {
22305
- await this.stateStore.deleteCheckpoints(execInfo.executionId);
22322
+ await this.stateStore.deleteCheckpoints(execInfo.executionId).catch((err) => {
22323
+ console.error(
22324
+ `[axl] Failed to delete checkpoints for executionId=${execInfo.executionId}: ` + (err instanceof Error ? err.message : String(err))
22325
+ );
22326
+ });
22306
22327
  }
22307
22328
  span?.setAttribute("axl.workflow.cost", execInfo.totalCost);
22308
22329
  span?.setAttribute("axl.workflow.duration", execInfo.duration);
@@ -22374,7 +22395,10 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
22374
22395
  const chain = this.stateStore.saveExecution(snapshot).then(() => {
22375
22396
  return this.streamingFlusher?.finalize(id).catch(() => {
22376
22397
  });
22377
- }).catch(() => {
22398
+ }).catch((err) => {
22399
+ console.error(
22400
+ `[axl] Failed to save execution history for executionId=${id}: ` + (err instanceof Error ? err.message : String(err))
22401
+ );
22378
22402
  }).then(() => void 0);
22379
22403
  this.persistInflight.add(chain);
22380
22404
  const pending = { snapshot, settled: chain };
@@ -24797,12 +24821,108 @@ function warnIfRedisExecutionRetentionUnset(opts) {
24797
24821
  warnExecutionRetentionUnset("RedisStore");
24798
24822
  }
24799
24823
  }
24824
+ var CLOSE_TIMEOUT_MS = 2e3;
24825
+ function reconnectDelayMs(retries) {
24826
+ return Math.min(2 ** retries * 50, 2e3) + Math.floor(Math.random() * 200);
24827
+ }
24828
+ function abandonConnection(client) {
24829
+ try {
24830
+ client.destroy?.();
24831
+ } catch {
24832
+ }
24833
+ client.unref?.();
24834
+ }
24835
+ function errorMessage(error) {
24836
+ return error instanceof Error ? error.message : String(error);
24837
+ }
24838
+ function connectionEventReporter(client, onConnectionEvent) {
24839
+ if (onConnectionEvent) {
24840
+ const reportFailure = (error) => console.error(`[axl] RedisStore: onConnectionEvent failed: ${errorMessage(error)}`);
24841
+ return (event) => {
24842
+ try {
24843
+ Promise.resolve(onConnectionEvent(event)).catch(reportFailure);
24844
+ } catch (error) {
24845
+ reportFailure(error);
24846
+ }
24847
+ };
24848
+ }
24849
+ let outageLogged = false;
24850
+ return (event) => {
24851
+ if (event.type === "reconnected") {
24852
+ outageLogged = false;
24853
+ console.warn("[axl] RedisStore: reconnected to Redis.");
24854
+ } else if (!outageLogged) {
24855
+ outageLogged = client.isReady === false;
24856
+ console.warn(
24857
+ `[axl] RedisStore: Redis connection lost (${event.error.message}). Store calls fail until it reconnects, which happens automatically.`
24858
+ );
24859
+ }
24860
+ };
24861
+ }
24862
+ async function connectClient(createClient, opts) {
24863
+ let established = false;
24864
+ const client = createClient({
24865
+ ...opts.url ? { url: opts.url } : {},
24866
+ // A command accepted just before a drop rejects instead of waiting for
24867
+ // the reconnect.
24868
+ disableOfflineQueue: true,
24869
+ socket: { reconnectStrategy: (retries) => established ? reconnectDelayMs(retries) : false }
24870
+ });
24871
+ const reportConnectionEvent = connectionEventReporter(client, opts.onConnectionEvent);
24872
+ client.on("error", (error) => {
24873
+ if (!established || client.isOpen === false) return;
24874
+ reportConnectionEvent({
24875
+ type: "error",
24876
+ error: error instanceof Error ? error : new Error(String(error))
24877
+ });
24878
+ });
24879
+ client.on("connect", () => {
24880
+ if (client.isOpen === false) client.unref?.();
24881
+ });
24882
+ client.on("ready", () => {
24883
+ if (!established) {
24884
+ established = true;
24885
+ } else if (client.isOpen !== false) {
24886
+ reportConnectionEvent({ type: "reconnected" });
24887
+ }
24888
+ });
24889
+ await client.connect();
24890
+ return client;
24891
+ }
24800
24892
  var RedisStore = class _RedisStore {
24801
- constructor(client, keyPrefix, ttls) {
24802
- this.client = client;
24893
+ constructor(connection, keyPrefix, ttls) {
24894
+ this.connection = connection;
24803
24895
  this.keyPrefix = keyPrefix;
24804
24896
  this.ttls = ttls;
24805
24897
  }
24898
+ /**
24899
+ * Whether the store can send commands right now, for a health check or for
24900
+ * telling an outage from other failures when a call rejects. `false` while
24901
+ * reconnecting and after `close()`.
24902
+ */
24903
+ get isConnected() {
24904
+ return this.connection.isOpen !== false && this.connection.isReady !== false;
24905
+ }
24906
+ /**
24907
+ * The client for sending a command, failing immediately while disconnected.
24908
+ *
24909
+ * Every command goes through here because node-redis's `disableOfflineQueue`
24910
+ * rejects single commands but not `multi().exec()`, which would wait for a
24911
+ * reconnect attempt to fail or succeed. Nothing may `await` between
24912
+ * `client.multi()` and `.exec()`, or a transaction could get past this check.
24913
+ *
24914
+ * A closed store isn't "unavailable": it falls through to node-redis's own
24915
+ * "The client is closed" rejection.
24916
+ */
24917
+ get client() {
24918
+ if (this.connection.isOpen !== false && this.connection.isReady === false) {
24919
+ throw new AxlError(
24920
+ "REDIS_UNAVAILABLE",
24921
+ "RedisStore: not connected to Redis, so the command was not sent. The store reconnects automatically; retry once Redis is back."
24922
+ );
24923
+ }
24924
+ return this.connection;
24925
+ }
24806
24926
  /**
24807
24927
  * History kinds with a prune running in this process. A save skips its
24808
24928
  * prune while another one of the same kind is in flight: the running one
@@ -24872,8 +24992,7 @@ var RedisStore = class _RedisStore {
24872
24992
  if (err instanceof Error && err.message.includes("createClient")) throw err;
24873
24993
  throw new Error("redis is required for RedisStore. Install it with: npm install redis");
24874
24994
  }
24875
- const client = opts.url ? createClient({ url: opts.url }) : createClient();
24876
- await client.connect();
24995
+ const client = await connectClient(createClient, opts);
24877
24996
  const resolveCategory = (override) => {
24878
24997
  if (override === null) return null;
24879
24998
  if (typeof override === "number") return override;
@@ -24897,8 +25016,13 @@ var RedisStore = class _RedisStore {
24897
25016
  const store = new _RedisStore(client, keyPrefix, effectiveTtls);
24898
25017
  warnIfRedisExecutionRetentionUnset(opts);
24899
25018
  if (!opts.skipMigration) {
24900
- await store.backfillExecutionIndex();
24901
- await store.backfillEvalIndex();
25019
+ try {
25020
+ await store.backfillExecutionIndex();
25021
+ await store.backfillEvalIndex();
25022
+ } catch (error) {
25023
+ abandonConnection(client);
25024
+ throw error;
25025
+ }
24902
25026
  }
24903
25027
  return store;
24904
25028
  }
@@ -25167,7 +25291,7 @@ var RedisStore = class _RedisStore {
25167
25291
  }
25168
25292
  }
25169
25293
  if (dead.length > 0) {
25170
- this.client.sRem(this.pendingExecSetKey(), dead).catch((err) => {
25294
+ this.connection.sRem(this.pendingExecSetKey(), dead).catch((err) => {
25171
25295
  console.error(
25172
25296
  `[axl] RedisStore: failed to prune ${dead.length} stale pending-exec ids: ` + (err instanceof Error ? err.message : String(err))
25173
25297
  );
@@ -25213,7 +25337,7 @@ var RedisStore = class _RedisStore {
25213
25337
  }
25214
25338
  const ttl = this.ttlFor("memory");
25215
25339
  if (ttl !== void 0) {
25216
- this.client.expire(this.memoryKey(scope), ttl).catch((err) => {
25340
+ this.connection.expire(this.memoryKey(scope), ttl).catch((err) => {
25217
25341
  console.error(
25218
25342
  `[axl] RedisStore: failed to set TTL on migrated memory key ${this.memoryKey(scope)}: ` + (err instanceof Error ? err.message : String(err))
25219
25343
  );
@@ -25505,6 +25629,7 @@ var RedisStore = class _RedisStore {
25505
25629
  try {
25506
25630
  await this.pruneHistory(index);
25507
25631
  } catch (err) {
25632
+ if (this.connection.isReady === false) return;
25508
25633
  warnDiagnosticOnce(
25509
25634
  `redis_history_prune_failed\0${index.kind}`,
25510
25635
  `RedisStore: removing expired ${index.label} index entries failed (${err instanceof Error ? err.message : String(err)}). The record was saved; the next save retries the cleanup. Listings stay correct meanwhile. The cleanup runs a Lua script, so the Redis user needs permission to run EVAL.`,
@@ -25709,10 +25834,31 @@ var RedisStore = class _RedisStore {
25709
25834
  * must not get a rejected promise for it. `MemoryStore.close()` and
25710
25835
  * better-sqlite3's `close()` are both no-ops the second time; node-redis
25711
25836
  * instead rejects with "The client is closed", so absorb that here.
25837
+ *
25838
+ * Bounded, so a shutdown can't hang on Redis. `QUIT` lets queued commands
25839
+ * finish on a healthy connection, but node-redis never settles it if the
25840
+ * socket closes first, and a stalled server may never answer it. While
25841
+ * disconnected it couldn't be sent at all.
25712
25842
  */
25713
25843
  async close() {
25714
- if (this.client.isOpen === false) return;
25715
- await this.client.quit();
25844
+ if (this.connection.isOpen === false) return;
25845
+ if (this.connection.isReady === false) {
25846
+ abandonConnection(this.connection);
25847
+ return;
25848
+ }
25849
+ let timer;
25850
+ try {
25851
+ await Promise.race([
25852
+ this.connection.quit(),
25853
+ new Promise((_resolve, reject) => {
25854
+ timer = setTimeout(() => reject(new Error("QUIT timed out")), CLOSE_TIMEOUT_MS);
25855
+ })
25856
+ ]);
25857
+ } catch {
25858
+ abandonConnection(this.connection);
25859
+ } finally {
25860
+ clearTimeout(timer);
25861
+ }
25716
25862
  }
25717
25863
  async deleteCheckpoints(executionId) {
25718
25864
  await this.client.del(this.checkpointKey(executionId));