@jskit-ai/assistant-runtime 0.1.208 → 0.1.210

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.
@@ -155,6 +155,7 @@ export default defineConfig({
155
155
  return json({ ok: true, delivered: true, messageId: input.messageId }, 202);
156
156
  }
157
157
  if (mode === "rejected") return json({ error: "The message was rejected before admission.", code: "ACTION_VALIDATION_FAILED" }, 400);
158
+ if (mode === "unadmitted") return json({ error: "Request failed with status 503." }, 503);
158
159
  if (mode === "uncertain") {
159
160
  state.status = "unconfirmed";
160
161
  state.pendingRequest = { messageId: input.messageId, text: input.text, origin: "user",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/assistant-runtime",
3
- "version": "0.1.208",
3
+ "version": "0.1.210",
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.215",
15
+ "@jskit-ai/assistant-core": "0.1.217",
16
16
  "json-rest-schema": "^1.0.17"
17
17
  },
18
18
  "peerDependencies": {
19
- "@jskit-ai/http-runtime": "0.1.235",
20
- "@jskit-ai/http-web": "0.1.82",
21
- "@jskit-ai/kernel": "0.1.237",
22
- "@jskit-ai/realtime": "0.1.234",
23
- "@jskit-ai/shell-web": "0.1.241",
19
+ "@jskit-ai/http-runtime": "0.1.237",
20
+ "@jskit-ai/http-web": "0.1.84",
21
+ "@jskit-ai/kernel": "0.1.239",
22
+ "@jskit-ai/realtime": "0.1.236",
23
+ "@jskit-ai/shell-web": "0.1.243",
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.237"
27
+ "@jskit-ai/database-runtime": "0.1.239"
28
28
  },
29
29
  "description": "Capability-based assistant runtime with per-surface chat, transcripts, settings, and action tools.",
30
30
  "jskit": {
@@ -117,7 +117,8 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
117
117
  const queueing = computed(() => queueWhileSending !== false && (steerable.value || workingPolicy.value.deferWhileWorking || workingPolicy.value.admitWhileWorking));
118
118
  const canSubmit = computed(() => available.value && (snapshot.value.status === "ready" || steerable.value || (workingPolicy.value.deferWhileWorking || workingPolicy.value.admitWhileWorking) && snapshot.value.status === "working") &&
119
119
  (queueWhileSending !== false || !delivery.state.sending) &&
120
- !delivery.state.messages.some(message => message.status === "uncertain"));
120
+ !delivery.state.messages.some(message => message.status === "uncertain" &&
121
+ (snapshot.value.status !== "ready" || !message.payload || text(message.payload.draft ?? message.payload.message) === text(draft))));
121
122
  const savedDraft = toValue(draftStorage);
122
123
  if (savedDraft && (typeof savedDraft.key !== "string" || !savedDraft.key ||
123
124
  !["getItem", "setItem", "removeItem"].every(name => typeof savedDraft.storage?.[name] === "function"))) {
@@ -340,8 +341,6 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
340
341
  for (const pending of pendingMessages.values()) pending.controller.abort();
341
342
  pendingMessages.clear();
342
343
  goalReadController?.abort();
343
- globalThis.removeEventListener?.("focus", refreshVisibleGoal);
344
- globalThis.document?.removeEventListener("visibilitychange", refreshVisibleGoal);
345
344
  delivery.reset();
346
345
  });
347
346
 
@@ -350,6 +349,8 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
350
349
  if (retry?.status === "failed" && retry.payload.goalRequest) return changeGoal(retry.payload.goalRequest.action, {}, { messageId });
351
350
  if (!canSubmit.value) return false;
352
351
  if (retry && retry.status !== "failed") return false;
352
+ if (delivery.state.messages.some(message => message.status === "uncertain" &&
353
+ text(message.payload?.message) === text((retry?.payload || payload).message))) return false;
353
354
  const request = retry?.payload?.request || payload.request;
354
355
  const requestedSteering = request?.steer === true;
355
356
  if (requestedSteering && snapshot.value.capabilities?.steering !== true) return false;
@@ -572,11 +573,6 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
572
573
  goalRead = job;
573
574
  return job;
574
575
  }
575
- function refreshVisibleGoal() {
576
- if (!globalThis.document?.hidden) void refreshGoal();
577
- }
578
- globalThis.addEventListener?.("focus", refreshVisibleGoal);
579
- globalThis.document?.addEventListener("visibilitychange", refreshVisibleGoal);
580
576
  async function changeGoal(action, input = {}, { messageId = crypto.randomUUID() } = {}) {
581
577
  const view = goalView.value;
582
578
  const target = view?.target;
@@ -665,9 +661,16 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
665
661
  if (!current.value) return false;
666
662
  if (receipt.status === "accepted") delivery.accept(messageId);
667
663
  else if (receipt.status === "not-sent" && receipt.messageId === messageId) delivery.rejectUnsent(messageId, receipt.error);
664
+ else if (receipt.status === "unknown" && message?.status === "uncertain") {
665
+ message.error = "Delivery is still unknown. Retry to check again.";
666
+ }
668
667
  subscription?.reload();
669
668
  return receipt;
670
- } catch (failure) { receiveError(failure); return false; }
669
+ } catch (failure) {
670
+ if (current.value && message?.status === "uncertain") message.error = failure.message || "Delivery could not be checked.";
671
+ receiveError(failure);
672
+ return false;
673
+ }
671
674
  finally { if (message) message.checking = false; }
672
675
  }
673
676
 
@@ -51,7 +51,7 @@ test("inspecting a restored no-admission request releases Send while preserving
51
51
  await f.command("mode", { mode: "uncertain" });
52
52
  await f.input.fill("Original unadmitted words");
53
53
  await f.input.press("Enter");
54
- const check = f.primary.getByRole("button", { name: "Check delivery", exact: true });
54
+ const check = f.primary.getByRole("button", { name: "Retry delivery check", exact: true });
55
55
  await expect(check).toBeVisible();
56
56
  const [original] = await f.sent();
57
57
  await f.input.fill("A separate newer draft");
@@ -62,7 +62,7 @@ test("inspecting a restored no-admission request releases Send while preserving
62
62
  await attach();
63
63
  await expect(f.input).toHaveValue("A separate newer draft");
64
64
  const send = f.primary.getByRole("button", { name: "Send message", exact: true });
65
- await expect(send).toBeDisabled();
65
+ await expect(send).toBeEnabled();
66
66
  await f.page.evaluate(() => window.conversationFixture.attachments(true));
67
67
  await f.primary.locator("input[type=file]").setInputFiles({ name: "newer.txt", mimeType: "text/plain", buffer: Buffer.from("Keep this upload") });
68
68
  const uploads = () => f.page.evaluate(() => window.conversationFixture.attachmentState());
@@ -222,7 +222,7 @@ test("a lost goal receipt uses the same inspection-only ledger and never starts
222
222
  await expect(start).toBeEnabled();
223
223
  await start.click();
224
224
  await f.input.click();
225
- const check = f.primary.getByRole("button", { name: "Check delivery", exact: true });
225
+ const check = f.primary.getByRole("button", { name: "Retry delivery check", exact: true });
226
226
  await expect(check).toBeVisible();
227
227
  await expect(f.primary.getByText("Finish the uncertain goal", { exact: true })).toHaveCount(1);
228
228
  const requests = async () => (await f.state()).requests.filter(request => request.suffix === "/goal" && request.method === "POST");
@@ -288,7 +288,9 @@ test("a supplied goal view keeps its pinned target and refreshes through the ret
288
288
  await expect.poll(() => reads).toBeGreaterThan(beforeReconnect);
289
289
  const beforeFocus = reads;
290
290
  await f.page.evaluate(() => window.dispatchEvent(new Event("focus")));
291
- await expect.poll(() => reads).toBeGreaterThan(beforeFocus);
291
+ await f.page.evaluate(() => document.dispatchEvent(new Event("visibilitychange")));
292
+ await f.page.waitForTimeout(100);
293
+ assert.equal(reads, beforeFocus, "Focus and visibility do not request goal state");
292
294
  await expect.poll(() => f.page.evaluate(() => window.conversationFixture.current().goalState.value.pending)).toBe(false);
293
295
  failedRead = true;
294
296
  await f.page.evaluate(() => window.conversationFixture.current().refreshGoal());
@@ -389,7 +391,7 @@ test("explicit delivery inspection acknowledges uncertain attachments without cl
389
391
  await f.command("mode", { mode: "uncertain" });
390
392
  await f.input.fill("Inspect this file.");
391
393
  await f.input.press("Enter");
392
- const check = f.primary.getByRole("button", { name: "Check delivery", exact: true });
394
+ const check = f.primary.getByRole("button", { name: "Retry delivery check", exact: true });
393
395
  await expect(check).toBeVisible();
394
396
  assert.deepEqual((await attachments()).acknowledged, []);
395
397
  await files.setInputFiles({ name: "newer.txt", mimeType: "text/plain", buffer: Buffer.from("Keep this newer file") });
@@ -620,7 +622,7 @@ test("canonical receipts settle a held response and uncertain delivery stays ins
620
622
  await f.command("mode", { mode: "uncertain" });
621
623
  await f.input.fill("Keep this exact uncertain request");
622
624
  await f.input.press("Enter");
623
- const check = f.primary.getByRole("button", { name: "Check delivery", exact: true });
625
+ const check = f.primary.getByRole("button", { name: "Retry delivery check", exact: true });
624
626
  await expect(check).toBeVisible();
625
627
  await expect(f.primary.getByText("Keep this exact uncertain request", { exact: true })).toHaveCount(1);
626
628
  await expect(f.primary.getByRole("button", { name: /^(Retry|Edit|Cancel)$/u })).toHaveCount(0);
@@ -717,7 +719,7 @@ for (const status of ["uncertain", "pending", "accepted", "failed", "unrecognise
717
719
  assert.equal((await f.sent()).length, 1, "restoration never dispatches a saved request");
718
720
  assert.equal(await f.page.evaluate(() => window.savedBinding.runtime.value.canSubmit.value), !unconfirmed);
719
721
  if (unconfirmed) {
720
- const check = f.primary.getByRole("button", { name: "Check delivery", exact: true });
722
+ const check = f.primary.getByRole("button", { name: "Retry delivery check", exact: true });
721
723
  try { await expect(check).toBeEnabled(); }
722
724
  catch (failure) {
723
725
  t.diagnostic(JSON.stringify(await f.page.evaluate(() => {
@@ -750,7 +752,7 @@ for (const status of ["uncertain", "pending", "accepted", "failed", "unrecognise
750
752
  await expect(check).toHaveCount(0);
751
753
  assert.equal((await f.sent()).length, 1, "only the actual canonical receipt settles the original identity");
752
754
  } else {
753
- await expect(f.primary.getByRole("button", { name: "Check delivery", exact: true })).toHaveCount(0);
755
+ await expect(f.primary.getByRole("button", { name: "Retry delivery check", exact: true })).toHaveCount(0);
754
756
  await expect(f.primary.getByRole("button", { name: "Retry", exact: true })).toHaveCount(status === "failed" ? 1 : 0);
755
757
  }
756
758
  });
@@ -779,7 +781,7 @@ test(`an explicit pre-admission rejection retains original retry identity and th
779
781
  assert.equal((await f.sent()).length, 1, "a definite rejection never silently resubmits steering as new work");
780
782
  assert.equal(await f.page.evaluate(id => window.conversationFixture.current().delivery.find(id).status, rejected.input.messageId), "failed");
781
783
  assert.equal(await f.page.evaluate(() => window.conversationFixture.current().canSubmit.value), true);
782
- await expect(f.primary.getByRole("button", { name: "Check delivery", exact: true })).toHaveCount(0);
784
+ await expect(f.primary.getByRole("button", { name: "Retry delivery check", exact: true })).toHaveCount(0);
783
785
  if (code === "conversation_not_steerable") {
784
786
  assert.equal(rejected.input.steer, true);
785
787
  }
@@ -924,7 +926,7 @@ test("authored application data stays captured across retry, a lost receipt and
924
926
  await f.input.fill("Keep the newer typed draft");
925
927
  await f.command("mode", { mode: "uncertain" });
926
928
  await f.primary.getByRole("button", { name: "Retry", exact: true }).click();
927
- const check = f.primary.getByRole("button", { name: "Check delivery", exact: true });
929
+ const check = f.primary.getByRole("button", { name: "Retry delivery check", exact: true });
928
930
  await expect(check).toBeVisible();
929
931
  assert.deepEqual((await f.sent())[1].input, original.input);
930
932
  assert.deepEqual(await f.page.evaluate(() => window.conversationFixture.current().delivery.state.messages[0].payload.request.data), originalData);
@@ -1066,7 +1068,7 @@ test("canonical questions retry their exact formatted reply and uncertain delive
1066
1068
  await f.input.fill("A separate newer thought.");
1067
1069
  await f.command("mode", { mode: "uncertain" });
1068
1070
  await f.primary.getByRole("button", { name: "Retry", exact: true }).click();
1069
- const check = f.primary.getByRole("button", { name: "Check delivery", exact: true });
1071
+ const check = f.primary.getByRole("button", { name: "Retry delivery check", exact: true });
1070
1072
  await expect(check).toBeVisible();
1071
1073
  const requests = await f.sent();
1072
1074
  assert.equal(requests.length, 2);
@@ -1312,7 +1314,7 @@ test("accepted draft identity survives voice retention and lost receipts without
1312
1314
  await expect(f.input).toHaveValue("Keep the typed capture.");
1313
1315
  await f.command("mode", { mode: "uncertain" });
1314
1316
  await f.input.press("Enter");
1315
- const check = f.primary.getByRole("button", { name: "Check delivery", exact: true });
1317
+ const check = f.primary.getByRole("button", { name: "Retry delivery check", exact: true });
1316
1318
  await expect(check).toBeVisible();
1317
1319
  assert.deepEqual((await f.sent())[2].input, typed.input);
1318
1320
  await expect(f.input).toHaveValue("Keep the typed capture.");
@@ -1414,7 +1416,7 @@ test("a voice preview cannot acknowledge sending or uncertain delivery and dedup
1414
1416
  await f.command("mode", { mode: "uncertain" });
1415
1417
  await f.input.fill("Keep this uncertain request.");
1416
1418
  await f.input.press("Enter");
1417
- const check = f.primary.getByRole("button", { name: "Check delivery", exact: true });
1419
+ const check = f.primary.getByRole("button", { name: "Retry delivery check", exact: true });
1418
1420
  await expect(check).toBeVisible();
1419
1421
  const second = (await f.sent())[1].input.messageId;
1420
1422
  await preview(second, "An uncertain preview is not an acknowledgement.");
@@ -1450,7 +1452,7 @@ test("an interim reply stays on its exact turn without acknowledging delivery an
1450
1452
  await f.command("mode", { mode: "uncertain" });
1451
1453
  await f.input.fill("Check the project before answering.");
1452
1454
  await f.input.press("Enter");
1453
- const check = f.primary.getByRole("button", { name: "Check delivery", exact: true });
1455
+ const check = f.primary.getByRole("button", { name: "Retry delivery check", exact: true });
1454
1456
  await expect(check).toBeVisible();
1455
1457
  const [{ input: { messageId } }] = await f.sent();
1456
1458
  const reply = { id: "native-ack", outputId: "native-ack-output", turnId: messageId,
@@ -2497,7 +2499,7 @@ test("an exact host pre-admission receipt preserves one manual retry and actor a
2497
2499
  await expect(f.primary.getByText("Failed: The host refused this message before native admission.", { exact: true })).toBeVisible();
2498
2500
  const [original] = await f.sent();
2499
2501
  assert.equal(await f.page.evaluate(id => window.conversationFixture.current().delivery.find(id).status, original.input.messageId), "failed");
2500
- await expect(f.primary.getByRole("button", { name: "Check delivery", exact: true })).toHaveCount(0);
2502
+ await expect(f.primary.getByRole("button", { name: "Retry delivery check", exact: true })).toHaveCount(0);
2501
2503
  await f.input.fill("Keep my newer draft");
2502
2504
  await f.page.evaluate(() => window.conversationFixture.data({ originId: "newer-tab", choice: "newer" }));
2503
2505
  assert.equal((await f.sent()).length, 1, "a rejection never retries automatically");
@@ -2532,7 +2534,7 @@ for (const kind of ["mismatched-id", "malformed-status", "missing-marker", "post
2532
2534
  });
2533
2535
  await f.input.fill("Do not resubmit unknown work");
2534
2536
  await f.input.press("Enter");
2535
- const check = f.primary.getByRole("button", { name: "Check delivery", exact: true });
2537
+ const check = f.primary.getByRole("button", { name: "Retry delivery check", exact: true });
2536
2538
  await expect(check).toBeVisible();
2537
2539
  const [original] = await f.sent();
2538
2540
  await check.click();
@@ -2663,7 +2665,7 @@ test("application admission keeps unknown delivery inspection-only and rejects c
2663
2665
  await f.command("mode", { mode: "uncertain" });
2664
2666
  await f.input.fill("Do not repeat unknown work");
2665
2667
  await f.input.press("Enter");
2666
- const check = f.primary.getByRole("button", { name: "Check delivery", exact: true });
2668
+ const check = f.primary.getByRole("button", { name: "Retry delivery check", exact: true });
2667
2669
  await expect(check).toBeVisible();
2668
2670
  const [original] = await f.sent();
2669
2671
  await f.input.fill("Keep my newer draft");
@@ -2892,3 +2894,47 @@ test("changing the observed native segment blocks cached goal controls until the
2892
2894
  releaseRead?.();
2893
2895
  }
2894
2896
  });
2897
+
2898
+
2899
+ test("unknown delivery reports its current result and allows different idle input without replaying the old message", options, async t => {
2900
+ const f = await fixture(t, 390);
2901
+ await f.command("mode", { mode: "unadmitted" });
2902
+ await f.input.fill("Original uncertain request");
2903
+ await f.input.press("Enter");
2904
+ const check = f.primary.getByRole("button", { name: "Retry delivery check", exact: true });
2905
+ const send = f.primary.getByRole("button", { name: "Send message", exact: true });
2906
+ await expect(check).toBeVisible();
2907
+ const bubble = f.primary.locator(".assistant-transcript__message--user").filter({ hasText: "Original uncertain request" });
2908
+ await expect(bubble.getByRole("button", { name: "Retry delivery check", exact: true })).toHaveCount(1);
2909
+ await expect(bubble).not.toHaveClass(/assistant-transcript__message--unconfirmed/u);
2910
+ const recovery = bubble.locator(".assistant-transcript__optimistic-failure");
2911
+ assert.ok(await recovery.evaluate(el => parseFloat(getComputedStyle(el).fontSize) < 14));
2912
+ await expect(check).toHaveClass(/v-btn--variant-text/u);
2913
+ await expect(f.primary.locator(".assistant-transcript__system")).toHaveCount(0);
2914
+ await expect(bubble.getByText("Request failed with status 503.", { exact: true })).toBeVisible();
2915
+ await f.input.fill("Original uncertain request");
2916
+ await expect(send).toBeDisabled();
2917
+ await f.input.fill("A different message");
2918
+ await expect(send).toBeEnabled();
2919
+ await check.click();
2920
+ await expect(f.primary.getByText("Delivery is still unknown.", { exact: false })).toBeVisible();
2921
+ await expect(f.input).toHaveValue("A different message");
2922
+ await expect(bubble.getByText("Request failed with status 503.", { exact: true })).toHaveCount(0);
2923
+ const screenshots = process.env.JSKIT_ASSISTANT_SCREENSHOTS;
2924
+ for (const width of [390, 1280]) {
2925
+ await f.page.setViewportSize({ width, height: 900 });
2926
+ await expect(bubble.getByRole("button", { name: "Retry delivery check", exact: true })).toBeVisible();
2927
+ assert.ok(await f.page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
2928
+ if (screenshots) await f.page.screenshot({ path: `${screenshots}/delivery-attached-${width}.png` });
2929
+ }
2930
+ assert.equal((await f.sent()).length, 1);
2931
+ await f.command("mode", { mode: "product-accepted" });
2932
+ await send.click();
2933
+ await expect.poll(async () => (await f.sent()).length).toBe(2);
2934
+ const [old, next] = await f.sent();
2935
+ assert.notEqual(old.input.messageId, next.input.messageId);
2936
+ assert.equal(next.input.text, "A different message");
2937
+ await expect(check).toBeVisible();
2938
+ assert.equal(await f.page.evaluate(id => window.conversationFixture.current().delivery.find(id).status,
2939
+ old.input.messageId), "uncertain");
2940
+ });