@jskit-ai/assistant-runtime 0.1.237 → 0.1.238
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/assistant-runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.238",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test --test-concurrency=1"
|
|
@@ -12,19 +12,19 @@
|
|
|
12
12
|
"./server": "./src/server/index.js"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@jskit-ai/assistant-core": "0.1.
|
|
15
|
+
"@jskit-ai/assistant-core": "0.1.244",
|
|
16
16
|
"json-rest-schema": "^1.0.17"
|
|
17
17
|
},
|
|
18
18
|
"peerDependencies": {
|
|
19
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
20
|
-
"@jskit-ai/http-web": "0.1.
|
|
21
|
-
"@jskit-ai/kernel": "0.1.
|
|
22
|
-
"@jskit-ai/realtime": "0.1.
|
|
23
|
-
"@jskit-ai/shell-web": "0.1.
|
|
19
|
+
"@jskit-ai/http-runtime": "0.1.246",
|
|
20
|
+
"@jskit-ai/http-web": "0.1.96",
|
|
21
|
+
"@jskit-ai/kernel": "0.1.248",
|
|
22
|
+
"@jskit-ai/realtime": "0.1.248",
|
|
23
|
+
"@jskit-ai/shell-web": "0.1.255",
|
|
24
24
|
"@tanstack/vue-query": "^5.90.5",
|
|
25
25
|
"vue": "^3.5.13",
|
|
26
26
|
"vuetify": "^4.0.0",
|
|
27
|
-
"@jskit-ai/database-runtime": "0.1.
|
|
27
|
+
"@jskit-ai/database-runtime": "0.1.248"
|
|
28
28
|
},
|
|
29
29
|
"description": "Capability-based assistant runtime with per-surface chat, transcripts, settings, and action tools.",
|
|
30
30
|
"jskit": {
|
|
@@ -40,6 +40,10 @@ function subscribeAssistantConversation({
|
|
|
40
40
|
let streaming = null;
|
|
41
41
|
let reloadInFlight = null;
|
|
42
42
|
let reloadQueued = false;
|
|
43
|
+
// A transient subscription failure retries while the socket stays connected;
|
|
44
|
+
// otherwise only the next connect would recover the chat.
|
|
45
|
+
let retryTimer = null;
|
|
46
|
+
let retryDelay = 1_000;
|
|
43
47
|
const completed = new Map();
|
|
44
48
|
const settledTurns = new Set();
|
|
45
49
|
const pendingReads = new Set();
|
|
@@ -183,7 +187,12 @@ function subscribeAssistantConversation({
|
|
|
183
187
|
if (epoch === null) pending.push(payload);
|
|
184
188
|
else deliver(payload);
|
|
185
189
|
}
|
|
190
|
+
function clearRetry() {
|
|
191
|
+
clearTimeout(retryTimer);
|
|
192
|
+
retryTimer = null;
|
|
193
|
+
}
|
|
186
194
|
function subscribe({ preserveHistory = false } = {}) {
|
|
195
|
+
clearRetry();
|
|
187
196
|
if (disposed || !socket.connected) return;
|
|
188
197
|
const attempt = ++generation;
|
|
189
198
|
epoch = null;
|
|
@@ -202,13 +211,18 @@ function subscribeAssistantConversation({
|
|
|
202
211
|
listening = false;
|
|
203
212
|
pending = [];
|
|
204
213
|
if (socket.connected) socket.emit(ASSISTANT_CONVERSATION_UNSUBSCRIBE, { subscriptionId });
|
|
205
|
-
|
|
214
|
+
if (error || response?.status >= 500) {
|
|
215
|
+
retryTimer = setTimeout(() => subscribe({ preserveHistory }), retryDelay);
|
|
216
|
+
retryDelay = Math.min(retryDelay * 2, 30_000);
|
|
217
|
+
}
|
|
218
|
+
onError(error ? Object.assign(new Error("Chat updates could not reconnect. Retrying automatically; Reload chat to try now.", { cause: error }), {
|
|
206
219
|
code: "assistant_subscription_transport_failed"
|
|
207
220
|
}) : Object.assign(new Error(response?.error || "Conversation subscription failed."), {
|
|
208
221
|
code: response?.code, statusCode: response?.status
|
|
209
222
|
}));
|
|
210
223
|
return;
|
|
211
224
|
}
|
|
225
|
+
retryDelay = 1_000;
|
|
212
226
|
epoch = response.streamEpoch;
|
|
213
227
|
receiveState(response.state, { initial: !preserveHistory, canonical: !preserveHistory });
|
|
214
228
|
for (const payload of pending.sort((left, right) => left.streamRevision - right.streamRevision)) deliver(payload);
|
|
@@ -216,6 +230,7 @@ function subscribeAssistantConversation({
|
|
|
216
230
|
});
|
|
217
231
|
}
|
|
218
232
|
function disconnected() {
|
|
233
|
+
clearRetry();
|
|
219
234
|
generation += 1;
|
|
220
235
|
epoch = null;
|
|
221
236
|
pending = [];
|
|
@@ -231,6 +246,7 @@ function subscribeAssistantConversation({
|
|
|
231
246
|
function release() {
|
|
232
247
|
if (disposed) return;
|
|
233
248
|
disposed = true;
|
|
249
|
+
clearRetry();
|
|
234
250
|
generation += 1;
|
|
235
251
|
pending = [];
|
|
236
252
|
completed.clear();
|
|
@@ -343,7 +343,7 @@ test("the real ten-second subscription deadline releases its late observer and R
|
|
|
343
343
|
while (!view.errors.length || f.active() !== 0) {
|
|
344
344
|
await once(f.changes, "change", { signal: AbortSignal.timeout(12_000) });
|
|
345
345
|
}
|
|
346
|
-
assert.equal(view.errors[0].message, "Chat updates could not reconnect. Reload chat to try
|
|
346
|
+
assert.equal(view.errors[0].message, "Chat updates could not reconnect. Retrying automatically; Reload chat to try now.");
|
|
347
347
|
assert.equal(view.errors[0].cause.message, "operation has timed out");
|
|
348
348
|
assert.equal(socket.id, socketId);
|
|
349
349
|
assert.equal(socket.connected, true);
|
|
@@ -1012,6 +1012,70 @@ test("host subscription policy retains authenticated actor metadata and per-even
|
|
|
1012
1012
|
assert.ok(cookies.every(value => value === "host-session"));
|
|
1013
1013
|
});
|
|
1014
1014
|
|
|
1015
|
+
test("a transient subscription failure retries by itself while connected; denials never retry", t => {
|
|
1016
|
+
// Production: after a laptop woke, the resubscribe acknowledgement missed its
|
|
1017
|
+
// deadline while the socket stayed connected, and the chat stayed stuck.
|
|
1018
|
+
t.mock.timers.enable({ apis: ["setTimeout"] });
|
|
1019
|
+
const socket = new EventEmitter();
|
|
1020
|
+
socket.connected = true;
|
|
1021
|
+
const requests = [], states = [], errors = [];
|
|
1022
|
+
socket.timeout = () => ({ emit(name, input, acknowledge) { requests.push({ input, acknowledge }); } });
|
|
1023
|
+
const dispose = subscribeAssistantConversation({ socket, conversationId: "chat", targetSurfaceId: "home", hostSurfaceId: "home",
|
|
1024
|
+
read() {}, onState: state => states.push(state), onError: error => errors.push(error) });
|
|
1025
|
+
const state = { id: "chat", conversationLog: [], streaming: { revision: 0, messages: [] } };
|
|
1026
|
+
requests[0].acknowledge(new Error("operation has timed out"));
|
|
1027
|
+
assert.match(errors[0].message, /Retrying automatically/u);
|
|
1028
|
+
t.mock.timers.tick(999);
|
|
1029
|
+
assert.equal(requests.length, 1);
|
|
1030
|
+
t.mock.timers.tick(1);
|
|
1031
|
+
assert.equal(requests.length, 2);
|
|
1032
|
+
assert.deepEqual(requests[1].input, requests[0].input);
|
|
1033
|
+
requests[1].acknowledge(null, { ok: false, error: "Retry shortly.", status: 503 });
|
|
1034
|
+
t.mock.timers.tick(1_999);
|
|
1035
|
+
assert.equal(requests.length, 2, "Each retry waits twice as long");
|
|
1036
|
+
t.mock.timers.tick(1);
|
|
1037
|
+
assert.equal(requests.length, 3);
|
|
1038
|
+
requests[2].acknowledge(null, { ok: true, streamEpoch: "recovered", state });
|
|
1039
|
+
assert.equal(states.length, 1);
|
|
1040
|
+
// A reconnect replaces a pending retry, and success restarts the backoff.
|
|
1041
|
+
socket.emit("disconnect");
|
|
1042
|
+
socket.emit("connect");
|
|
1043
|
+
requests[3].acknowledge(new Error("operation has timed out"));
|
|
1044
|
+
socket.connected = false;
|
|
1045
|
+
socket.emit("disconnect");
|
|
1046
|
+
t.mock.timers.tick(60_000);
|
|
1047
|
+
assert.equal(requests.length, 4, "A disconnected socket waits for its own reconnect");
|
|
1048
|
+
socket.connected = true;
|
|
1049
|
+
socket.emit("connect");
|
|
1050
|
+
requests[4].acknowledge(null, { ok: false, error: "Access denied.", status: 403 });
|
|
1051
|
+
t.mock.timers.tick(60_000);
|
|
1052
|
+
assert.equal(requests.length, 5, "A denial never retries");
|
|
1053
|
+
dispose.reload();
|
|
1054
|
+
requests[5].acknowledge(new Error("operation has timed out"));
|
|
1055
|
+
t.mock.timers.tick(999);
|
|
1056
|
+
dispose();
|
|
1057
|
+
t.mock.timers.tick(60_000);
|
|
1058
|
+
assert.equal(requests.length, 6, "Disposal cancels the pending retry");
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
test("a retried resubscribe keeps the loaded history its caller preserved", t => {
|
|
1062
|
+
t.mock.timers.enable({ apis: ["setTimeout"] });
|
|
1063
|
+
const socket = new EventEmitter();
|
|
1064
|
+
socket.connected = true;
|
|
1065
|
+
const requests = [], publications = [];
|
|
1066
|
+
socket.timeout = () => ({ emit(name, input, acknowledge) { requests.push(acknowledge); } });
|
|
1067
|
+
const subscription = subscribeAssistantConversation({ socket, conversationId: "chat", targetSurfaceId: "home", hostSurfaceId: "home",
|
|
1068
|
+
read() {}, onState: (state, options) => publications.push(options) });
|
|
1069
|
+
const state = { id: "chat", conversationLog: [], streaming: { revision: 0, messages: [] } };
|
|
1070
|
+
requests[0](null, { ok: true, streamEpoch: "first", state });
|
|
1071
|
+
subscription.reload({ resubscribe: true });
|
|
1072
|
+
requests[1](new Error("operation has timed out"));
|
|
1073
|
+
t.mock.timers.tick(1_000);
|
|
1074
|
+
requests[2](null, { ok: true, streamEpoch: "second", state });
|
|
1075
|
+
assert.deepEqual(publications, [{ initial: true, canonical: true }, { initial: false, canonical: false }]);
|
|
1076
|
+
subscription();
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1015
1079
|
for (const status of [401, 403, 503]) {
|
|
1016
1080
|
test(`subscription read failure ${status} fences denied updates but retains transient history`, async t => {
|
|
1017
1081
|
const changes = new EventEmitter();
|