@jskit-ai/assistant-core 0.2.17 → 0.2.19

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/CHANGELOG.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - The API driver's state note, which carries the current time, follows the conversation as its last message instead of ending the system prompt, so the provider's prompt cache keeps the whole history from one turn to the next instead of charging it uncached on each turn's first model call (`@jskit-ai/agent` `runLoop`).
5
6
  - Assistant failures that reach a person translate: 63 more undotted codes render from `jskit.errors.<code>`, and 98 sentences that share a code with others, or are sent without one, carry a `messageCode` (`assistant.claude.*`, `assistant.codex.*`, `assistant.opencode.*`, `assistant.conversation.*`, `assistant.turn.*`). The English messages are unchanged.
6
7
  - The OpenCode version mismatch, Claude control-request timeout and unreadable saved Claude conversation errors carry `params` for their catalog English.
7
8
  - The assistant settings resource sends its system prompt field messages in the coded `{ code, message, params }` form.
@@ -8,14 +8,14 @@
8
8
  "build": "vite build"
9
9
  },
10
10
  "dependencies": {
11
- "@jskit-ai/assistant-core": "0.2.17",
12
- "@jskit-ai/http-runtime": "0.2.7",
13
- "@jskit-ai/kernel": "0.2.7",
11
+ "@jskit-ai/assistant-core": "0.2.19",
12
+ "@jskit-ai/http-runtime": "0.2.9",
13
+ "@jskit-ai/kernel": "0.2.9",
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.2.8",
18
- "@jskit-ai/connectors-catalog": "0.2.8"
17
+ "@jskit-ai/connectors-core": "0.2.10",
18
+ "@jskit-ai/connectors-catalog": "0.2.10"
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.2.17",
3
+ "version": "0.2.19",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test --test-concurrency=1"
@@ -50,9 +50,9 @@
50
50
  "@ai-sdk/perplexity": "^4.0.43",
51
51
  "@ai-sdk/togetherai": "^3.0.49",
52
52
  "@ai-sdk/xai": "^4.0.58",
53
- "@jskit-ai/agent": "0.1.0",
54
- "@jskit-ai/resource-core": "0.1.198",
55
- "@jskit-ai/resource-crud-core": "0.1.198",
53
+ "@jskit-ai/agent": "0.1.1",
54
+ "@jskit-ai/resource-core": "0.1.200",
55
+ "@jskit-ai/resource-crud-core": "0.1.200",
56
56
  "@mdi/js": "^7.4.47",
57
57
  "@openrouter/ai-sdk-provider": "^3.0.0",
58
58
  "ai": "^7.0.152",
@@ -61,8 +61,8 @@
61
61
  "ws": "^8.20.1"
62
62
  },
63
63
  "peerDependencies": {
64
- "@jskit-ai/http-runtime": "0.2.7",
65
- "@jskit-ai/kernel": "0.2.7",
64
+ "@jskit-ai/http-runtime": "0.2.9",
65
+ "@jskit-ai/kernel": "0.2.9",
66
66
  "vue": "^3.5.13",
67
67
  "vuetify": "^4.0.0"
68
68
  },
@@ -1,4 +1,4 @@
1
- import { codexAppServerTurnStatusIsActive, codexAppServerTurnStatusIsSuccessfulComplete, codexAppServerTurnStatusIsProviderFailure, CODEX_APP_SERVER_AGENT_RUN_ID, CODEX_AGENT_PROVIDER, CODEX_APP_SERVER_RUN_STATE } from "./codexTurnState.js";
1
+ import { codexAppServerTurnStatusIsActive, codexAppServerTurnStatusIsSuccessfulComplete, codexAppServerTurnStatusIsProviderFailure, codexAppServerTurnKey, CODEX_APP_SERVER_AGENT_RUN_ID, CODEX_AGENT_PROVIDER, CODEX_APP_SERVER_RUN_STATE } from "./codexTurnState.js";
2
2
  import { codexAppServerProviderConnectionGeneration } from "./codexModelCatalog.js";
3
3
  import {
4
4
  codexAppServerAssistantItemText,
@@ -239,6 +239,8 @@ export function createCodexNotificationObserver({
239
239
  threadId: normalizedThreadId,
240
240
  turnId: codexAppServerNotificationTurnId(notification)
241
241
  };
242
+ // Anything Codex reports about a turn means it has started that turn.
243
+ journal.unstartedTurns.delete(codexAppServerTurnKey(normalizedThreadId, notificationContext.turnId));
242
244
  if (method === "thread/tokenUsage/updated") {
243
245
  notificationQueue.run(notificationContext, async () => {
244
246
  const store = await createStore(normalizedSessionId);
@@ -1,4 +1,4 @@
1
- import { codexAppServerTurnStatusIsSuccessfulComplete, codexAppServerTurnStatusIsProviderFailure, codexAppServerErrorMessage, codexAppServerSteerFailure, codexAppServerMessageRequiresNewTurn, codexAppServerMessageDeferred } from "./codexTurnState.js";
1
+ import { codexAppServerTurnStatusIsComplete, codexAppServerTurnStatusIsSuccessfulComplete, codexAppServerTurnStatusIsProviderFailure, codexAppServerErrorMessage, codexAppServerSteerFailure, codexAppServerMessageRequiresNewTurn, codexAppServerMessageDeferred, codexAppServerTurnKey } from "./codexTurnState.js";
2
2
  import { codexAppServerRequestIsInvalid } from "./codexProviderThreads.js";
3
3
  import { codexLocalImageInput, codexAppServerDeliveredTurn, sendCodexAppServerPrompt } from "./codexProviderTurns.js";
4
4
  import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
@@ -74,6 +74,10 @@ export function createCodexPromptDispatch({
74
74
  if (!deliveredTurnId) {
75
75
  throw new Error("Codex app-server accepted the prompt without returning a turn id.");
76
76
  }
77
+ // Codex answers turn/start before it starts the turn; its notifications about the turn say when it has.
78
+ if (!codexAppServerTurnStatusIsComplete(deliveredTurnStatus)) {
79
+ journal.unstartedTurns.add(codexAppServerTurnKey(threadId, deliveredTurnId));
80
+ }
77
81
  delivery.pendingUserMessage(promptSessionId, clientUserMessageId)?.onNativeReceipt();
78
82
  await recovery.markTurnActive(sessionId, {
79
83
  requireTrackedTurn: true,
@@ -88,6 +88,8 @@ export function createCodexRunJournal({
88
88
  isClosing
89
89
  }) {
90
90
  const codexAppServerCompletedTurns = new Set();
91
+ // Turns Codex accepted with turn/start but has not yet reported: until it does, its thread still reads idle.
92
+ const codexAppServerUnstartedTurns = new Set();
91
93
  const codexAppServerProcessedTurns = new Set();
92
94
  const codexAppServerActiveTimers = new Map();
93
95
  const codexAppServerFinalizingTimers = new Map();
@@ -637,6 +639,7 @@ export function createCodexRunJournal({
637
639
 
638
640
  return Object.freeze({
639
641
  completedTurns: codexAppServerCompletedTurns,
642
+ unstartedTurns: codexAppServerUnstartedTurns,
640
643
  processedTurns: codexAppServerProcessedTurns,
641
644
  activeTimers: codexAppServerActiveTimers,
642
645
  finalizingTimers: codexAppServerFinalizingTimers,
@@ -1,4 +1,4 @@
1
- import { codexAppServerTurnStatusIsActive, codexAppServerTurnStatusIsComplete, codexAppServerTurnStatusIsSuccessfulComplete, codexAppServerTurnStatusIsProviderFailure, codexAppServerThreadStatus, codexAppServerReadThreadStatus, codexAppServerAgentRun, codexAppServerTurnCanReceiveProviderCompletion, codexAppServerTurnCanAdoptSuccessor, codexAppServerErrorMessage } from "./codexTurnState.js";
1
+ import { codexAppServerTurnStatusIsActive, codexAppServerTurnStatusIsComplete, codexAppServerTurnStatusIsSuccessfulComplete, codexAppServerTurnStatusIsProviderFailure, codexAppServerThreadStatus, codexAppServerReadThreadStatus, codexAppServerAgentRun, codexAppServerTurnCanReceiveProviderCompletion, codexAppServerTurnCanAdoptSuccessor, codexAppServerErrorMessage, codexAppServerTurnKey } from "./codexTurnState.js";
2
2
  import { isRecord, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
3
3
  import { codexAppServerStatusFromValue } from "./codexEvents.js";
4
4
  import { codexAppServerThreadRawValue, codexAppServerThreadTurnId, codexAppServerThreadError } from "./codexThreadSnapshot.js";
@@ -200,6 +200,27 @@ export function createCodexThreadStatus({ journal, settlement, recovery, output,
200
200
  turnId
201
201
  };
202
202
  }
203
+ // Codex has accepted the tracked turn but not started it. Until it does, its thread reads idle and lists the
204
+ // turn as interrupted, so only Codex's own report about the turn settles it.
205
+ if (
206
+ trackedProviderTurn?.state === "active" &&
207
+ !codexAppServerTurnStatusIsActive(status) &&
208
+ journal.unstartedTurns.has(codexAppServerTurnKey(trackedProviderTurn.threadId, trackedProviderTurn.turnId))
209
+ ) {
210
+ debugLog("appServerThread.reconcile.awaitingProviderStart", {
211
+ sessionId: normalizedSessionId,
212
+ source: normalizeText(source),
213
+ status,
214
+ threadId: trackedProviderTurn.threadId,
215
+ turnId: trackedProviderTurn.turnId
216
+ });
217
+ return {
218
+ ok: true,
219
+ reason: "provider_turn_not_started",
220
+ status: trackedProviderTurn.status || "inProgress",
221
+ turnId: trackedProviderTurn.turnId
222
+ };
223
+ }
203
224
  if (
204
225
  trackedProviderTurn &&
205
226
  turnId &&
@@ -1,4 +1,4 @@
1
- import { codexAppServerTurnStatusIsActive, codexAppServerTurnStatusIsComplete, codexAppServerTurnStatusIsSuccessfulComplete, codexAppServerTurnStatusIsProviderFailure, codexAppServerThreadStatus, codexAppServerReadThreadStatus, codexAppServerAgentRun, codexAppServerPendingUserMessageOwnership, codexAppServerTurnCanAdoptSuccessor } from "./codexTurnState.js";
1
+ import { codexAppServerTurnStatusIsActive, codexAppServerTurnStatusIsComplete, codexAppServerTurnStatusIsSuccessfulComplete, codexAppServerTurnStatusIsProviderFailure, codexAppServerThreadStatus, codexAppServerReadThreadStatus, codexAppServerAgentRun, codexAppServerPendingUserMessageOwnership, codexAppServerTurnCanAdoptSuccessor, codexAppServerTurnKey } from "./codexTurnState.js";
2
2
  import { codexAppServerProviderTurnForOperation, codexAppServerThreadTurnId, codexAppServerThreadError } from "./codexThreadSnapshot.js";
3
3
  import { createCodexTurnActivity } from "./codexTurnActivity.js";
4
4
  import { createCodexTurnEndings } from "./codexTurnEndings.js";
@@ -270,6 +270,17 @@ export function createCodexTurnRecovery({
270
270
  if (!codexAppServerTurnStatusIsComplete(status)) {
271
271
  return currentSession;
272
272
  }
273
+ if (journal.unstartedTurns.has(codexAppServerTurnKey(currentTurn.threadId, currentTurn.turnId))) {
274
+ // Codex has accepted this turn but not started it, and its thread reads idle until it does.
275
+ debugLog("appServerTurn.reconcile.awaitingProviderStart", {
276
+ sessionId,
277
+ status,
278
+ threadId: currentTurn.threadId,
279
+ turnId: currentTurn.turnId
280
+ });
281
+ scheduleCodexAppServerActiveRecovery(sessionId);
282
+ return currentSession;
283
+ }
273
284
  const completedTurnId = providerTurnId || currentTurn.turnId;
274
285
  debugLog("appServerTurn.reconcile.complete", {
275
286
  sessionId,
@@ -6,7 +6,8 @@ import { createApiConversationDriver } from "../src/server/conversation/provider
6
6
  const CHECKS = checkTexts("the app");
7
7
  const frame = (delta, finish_reason) => `data: ${JSON.stringify({ choices: [{ index: 0, delta, finish_reason }] })}\n\n`;
8
8
  const schema = tool => ({ type: "function", function: { name: tool.name, parameters: tool.parameters } });
9
- const lastText = body => body.messages.at(-1).content;
9
+ // The engine's state note ends every request's conversation; what the app sent last comes before it.
10
+ const lastText = body => body.messages.at(-2).content;
10
11
 
11
12
  // One step of a streamed reply: words, an action call or both. Calls the engine
12
13
  // makes without streaming (accounts, checks, summaries) answer with `once`.
@@ -113,7 +114,10 @@ test("admission precedes every model call, the state note gives the time, and a
113
114
  await f.run("What day is it today?");
114
115
  assert.deepEqual(f.order, ["accept", "model"]);
115
116
  assert.deepEqual(f.executions, [], "No action runs before the model asks for one");
116
- assert.match(f.requests[0].messages[0].content, /^Use the supplied actions\.\n\nCurrent state, from the system\. It is the truth; earlier messages may be out of date:\n- The time: .+ UTC\. The person may be in another time zone\.$/u);
117
+ assert.equal(f.requests[0].messages[0].content, "Use the supplied actions.", "The state note is not in the system prompt");
118
+ assert.equal(f.requests[0].messages.at(-1).role, "user");
119
+ assert.match(f.requests[0].messages.at(-1).content, /^Current state, from the system\. It is the truth; earlier messages may be out of date:\n- The time: .+ UTC\. The person may be in another time zone\.$/u,
120
+ "The state note follows the conversation, so the history before it stays in the provider's prompt cache");
117
121
  const denied = fixture({ authorize: () => { throw new Error("Connection access revoked"); } });
118
122
  await assert.rejects(denied.run(), /Connection access revoked/);
119
123
  assert.deepEqual(denied.requests, []);
@@ -223,7 +227,7 @@ test("a long history is compacted: the summary and its last turn are saved, and
223
227
  assert.ok(kept.length > 0 && kept.length < turns.length);
224
228
  const [request] = f.streamed;
225
229
  assert.match(request.messages[0].content, /Summary of the earlier part of this conversation[^]*The person asked six long questions\./u);
226
- assert.deepEqual(request.messages.filter(message => message.role === "user").map(message => message.content),
230
+ assert.deepEqual(request.messages.slice(0, -1).filter(message => message.role === "user").map(message => message.content),
227
231
  [...kept.map(turn => turn.user.text), "Next question"]);
228
232
  assert.deepEqual(phases, ["compacting", "working"]);
229
233
  });
@@ -248,5 +252,5 @@ test("a short history is read whole with its stored summary, and nothing is save
248
252
  assert.deepEqual(saved, []);
249
253
  assert.equal(f.requests.length, 1);
250
254
  assert.match(f.requests[0].messages[0].content, /Summary of the earlier part of this conversation[^]*Earlier: hello\./u);
251
- assert.deepEqual(f.requests[0].messages.slice(1).map(message => message.content), ["Hi", "Hello", "Next question"]);
255
+ assert.deepEqual(f.requests[0].messages.slice(1, -1).map(message => message.content), ["Hi", "Hello", "Next question"]);
252
256
  });
@@ -1910,6 +1910,51 @@ test("original dispatch steering preserves deferred versus new-turn recovery and
1910
1910
  }
1911
1911
  });
1912
1912
 
1913
+ // Codex answers turn/start before it starts the turn. Until its turn/started notification, codex-cli 0.163.0 reads
1914
+ // the thread as idle and lists the new turn as interrupted, so neither read means the turn ended. On 2026-11-06 the
1915
+ // starting watchdog's status read landed in that gap: the turn was marked finished 0.46 s after delivery, and Codex's
1916
+ // answer 33 s later was dropped behind "Codex finished, but Vibe64 could not recover its final response".
1917
+ for (const read of ["starting watchdog", "status check"]) {
1918
+ test(`a delivered turn Codex has not started stays active through a ${read} and keeps its answer`, async t => {
1919
+ const notices = [];
1920
+ const f = await receiptFixture({
1921
+ finalizingGraceMs: 30,
1922
+ outcomeNotice(_runtime, _sessionId, _threadId, _turnId, outcome) { notices.push(outcome); }
1923
+ });
1924
+ t.after(() => f.owner.clearSessionRecoveryTimers(f.sessionId));
1925
+ const context = dispatchContext(f);
1926
+ let codexStatus = "idle";
1927
+ f.run = { ...f.run, state: "starting", providerTurnId: "", providerStatus: "", outerTurnId: context.messageId };
1928
+ f.provider.sendTurn = async () => ({ id: f.turnId, status: "inProgress" });
1929
+ f.provider.readThreadStatus = async () => ({ raw: { status: { type: codexStatus } } });
1930
+ f.history = [turn(f.turnId, "interrupted")];
1931
+ f.observe();
1932
+ assert.equal((await f.owner.startTurn(f.sessionId, {}, context)).providerFailure, "");
1933
+ assert.equal(f.run.state, "active");
1934
+ if (read === "starting watchdog") {
1935
+ await f.owner.recoverActiveTurn(f.sessionId, { provider: f.provider, retryOnError: false });
1936
+ } else {
1937
+ await f.owner.reconcileThreadStatus(f.sessionId, f.provider, f.threadId, {
1938
+ observeLatestTurn: true, source: "wellbeing"
1939
+ });
1940
+ }
1941
+ await f.drain();
1942
+ assert.equal(f.run.state, "active", "An idle read before turn/started does not end the turn");
1943
+ assert.equal(f.run.providerTurnId, f.turnId);
1944
+ codexStatus = "active";
1945
+ f.history = [turn(f.turnId, "inProgress")];
1946
+ f.emit({ method: "turn/started", params: { threadId: f.threadId, turn: { id: f.turnId, status: "inProgress" } } });
1947
+ await f.drain();
1948
+ codexStatus = "idle";
1949
+ f.history = [turn(f.turnId, "completed", "The complete reply.")];
1950
+ f.emit({ method: "turn/completed", params: { threadId: f.threadId, turn: { id: f.turnId, status: "completed" } } });
1951
+ await f.drain();
1952
+ assert.equal(f.run.state, "completed");
1953
+ assert.deepEqual((await f.replies()).map(row => row.assistant.text), ["The complete reply."]);
1954
+ assert.deepEqual(notices, []);
1955
+ });
1956
+ }
1957
+
1913
1958
  function controlContext(f, overrides = {}) {
1914
1959
  return {
1915
1960
  runtime: f.runtime,
@@ -65,7 +65,7 @@ test("an existing API client keeps opaque receipts path-safe and selected histor
65
65
  assert.deepEqual(requests[0][1], { role: "user", content: [
66
66
  { type: "text", text: "Selected prior context" }, { type: "text", text: "Authorized historical bytes" }
67
67
  ] });
68
- assert.deepEqual(requests[0].at(-1), { role: "user", content: [
68
+ assert.deepEqual(requests[0].at(-2), { role: "user", content: [
69
69
  { type: "text", text: "Current question" }, { type: "text", text: "Authorized current bytes" }
70
70
  ] });
71
71
  const files = await readdir(directory);
@@ -84,8 +84,9 @@ test("an existing API client keeps opaque receipts path-safe and selected histor
84
84
  context.history = [];
85
85
  await conversation.send({ messageId: "empty-context", text: "No prior model context" });
86
86
  const last = await conversation.wait();
87
- assert.match(requests[2][0].content, /^Existing instructions\n\nCurrent state, from the system\./u);
88
- assert.deepEqual(requests[2].slice(1).map(message => message.content), ["No prior model context"]);
87
+ assert.equal(requests[2][0].content, "Existing instructions");
88
+ assert.deepEqual(requests[2].slice(1, -1).map(message => message.content), ["No prior model context"]);
89
+ assert.match(requests[2].at(-1).content, /^Current state, from the system\./u, "The engine's state note follows the conversation");
89
90
  assert.equal(last.conversationLog.length, 3, "Explicitly empty model context never removes saved conversation history");
90
91
  await assert.rejects(conversation.send({ messageId: "x".repeat(129), text: "Must not dispatch" }), { code: "conversation_invalid_message" });
91
92
  assert.equal(requests.length, 3);
@@ -147,7 +147,7 @@ test("authorized image bytes reach the API once and only safe receipts enter sto
147
147
  assert.equal(f.reads[0].context, context);
148
148
  assert.equal(f.reads[0].conversationId, "one");
149
149
  assert.deepEqual(f.reads[0].attachmentIds, [receipt.attachmentId]);
150
- assert.deepEqual(f.requests[0].messages.at(-1).content, [
150
+ assert.deepEqual(f.requests[0].messages.at(-2).content, [
151
151
  { type: "text", text: input.text }, { type: "image_url", image_url: { url: `data:image/png;base64,${image.toString("base64")}` } }
152
152
  ]);
153
153
  assert.deepEqual(state.conversationLog[0].user.attachments, [receipt]);
@@ -174,7 +174,7 @@ test("API history and continuity reauthorize files after reopen and renewal", as
174
174
  await resumed.conversation.replace({ operationId: "renew", reason: "renewal", expectedSegmentId: first.segmentId });
175
175
  await resumed.conversation.send({ messageId: "third", text: "Look again" });
176
176
  await resumed.conversation.wait();
177
- assert.equal(resumed.requests[1].messages.at(-1).content[1].type, "image_url");
177
+ assert.equal(resumed.requests[1].messages.at(-2).content[1].type, "image_url");
178
178
  allowed = false;
179
179
  await assert.rejects(resumed.conversation.send({ messageId: "denied", text: "Look again" }), /attachment access/);
180
180
  assert.equal(resumed.requests.length, 2);
@@ -185,8 +185,8 @@ test("an image-only request is accepted without invented user text", async t =>
185
185
  const f = await fixture(t);
186
186
  await f.conversation.send({ ...input, text: "" });
187
187
  const state = await f.conversation.wait();
188
- assert.equal(f.requests[0].messages.at(-1).content.length, 1);
189
- assert.equal(f.requests[0].messages.at(-1).content[0].type, "image_url");
188
+ assert.equal(f.requests[0].messages.at(-2).content.length, 1);
189
+ assert.equal(f.requests[0].messages.at(-2).content[0].type, "image_url");
190
190
  assert.equal(state.conversationLog[0].user.text, "");
191
191
  });
192
192
 
@@ -60,7 +60,7 @@ test("renewal preserves logical history and receipts while subsequent inference
60
60
  assert.equal(f.requests.length, 1, "Replacement itself does not perform inference");
61
61
  await f.say("Continue", "second");
62
62
  const sent = f.requests[1].messages;
63
- assert.equal(sent.at(-1).content, "Continue");
63
+ assert.equal(sent.at(-2).content, "Continue", "The request comes last, before the engine's state note");
64
64
  assert.match(sent[1].content, /Retain the agreed colour: blue/);
65
65
  assert.match(sent[1].content, /Keep my context/);
66
66
  assert.match(sent[1].content, /Reply 1/);
@@ -82,9 +82,9 @@ test("saved history and current instructions use the existing authorized model c
82
82
  await f.conversation.configure({ systemPrompt: "Use the new instructions." });
83
83
  await f.conversation.send({ messageId: "two", text: "Continue" });
84
84
  await f.conversation.wait();
85
- assert.match(f.requests[1].messages[0].content, /^Use the new instructions\.\n\nCurrent state, from the system\./u,
86
- "The current instructions lead the system prompt, before the engine's state note");
87
- assert.deepEqual(f.requests[1].messages.slice(1).map(message => message.content), ["Hello", "Hello there.", "Continue"]);
85
+ assert.equal(f.requests[1].messages[0].content, "Use the new instructions.", "The current instructions are the system prompt");
86
+ assert.deepEqual(f.requests[1].messages.slice(1, -1).map(message => message.content), ["Hello", "Hello there.", "Continue"]);
87
+ assert.match(f.requests[1].messages.at(-1).content, /^Current state, from the system\./u, "The engine's state note follows the conversation");
88
88
  f.state.modelAllowed = false;
89
89
  await assert.rejects(f.conversation.send({ messageId: "three", text: "No longer authorized" }));
90
90
  assert.equal((await f.conversation.read()).conversationLog.length, 2);
@@ -114,7 +114,7 @@ test("opening an existing application transcript retains its context before and
114
114
  const f = await fixture(t, { storage });
115
115
  await f.conversation.send(input);
116
116
  const first = await f.conversation.wait();
117
- assert.deepEqual(f.requests[0].messages.slice(1).map(message => message.content), [
117
+ assert.deepEqual(f.requests[0].messages.slice(1, -1).map(message => message.content), [
118
118
  "Our chosen colour is blue.", "I will remember blue.", "Hello"
119
119
  ]);
120
120
  assert.equal((await f.conversation.send({ messageId: "earlier", text: "Our chosen colour is blue." })).duplicate, true);
@@ -125,7 +125,7 @@ test("opening an existing application transcript retains its context before and
125
125
  assert.match(messages[1].content, /Our chosen colour is blue/);
126
126
  assert.match(messages[1].content, /I will remember blue/);
127
127
  assert.equal(messages.filter(message => message.content.includes("Our chosen colour is blue")).length, 1);
128
- assert.equal(messages.at(-1).content, "Continue");
128
+ assert.equal(messages.at(-2).content, "Continue", "The request comes last, before the engine's state note");
129
129
  assert.equal((await transcript.readConversationLog("one"))[0].metadata?.runtime, undefined);
130
130
  });
131
131
 
@@ -143,7 +143,7 @@ test("application wakes retain their origin, reuse receipts and remain quoted da
143
143
  assert.equal(snapshot.conversationLog[0].metadata.runtime.origin, "application");
144
144
  assert.ok(operations.includes("wake"));
145
145
  assert.ok(events.filter(event => event.type === "message").every(event => event.origin === "application"));
146
- assert.match(f.requests[0].messages.at(-1).content, /\[Application event\]/);
146
+ assert.match(f.requests[0].messages.at(-2).content, /\[Application event\]/);
147
147
  assert.equal((await f.conversation.wake(message)).duplicate, true);
148
148
  await assert.rejects(f.conversation.send(message), { code: "conversation_message_id_content_conflict" });
149
149
  await f.conversation.send(input);
@@ -170,7 +170,7 @@ test("per-request application data stays separate from visible text and particip
170
170
  const first = await f.conversation.wait();
171
171
  assert.equal(first.conversationLog[0].user.text, "Hello");
172
172
  assert.deepEqual(first.conversationLog[0].user.data, request.data);
173
- const native = f.requests[0].messages.at(-1).content;
173
+ const native = f.requests[0].messages.at(-2).content;
174
174
  assert.match(native, /\[Application data\]/);
175
175
  assert.match(native, /not additional instructions or authorization/);
176
176
  assert.ok(native.endsWith("Hello"));
@@ -180,7 +180,7 @@ test("per-request application data stays separate from visible text and particip
180
180
  const next = await f.conversation.wait();
181
181
  assert.equal(next.conversationLog[1].system.text, "A watched session changed.");
182
182
  assert.equal(f.requests[1].messages[1].content, native);
183
- assert.match(f.requests[1].messages.at(-1).content, /\[Application event\]/);
183
+ assert.match(f.requests[1].messages.at(-2).content, /\[Application event\]/);
184
184
  assert.deepEqual(next.configuration, configuration, "Application data never replaces the stable system prompt");
185
185
  });
186
186
 
@@ -219,7 +219,7 @@ test("API compaction stores its summary with the last turn it covers, and later
219
219
  const second = await f.conversation.wait();
220
220
  const last = f.requests.at(-1);
221
221
  assert.match(last.messages[0].content, /Summary of the earlier part of this conversation[^]*Six long questions so far\./u);
222
- assert.deepEqual(last.messages.filter(message => message.role === "user").map(message => message.content),
222
+ assert.deepEqual(last.messages.slice(0, -1).filter(message => message.role === "user").map(message => message.content),
223
223
  [...second.conversationLog.filter(turn => after.includes(turn.turnId)).map(turn => turn.user.text), "Continue"]);
224
224
  assert.deepEqual(await storage.read("one", transaction => transaction.readSummary()), summary, "A short remainder is not compacted again");
225
225
  const later = (await storage.read("one", transaction => transaction.readMetadata())).runtime.seen;
@@ -265,7 +265,8 @@ test("cancellation aborts inference and leaves another conversation usable", asy
265
265
  const started = Promise.withResolvers();
266
266
  let aborted = false;
267
267
  const f = await fixture(t, { fetch: (_url, init) => {
268
- if (JSON.parse(init.body).messages.at(-1).content === "Other") return response();
268
+ // The request comes last, before the engine's state note.
269
+ if (JSON.parse(init.body).messages.at(-2).content === "Other") return response();
269
270
  started.resolve();
270
271
  return new Promise((_resolve, reject) => init.signal.addEventListener("abort", () => {
271
272
  aborted = true; reject(init.signal.reason);
@@ -87,7 +87,7 @@ test("split internal tags and tool arguments never become common answer deltas",
87
87
  await f.conversation.send(message);
88
88
  const result = await f.conversation.wait();
89
89
  assert.deepEqual(f.observed.executions, [{ left: 2, right: 3 }]);
90
- assert.equal(f.observed.requests[1].messages.at(-1).content, checkTexts("the app").markup);
90
+ assert.equal(f.observed.requests[1].messages.at(-2).content, checkTexts("the app").markup, "The check comes last, before the engine's state note");
91
91
  assert.deepEqual(f.observed.events.filter(event => event.type === "message" && event.status !== "complete")
92
92
  .map(event => event.text), ["The search is complete."]);
93
93
  assert.equal(result.conversationLog[0].assistant.text, "The search is complete.");
@@ -101,7 +101,7 @@ test("text-encoded calls never run or reach the person, even when written again
101
101
  const result = await f.conversation.wait();
102
102
  assert.deepEqual(f.observed.executions, []);
103
103
  assert.equal(f.observed.requests.length, 2, "The check sends the reply back once");
104
- assert.equal(f.observed.requests[1].messages.at(-1).content, checkTexts("the app").markup);
104
+ assert.equal(f.observed.requests[1].messages.at(-2).content, checkTexts("the app").markup, "The check comes last, before the engine's state note");
105
105
  assert.deepEqual(result.conversationLog[0].metadata.applicationTools, []);
106
106
  assert.equal(f.observed.events.some(event => event.type === "message" && /invoke|function_calls/u.test(event.text)), false);
107
107
  });