@jskit-ai/assistant-core 0.1.238 → 0.1.240

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.
@@ -8,14 +8,14 @@
8
8
  "build": "vite build"
9
9
  },
10
10
  "dependencies": {
11
- "@jskit-ai/assistant-core": "0.1.238",
12
- "@jskit-ai/http-runtime": "0.1.242",
13
- "@jskit-ai/kernel": "0.1.244",
11
+ "@jskit-ai/assistant-core": "0.1.240",
12
+ "@jskit-ai/http-runtime": "0.1.243",
13
+ "@jskit-ai/kernel": "0.1.245",
14
14
  "@mdi/js": "^7.4.47",
15
15
  "vue": "^3.5.13",
16
16
  "vuetify": "^4.0.0",
17
- "@jskit-ai/connectors-core": "0.1.61",
18
- "@jskit-ai/connectors-catalog": "0.1.62"
17
+ "@jskit-ai/connectors-core": "0.1.62",
18
+ "@jskit-ai/connectors-catalog": "0.1.64"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@vitejs/plugin-vue": "^6.0.7",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/assistant-core",
3
- "version": "0.1.238",
3
+ "version": "0.1.240",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test --test-concurrency=1"
@@ -47,8 +47,8 @@
47
47
  "@ai-sdk/perplexity": "^4.0.43",
48
48
  "@ai-sdk/togetherai": "^3.0.49",
49
49
  "@ai-sdk/xai": "^4.0.58",
50
- "@jskit-ai/resource-core": "0.1.186",
51
- "@jskit-ai/resource-crud-core": "0.1.186",
50
+ "@jskit-ai/resource-core": "0.1.187",
51
+ "@jskit-ai/resource-crud-core": "0.1.187",
52
52
  "@mdi/js": "^7.4.47",
53
53
  "@openrouter/ai-sdk-provider": "^3.0.0",
54
54
  "ai": "^7.0.101",
@@ -57,8 +57,8 @@
57
57
  "ws": "^8.20.1"
58
58
  },
59
59
  "peerDependencies": {
60
- "@jskit-ai/http-runtime": "0.1.242",
61
- "@jskit-ai/kernel": "0.1.244",
60
+ "@jskit-ai/http-runtime": "0.1.243",
61
+ "@jskit-ai/kernel": "0.1.245",
62
62
  "vue": "^3.5.13",
63
63
  "vuetify": "^4.0.0"
64
64
  },
@@ -175,7 +175,7 @@
175
175
  class="assistant-transcript__message-footer assistant-transcript__message-footer--user"
176
176
  >
177
177
  <time v-if="entry.message.displayAt">{{ entry.message.displayAt }}</time>
178
- <span v-if="entry.turn.optimistic?.status === 'pending'" role="status">Pending</span>
178
+ <span v-if="entry.turn.optimistic?.status === 'pending'" role="status">{{ entry.turn.optimistic.waiting ? `Pending: ${entry.turn.optimistic.waiting}` : "Pending" }}</span>
179
179
  </div>
180
180
  <slot name="user-message-actions" :message="entry.message" :turn="entry.turn" />
181
181
  <div
@@ -96,7 +96,8 @@ function createAssistantMessageDelivery({ deliver: defaultDeliver } = {}) {
96
96
  delivery: { messageId: message.id, error: message.error, checking: message.checking === true }
97
97
  } : null;
98
98
  return {
99
- optimistic: { error: message.error, id: message.id, status: message.status },
99
+ optimistic: { error: message.error, id: message.id, status: message.status,
100
+ ...(message.status === "pending" && message.waiting ? { waiting: message.waiting } : {}) },
100
101
  turnId: message.id,
101
102
  [role]: authored,
102
103
  ...(status ? { system: role === "system" ? { ...authored, ...status, text: `${authored.text}\n\n${status.text}` } : status } : {})
@@ -201,6 +202,12 @@ function createAssistantMessageDelivery({ deliver: defaultDeliver } = {}) {
201
202
  }
202
203
  }
203
204
 
205
+ // Explains why a pending message is held locally, or clears that reason.
206
+ function wait(messageId, reason = "") {
207
+ const message = find(messageId);
208
+ if (message?.status === "pending") message.waiting = String(reason || "").trim();
209
+ }
210
+
204
211
  function rejectUnsent(messageId, error) {
205
212
  if (find(messageId)?.status !== "uncertain") return false;
206
213
  fail(messageId, error);
@@ -253,7 +260,7 @@ function createAssistantMessageDelivery({ deliver: defaultDeliver } = {}) {
253
260
  state.sending = false;
254
261
  }
255
262
 
256
- return { accept, cancel, edit, find, reconcile, remove, resend, reset, restoreUncertain, rejectUnsent, send, state, turns };
263
+ return { accept, cancel, edit, find, reconcile, remove, resend, reset, restoreUncertain, rejectUnsent, send, state, turns, wait };
257
264
  }
258
265
 
259
266
  export { createAssistantMessageDelivery, unmatchedOptimisticMessages };
@@ -893,6 +893,14 @@ export function createClaudeConversationOwner({
893
893
  return { status: "available", threadId: entry.id, goal: await entry.goals.read(history), goalCommandId: history.goalCommandId || null };
894
894
  }
895
895
 
896
+ // A native Set, Resume or Cancel supersedes a retained Pause or blocker.
897
+ async function releaseRetainedGoal(entry) {
898
+ if (!entry.pausedGoal && !entry.blockedGoal) return;
899
+ entry.pausedGoal = null;
900
+ entry.blockedGoal = null;
901
+ await store.save(entry);
902
+ }
903
+
896
904
  async function updateGoal(entry, input, operations) {
897
905
  await bindAccount(entry);
898
906
  const call = applicationCall.getStore();
@@ -928,7 +936,7 @@ export function createClaudeConversationOwner({
928
936
  for (;;) {
929
937
  const history = await readHistory(entry);
930
938
  if (history.goalCommandId === claudeNativeMessageId(messageId)) {
931
- if (entry.pausedGoal || entry.blockedGoal) { entry.pausedGoal = null; entry.blockedGoal = null; await store.save(entry); }
939
+ await releaseRetainedGoal(entry);
932
940
  result = { status: "available", threadId: entry.id, goal: await entry.goals.read(history), goalCommandId: history.goalCommandId };
933
941
  break;
934
942
  }
@@ -1124,7 +1132,7 @@ export function createClaudeConversationOwner({
1124
1132
  if (!binding) return { value: delivered.value };
1125
1133
  }
1126
1134
 
1127
- return Object.freeze({ applicationToolsSupported: true, entries, closingSessions, get closing() { return closing; }, createAccountQueries, open, acquire, createConversation, snapshot, readRetainedTurn, readFinalAssistantResult, readHistory, ensureReady, ensureProcess, readContextUsage, compactContext, stopForTerminal, prepareTerminal, send, startTurn,
1135
+ return Object.freeze({ applicationToolsSupported: true, entries, closingSessions, get closing() { return closing; }, createAccountQueries, open, acquire, createConversation, snapshot, readRetainedTurn, readFinalAssistantResult, readHistory, ensureReady, ensureProcess, readContextUsage, compactContext, releaseRetainedGoal, stopForTerminal, prepareTerminal, send, startTurn,
1128
1136
  interrupt, read, inspectAdmission, hasActiveTemporaryConversation, hasActiveConversation, retireConversationHistory, reconcileSessions, restoreSessionEntries, closeSession, closeProject, invalidateRuntimes, drain, forget, close, deleteConversation, recover, stopSelected, wait, stopConversation,
1129
1137
  readGoal, updateGoal, renewal, run, steer, cancel });
1130
1138
  }
@@ -117,7 +117,7 @@ export function createCodexGoalCommands({
117
117
  params: { goal: result.goal }
118
118
  });
119
119
  await publish(sessionId, { reason: "codex-goal", nativeGoal: { threadId, goal: result.goal || null } });
120
- return { ok: true, status: "available", goal: result.goal || null };
120
+ return { ok: true, status: "available", threadId, goal: result.goal || null };
121
121
  }
122
122
 
123
123
 
@@ -170,6 +170,8 @@ export function createClaudeConversationDriver({ connections, host = {}, limits
170
170
  if (event.type === "message") await current.onMessage({ ...event.message, transient: !event.message.complete });
171
171
  else if (event.type === "message-complete") await current.onEvent(event);
172
172
  else if ((event.type === "admitted" && current.goal) || event.type === "settled") {
173
+ // Claude has the authored /goal command; it now owns the goal state.
174
+ if (event.type === "admitted" && current.goal.action !== "pause") await standalone.releaseRetainedGoal(entry);
173
175
  await current.onEvent({ type: "goal", goal: claudeGoalValue(await entry.goals.read(), entry.id) });
174
176
  } else if (event.type === "phase") await current.onEvent({ type: "phase", phase: ["compacting", "background"].includes(event.phase) ? event.phase : "working" });
175
177
  }
@@ -933,7 +933,7 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
933
933
  createdAt: goal?.createdAt, objective: input.action === "set" ? input.objective : goal?.objective };
934
934
  }
935
935
  result = await owner.updateGoal(sessionId, command, {
936
- context: nativeContext, signal: context.signal, prepareConversation: () => goalContext(configuration, context, undefined, { prepareThread: true })
936
+ context: nativeContext, signal: context?.signal, prepareConversation: () => goalContext(configuration, context, undefined, { prepareThread: true })
937
937
  });
938
938
  if (result.ok !== false) binding.goal = result.goal || null;
939
939
  } finally { admission.release(); }
@@ -957,7 +957,7 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
957
957
  if ((goalValue(goal)?.id || null) !== (input.expectedGoalId || null)) throw new Error("The native goal changed. Read it again before changing it.");
958
958
  const result = await owner.updateGoal(sessionId, { ...input, threadId: binding.threadId,
959
959
  createdAt: goal?.createdAt, objective: input.action === "set" ? input.objective : goal?.objective }, {
960
- context: nativeContext, signal: context.signal,
960
+ context: nativeContext, signal: context?.signal,
961
961
  prepareConversation: () => goalContext(configuration, context, signal, { prepareThread: true, tools })
962
962
  });
963
963
  if (result.ok === false) throw new Error(result.error);
@@ -10,7 +10,7 @@ const NATIVE_AI_PROVIDERS = Object.freeze([
10
10
  claudeAutoCompactWindow: 786432, webSearch: false,
11
11
  models: Object.freeze([
12
12
  model("deepseek-flash", 1048576, ["low", "high", "max"], { images: true, freeformPatch: true, codexHistoryRouting: true }),
13
- model("deepseek-v4-pro", 1048576, ["low", "high", "max"], { freeformPatch: true })
13
+ model("deepseek-v4-pro", 1048576, ["low", "high", "max"], { freeformPatch: true, codexHistoryRouting: true })
14
14
  ])
15
15
  }),
16
16
  Object.freeze({
@@ -13,6 +13,19 @@ import {
13
13
  const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
14
14
  const FIXTURE_ROOT = path.resolve(TEST_DIRECTORY, "../fixtures/responsive-assistant");
15
15
  const RUN_BROWSER_TEST = process.env.JSKIT_ASSISTANT_CORE_BROWSER_INTEGRATION === "1";
16
+ // Turns use content-visibility estimates. Moving the reader materializes rows
17
+ // near the new position, and Chromium's scroll anchoring then shifts scrollTop
18
+ // to keep the same content in view. Hold a reading position only once that
19
+ // layout has settled, so later checks measure the conversation, not the estimates.
20
+ async function settleReaderAt(body, scrollTop) {
21
+ await expect.poll(() => body.evaluate(async (element, top) => {
22
+ element.scrollTop = top;
23
+ const height = element.scrollHeight;
24
+ await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
25
+ return element.scrollTop === top && element.scrollHeight === height;
26
+ }, scrollTop)).toBe(true);
27
+ }
28
+
16
29
  const VIEWPORTS = Object.freeze([
17
30
  Object.freeze({ name: "phone", width: 390, height: 844 }),
18
31
  Object.freeze({ name: "compact", width: 800, height: 900 }),
@@ -405,6 +418,14 @@ test("upward scrolling loads older history once and preserves retry and selectio
405
418
  await expect(page.getByText("Conversation line 70:", { exact: false })).toBeVisible();
406
419
  await expect(requests).toHaveText("History requests: 3");
407
420
  await external("Exhaust history");
421
+ // Removing the history control re-anchors the scroller, which would
422
+ // cancel a keyboard scroll already in flight; press Home once it settles.
423
+ await expect(page.getByRole("button", { name: "Load older messages", exact: true })).toHaveCount(0);
424
+ await expect.poll(() => body.evaluate(async element => {
425
+ const height = element.scrollHeight;
426
+ await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
427
+ return element.scrollHeight === height;
428
+ })).toBe(true);
408
429
  await expect.poll(async () => {
409
430
  await body.focus();
410
431
  return body.evaluate(element => document.activeElement === element);
@@ -412,7 +433,6 @@ test("upward scrolling loads older history once and preserves retry and selectio
412
433
  await body.press("Home");
413
434
  await expect.poll(() => body.evaluate(element => element.scrollTop)).toBe(0);
414
435
  await expect(requests).toHaveText("History requests: 3");
415
- await expect(page.getByRole("button", { name: "Load older messages", exact: true })).toHaveCount(0);
416
436
  assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
417
437
  assert.deepEqual(errors, []);
418
438
  await page.close();
@@ -1235,15 +1255,18 @@ test("connection warnings keep avatar controls and Reload reachable without repl
1235
1255
  await body.hover();
1236
1256
  await page.mouse.wheel(0, -1000);
1237
1257
  await expect.poll(() => body.evaluate(element => element.scrollHeight - element.clientHeight - element.scrollTop)).toBeGreaterThan(50);
1238
- await body.evaluate(element => { element.scrollTop = 420; window.errorAvatarBody = element; });
1239
- await expect.poll(() => body.evaluate(element => element.scrollTop)).toBe(420);
1258
+ await settleReaderAt(body, 420);
1259
+ await body.evaluate(element => { window.errorAvatarBody = element; });
1240
1260
  const history = await body.locator(".assistant-transcript__turn").allTextContents();
1241
1261
  let reloads = 0;
1242
1262
  for (const size of ["standard", "large", "hidden"]) {
1243
1263
  await page.getByRole("combobox", { name: "Avatar preference", exact: true }).selectOption(size);
1244
- // Establish the reading position after changing the fixture-only avatar preference.
1245
- await body.evaluate(element => { element.scrollTop = 420; });
1246
- await expect.poll(() => body.evaluate(element => element.scrollTop)).toBe(420);
1264
+ // Establish the reading position after changing the fixture-only avatar
1265
+ // preference. A reader's own wheel detaches the view from the latest
1266
+ // message even if a slow initial bottom scroll resumed following.
1267
+ await body.hover();
1268
+ await page.mouse.wheel(0, -200);
1269
+ await settleReaderAt(body, 420);
1247
1270
  await external("Show update error");
1248
1271
  await expect(conversation).toHaveClass(/assistant-conversation--error/u);
1249
1272
  await expect(avatar).toHaveCSS("position", "absolute");
@@ -320,7 +320,13 @@ async function fixture(t, options = {}) {
320
320
  attachment: { type: "goal_status", condition, sentinel: true, met: condition === "clear" } });
321
321
  if (process.env.TEST_GOAL_LOST_ACK) return;
322
322
  emit(frame);
323
- if (condition === "clear") emit({ type: "result", subtype: "success", result: "" });
323
+ if (condition === "clear") {
324
+ // Claude 2.1.289 saves the clear's local-command receipt and ends it
325
+ // with a result naming the command and the authored message.
326
+ record({ type: "system", subtype: "local_command", uuid: frame.uuid + "-clear", parentUuid: frame.uuid,
327
+ commandRun: { command: "goal", args: "clear" }, content: "<local-command-stdout>Goal cleared: prior goal</local-command-stdout>" });
328
+ emit({ type: "result", subtype: "success", result: "", local_command: "goal", user_message_uuid: frame.uuid });
329
+ }
324
330
  return;
325
331
  }
326
332
  emit(frame);
@@ -1167,8 +1173,9 @@ test("Claude native goal commands use ordinary admission, pause retains the goal
1167
1173
  assert.equal(events.filter(event => event.type === "accepted").length, 3);
1168
1174
  assert.equal(events.some(event => event.type === "goal" && event.goal === null), true);
1169
1175
  const trace = await f.trace();
1176
+ // Resume starts a new process, which first suspends the retained paused checker.
1170
1177
  assert.deepEqual(trace.filter(row => row.frame?.type === "user").map(row => row.frame.message.content),
1171
- ["/goal Tests pass", "/goal Tests pass", "/goal clear"]);
1178
+ ["/goal Tests pass", "/goal clear", "/goal Tests pass", "/goal clear"]);
1172
1179
  const starts = trace.filter(row => row.args?.includes("--print"));
1173
1180
  assert.equal(starts.length, 3);
1174
1181
  assert.equal(starts[1].args.includes("--resume"), true);
@@ -1229,7 +1229,9 @@ async function fixture(t, options = {}) {
1229
1229
  t.after(async () => {
1230
1230
  try { for (const runtime of runtimes) await runtime.close(); }
1231
1231
  finally {
1232
- if (!options.execution) await execution.close();
1232
+ // A supplied execution host is closed here too (its test may close it
1233
+ // again): its native processes must stop before their folder is removed.
1234
+ await execution.close();
1233
1235
  await rm(directory, { recursive: true, force: true });
1234
1236
  }
1235
1237
  });
@@ -267,7 +267,7 @@ async function compactedFixture(t, { maxRequestBytes } = {}) {
267
267
  test("mixed Codex recovery keeps exact text and tool facts without changing native history or current instructions", async (t) => {
268
268
  const fixture = await compactedFixture(t);
269
269
  const native = await readFile(fixture.historyPath);
270
- for (const [destination, model] of [["deepseek", "deepseek-flash"], ["zai-coding-plan", "glm-5.3"]]) {
270
+ for (const [destination, model] of [["deepseek", "deepseek-flash"], ["deepseek", "deepseek-v4-pro"], ["zai-coding-plan", "glm-5.3"]]) {
271
271
  const current = { type: "message", role: "developer", content: [{ type: "input_text", text: "CURRENT_INSTRUCTIONS" }] };
272
272
  const body = { model, input: [current, fixture.compacted, foreign], tools: [{ type: "function", name: "exec_command" }] };
273
273
  const original = structuredClone(body);
@@ -289,7 +289,7 @@ test("mixed Codex recovery keeps exact text and tool facts without changing nati
289
289
  assert.deepEqual(body, original);
290
290
  }
291
291
  assert.deepEqual(await readFile(fixture.historyPath), native);
292
- assert.equal(fixture.calls.length, 2, "one upstream request per user request");
292
+ assert.equal(fixture.calls.length, 3, "one upstream request per user request");
293
293
  });
294
294
 
295
295
  test("compacted native agent messages retain attribution, readable content and explicit opaque-content markers", async (t) => {
@@ -358,9 +358,11 @@ test("compacted screenshots stay visible and associated with their original mess
358
358
  assert.match(content[3].text, /Archived image 2/);
359
359
  assert.equal(content.at(-1).text, "[/Archived conversation]");
360
360
  assert.deepEqual(await readFile(fixture.historyPath), native);
361
- const unsupported = await fixture.send({ model: "glm-5.3", input: [fixture.compacted] }, "zai-coding-plan");
362
- assert.equal(unsupported.status, 422);
363
- assert.match((await unsupported.json()).error.message, /does not support images/);
361
+ for (const [model, destination] of [["glm-5.3", "zai-coding-plan"], ["deepseek-v4-pro", "deepseek"]]) {
362
+ const unsupported = await fixture.send({ model, input: [fixture.compacted] }, destination);
363
+ assert.equal(unsupported.status, 422);
364
+ assert.match((await unsupported.json()).error.message, /does not support images/);
365
+ }
364
366
  assert.equal(fixture.calls.length, 1);
365
367
  });
366
368