@testdriverai/agent 7.8.0-test.39 → 7.8.0-test.40

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.
@@ -64,6 +64,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
64
64
  callback(null, ablyToken);
65
65
  },
66
66
  clientId: "sdk-" + this._sandboxId,
67
+ echoMessages: false, // don't receive our own published messages
67
68
  disconnectedRetryTimeout: 5000, // retry reconnect every 5s (default 15s)
68
69
  suspendedRetryTimeout: 15000, // retry from suspended every 15s (default 30s)
69
70
  });
@@ -96,7 +97,8 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
96
97
  logger.warn("Failed to enter presence on session channel: " + (e.message || e));
97
98
  }
98
99
 
99
- this._sessionChannel.subscribe("response", function (msg) {
100
+ // Save subscription references for historyBeforeSubscribe() during discontinuity recovery
101
+ this._responseSubscription = await this._sessionChannel.subscribe("response", function (msg) {
100
102
  var message = msg.data;
101
103
  if (!message) return;
102
104
 
@@ -197,7 +199,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
197
199
  delete self.ps[message.requestId];
198
200
  });
199
201
 
200
- this._sessionChannel.subscribe("file", function (msg) {
202
+ this._fileSubscription = await this._sessionChannel.subscribe("file", function (msg) {
201
203
  var message = msg.data;
202
204
  if (!message) return;
203
205
  logger.log(`[ably] Received file: type=${message.type || 'unknown'} (requestId=${message.requestId || 'none'})`);
@@ -209,7 +211,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
209
211
  emitter.emit(events.sandbox.file, message);
210
212
  });
211
213
 
212
- this.heartbeat = setInterval(function () {}, 5000);
214
+ this.heartbeat = setInterval(function () { }, 5000);
213
215
  if (this.heartbeat.unref) this.heartbeat.unref();
214
216
 
215
217
  // ─── Periodic stats logging ────────────────────────────────────────
@@ -240,6 +242,61 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
240
242
  self.instanceSocketConnected = false;
241
243
  emitter.emit(events.error.sandbox, "Ably connection failed");
242
244
  });
245
+
246
+ // ─── Channel discontinuity detection ──────────────────────────────
247
+ // Set up BEFORE subscribing so we catch any continuity loss during
248
+ // the initial attachment. Fires at the channel level, covering all
249
+ // message types (response, file, control).
250
+ this._sessionChannel.on(function (stateChange) {
251
+ var current = stateChange.current;
252
+ var previous = stateChange.previous;
253
+ var reason = stateChange.reason;
254
+ var reasonMsg = reason ? (reason.message || reason.code || String(reason)) : '';
255
+
256
+ if (current === 'attached' && stateChange.resumed === false && previous) {
257
+ logger.warn('[ably] Channel DISCONTINUITY detected (resumed=false)' + (reasonMsg ? ' — ' + reasonMsg : ''));
258
+ emitter.emit(events.sandbox.progress, {
259
+ step: 'discontinuity',
260
+ message: 'Recovering missed messages after connection interruption...',
261
+ });
262
+ self._recoverFromDiscontinuity();
263
+ }
264
+ });
265
+ }
266
+
267
+ /**
268
+ * Recover missed messages after a channel discontinuity.
269
+ * Uses historyBeforeSubscribe() on each subscription, which guarantees
270
+ * no gap between historical and live messages.
271
+ */
272
+ async _recoverFromDiscontinuity() {
273
+ var subs = [
274
+ { name: 'response', sub: this._responseSubscription },
275
+ { name: 'file', sub: this._fileSubscription },
276
+ ];
277
+ var totalRecovered = 0;
278
+ for (var i = 0; i < subs.length; i++) {
279
+ var entry = subs[i];
280
+ if (!entry.sub) continue;
281
+ try {
282
+ logger.log('[ably] Discontinuity recovery: fetching historyBeforeSubscribe for ' + entry.name + '...');
283
+ var page = await entry.sub.historyBeforeSubscribe({ limit: 100 });
284
+ var recovered = 0;
285
+ while (page) {
286
+ recovered += page.items.length;
287
+ page = page.hasNext() ? await page.next() : null;
288
+ }
289
+ totalRecovered += recovered;
290
+ logger.log('[ably] Discontinuity recovery: found ' + recovered + ' ' + entry.name + ' message(s) in gap');
291
+ } catch (err) {
292
+ logger.error('[ably] Discontinuity recovery failed for ' + entry.name + ': ' + (err.message || err));
293
+ }
294
+ }
295
+ if (totalRecovered > 0) {
296
+ logger.warn('[ably] Recovered ' + totalRecovered + ' message(s) that were missed during connection interruption');
297
+ } else {
298
+ logger.log('[ably] Discontinuity recovery: no missed messages found');
299
+ }
243
300
  }
244
301
 
245
302
  /**
@@ -279,10 +336,10 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
279
336
  var elapsed = Date.now() - startTime;
280
337
  logger.warn(
281
338
  "Transient network error: " + (error.message || error.code) +
282
- " — POST " + path +
283
- " — retry " + attempt + "/3" +
284
- " in " + (delayMs / 1000).toFixed(1) + "s" +
285
- " (" + Math.round(elapsed / 1000) + "s elapsed)...",
339
+ " — POST " + path +
340
+ " — retry " + attempt + "/3" +
341
+ " in " + (delayMs / 1000).toFixed(1) + "s" +
342
+ " (" + Math.round(elapsed / 1000) + "s elapsed)...",
286
343
  );
287
344
  },
288
345
  });
@@ -294,10 +351,10 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
294
351
  var elapsed = Date.now() - startTime;
295
352
  logger.log(
296
353
  "Concurrency limit reached — waiting " +
297
- concurrencyRetryInterval / 1000 +
298
- "s for a slot to become available (" +
299
- Math.round(elapsed / 1000) +
300
- "s elapsed)...",
354
+ concurrencyRetryInterval / 1000 +
355
+ "s for a slot to become available (" +
356
+ Math.round(elapsed / 1000) +
357
+ "s elapsed)...",
301
358
  );
302
359
  await new Promise(function (resolve) {
303
360
  var t = setTimeout(resolve, concurrencyRetryInterval);
@@ -730,10 +787,10 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
730
787
  rejectPromise(
731
788
  new Error(
732
789
  "Sandbox message '" +
733
- message.type +
734
- "' timed out after " +
735
- timeout +
736
- "ms",
790
+ message.type +
791
+ "' timed out after " +
792
+ timeout +
793
+ "ms",
737
794
  ),
738
795
  );
739
796
  }
@@ -755,7 +812,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
755
812
  };
756
813
 
757
814
  if (message.type === "output") {
758
- p.catch(function () {});
815
+ p.catch(function () { });
759
816
  }
760
817
 
761
818
  this._throttledPublish(this._sessionChannel, "command", message)
@@ -848,7 +905,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
848
905
  logger.log("Trace Report (Share When Reporting Bugs):");
849
906
  logger.log(
850
907
  "https://testdriver.sentry.io/explore/traces/trace/" +
851
- reply.traceId,
908
+ reply.traceId,
852
909
  );
853
910
  }
854
911
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testdriverai/agent",
3
- "version": "7.8.0-test.39",
3
+ "version": "7.8.0-test.40",
4
4
  "description": "Next generation autonomous AI agent for end-to-end testing of web & desktop",
5
5
  "main": "sdk.js",
6
6
  "types": "sdk.d.ts",