@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.
- package/agent/lib/sandbox.js +74 -17
- package/package.json +1 -1
package/agent/lib/sandbox.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
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
|
-
|
|
908
|
+
reply.traceId,
|
|
852
909
|
);
|
|
853
910
|
}
|
|
854
911
|
|