@jskit-ai/assistant-runtime 0.2.9 → 0.2.11

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.
Files changed (66) hide show
  1. package/README.md +1 -1
  2. package/fixtures/canonical-conversation/main.js +2 -1
  3. package/fixtures/conversation/main.js +2 -1
  4. package/fixtures/conversation/vite.config.mjs +4 -3
  5. package/migrations/assistant_config_initial.cjs +1 -5
  6. package/migrations/assistant_transcripts_initial.cjs +3 -9
  7. package/migrations/assistant_transcripts_summary.cjs +28 -0
  8. package/migrations/assistant_turn_requests.cjs +2 -1
  9. package/migrations/constraints/assistant_users_foreign_keys.cjs +22 -0
  10. package/migrations/constraints/assistant_workspaces_foreign_keys.cjs +26 -0
  11. package/package.json +17 -10
  12. package/patterns/assistant-surface/example/src/ApplicationAiFeature.js +4 -2
  13. package/src/client/components/AssistantConversationClientElement.vue +10 -4
  14. package/src/client/components/AssistantSurfaceClientElement.vue +8 -4
  15. package/src/client/composables/conversationGoal.js +266 -0
  16. package/src/client/composables/conversationHistory.js +215 -0
  17. package/src/client/composables/conversationRuntime.js +624 -0
  18. package/src/client/composables/useAssistantConversation.js +208 -884
  19. package/src/client/composables/useAssistantRuntime.js +60 -45
  20. package/src/client/providers/AssistantClientProvider.js +1 -0
  21. package/src/client/support/assistantRuntimeState.js +6 -34
  22. package/src/client/support/conversationFailures.js +30 -0
  23. package/src/client/support/conversationRestoreSupport.js +13 -5
  24. package/src/client/support/subscribeAssistantConversation.js +32 -9
  25. package/src/client/support/workspaceScopeSupport.js +13 -22
  26. package/src/client/types.js +46 -0
  27. package/src/server/AssistantProvider.js +45 -17
  28. package/src/server/actions.js +162 -201
  29. package/src/server/conversationOperations.js +141 -0
  30. package/src/server/createAssistantRuntime.js +83 -44
  31. package/src/server/createAssistantSettingsActions.js +69 -47
  32. package/src/server/hostedAssistant.js +21 -0
  33. package/src/server/index.js +1 -0
  34. package/src/server/registerConversationSubscriptions.js +34 -11
  35. package/src/server/registerFastifyConversations.js +18 -6
  36. package/src/server/registerRoutes.js +5 -2
  37. package/src/server/registerSettingsRoutes.js +1 -1
  38. package/src/server/repositories/assistantConfigRepository.js +10 -20
  39. package/src/server/repositories/conversationsRepository.js +35 -10
  40. package/src/server/repositories/messagesRepository.js +14 -10
  41. package/src/server/repositories/repositoryPersistenceUtils.js +1 -6
  42. package/src/server/repositories/turnRequestsRepository.js +6 -1
  43. package/src/server/services/assistantConfigService.js +25 -23
  44. package/src/server/services/chatService.js +57 -61
  45. package/src/server/services/transcriptService.js +15 -7
  46. package/src/server/sqlConversationStorage.js +90 -18
  47. package/src/server/support/assistantServerConfig.js +1 -1
  48. package/src/server/support/assistantStreamWriter.js +7 -1
  49. package/src/server/support/createSurfaceAwareToolCatalog.js +35 -17
  50. package/src/server/types.js +38 -0
  51. package/src/shared/assistantSurfaces.js +2 -2
  52. package/test/assistantConversationOperations.test.js +5 -5
  53. package/test/assistantRuntimeState.test.js +1 -16
  54. package/test/chatServiceLifecycle.test.js +62 -154
  55. package/test/createSurfaceAwareToolCatalog.test.js +5 -0
  56. package/test/featureRuntime.test.js +12 -6
  57. package/test/lazyAppConfig.test.js +3 -5
  58. package/test/packageMetadata.test.js +7 -1
  59. package/test/sqlConversationStorage.mysql.test.js +89 -0
  60. package/test/sqlConversationStorage.test.js +161 -16
  61. package/test/support/memoryAssistantRows.js +6 -1
  62. package/test/support/scriptedModel.js +32 -0
  63. package/test/turnRequests.test.js +4 -2
  64. package/test/usersForeignKeysMigration.test.js +46 -0
  65. package/test/workspacesForeignKeysMigration.test.js +66 -0
  66. package/src/client/support/accessDenied.js +0 -6
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { isDeepStrictEqual } from "node:util";
2
3
  import { setTimeout as delay } from "node:timers/promises";
3
4
  import { AppError } from "@jskit-ai/kernel/server/runtime";
@@ -87,34 +88,85 @@ function requireUnambiguous(projection) {
87
88
  });
88
89
  }
89
90
 
90
- /** Existing SQL rows remain authoritative. Core supplies the transaction methods;
91
+ // Rows kept in memory across every cached conversation. A conversation with more rows
92
+ // than this is read from the database on each access.
93
+ const CACHED_ROWS = 50_000;
94
+
95
+ /** The SQL rows are authoritative. Core supplies the transaction methods;
91
96
  * the database owns locking, sequence allocation and commit/rollback together.
97
+ * Each conversation's rows stay cached while its transcript revision and last message
98
+ * sequence are unchanged. Every save that changes rows or the summary writes a new
99
+ * random revision under the conversation lock, so a save by another server makes the
100
+ * next access read the rows again, as does a row inserted without this storage; the
101
+ * cache takes a save only after its transaction commits.
102
+ * @param {object} dependencies
103
+ * @param {import("./types.js").ConversationsRepository} dependencies.conversationsRepository
104
+ * @param {import("./types.js").MessagesRepository} dependencies.messagesRepository
92
105
  */
93
106
  function createSqlConversationStorage({ conversationsRepository, messagesRepository }) {
94
- async function load(id, trx, write = false) {
95
- const conversation = await conversationsRepository.findById(id, { trx, forUpdate: write });
96
- if (!conversation) throw new AppError(404, "Conversation not found.", { code: "assistant.runtime.conversation.notFound" });
107
+ const cache = new Map();
108
+ let cachedRows = 0;
109
+
110
+ function forget(id) {
111
+ const entry = cache.get(id);
112
+ if (!entry) return;
113
+ cache.delete(id);
114
+ cachedRows -= entry.rows.length;
115
+ }
116
+
117
+ function remember(id, entry) {
118
+ forget(id);
119
+ if (!entry.revision || entry.rows.length > CACHED_ROWS) return;
120
+ cache.set(id, entry);
121
+ cachedRows += entry.rows.length;
122
+ for (const [oldest, value] of cache) {
123
+ if (cachedRows <= CACHED_ROWS) break;
124
+ cache.delete(oldest);
125
+ cachedRows -= value.rows.length;
126
+ }
127
+ }
128
+
129
+ async function readRows(id, conversation, trx) {
97
130
  const rows = [];
98
131
  for (let page = 1; ; page += 1) {
99
132
  const batch = await messagesRepository.listByConversationScope(id, { workspaceId: conversation.workspaceId }, { page, pageSize: 500 }, { trx });
100
133
  rows.push(...batch);
101
134
  if (batch.length < 500) break;
102
135
  }
103
- return { conversation, rows, projection: inspectSqlConversationRows(conversation, rows) };
136
+ return rows;
137
+ }
138
+
139
+ async function load(id, trx, write = false) {
140
+ const conversation = await conversationsRepository.findById(id, { trx, forUpdate: write });
141
+ if (!conversation) throw new AppError(404, "Conversation not found.", { code: "assistant.runtime.conversation.notFound" });
142
+ const { revision, summary } = await conversationsRepository.readTranscriptState(id, { trx });
143
+ const lastSeq = await messagesRepository.lastSequence(id, { trx });
144
+ let saved = cache.get(id);
145
+ if (saved && revision && saved.revision === revision && saved.lastSeq === lastSeq) remember(id, saved);
146
+ else {
147
+ const rows = await readRows(id, conversation, trx);
148
+ saved = { revision, lastSeq, rows, projection: inspectSqlConversationRows(conversation, rows) };
149
+ remember(id, saved);
150
+ }
151
+ const { projection } = saved;
152
+ const record = { ...projection.record, metadata: structuredClone(conversation.metadata || {}), ...(summary ? { summary } : {}) };
153
+ return { conversation, summary, lastSeq, rows: saved.rows, projection: { ...projection, record }, next: null };
104
154
  }
105
155
 
106
156
  async function save(id, loaded, record, trx) {
107
157
  const { conversation, projection } = loaded;
108
158
  let added = 0;
109
159
  let title = conversation.title;
160
+ const updated = new Map();
161
+ const inserted = [];
110
162
  const persist = async (previous, fields) => {
111
163
  if (previous) {
112
164
  if (previous.contentText !== fields.contentText || !isDeepStrictEqual(previous.metadata, fields.metadata)) {
113
- await messagesRepository.updateById(previous.id, { contentText: fields.contentText, metadata: fields.metadata }, { trx });
165
+ updated.set(previous.id, await messagesRepository.updateById(previous.id, { contentText: fields.contentText, metadata: fields.metadata }, { trx }));
114
166
  }
115
167
  } else {
116
- await messagesRepository.create({ conversationId: id, workspaceId: conversation.workspaceId,
117
- actorUserId: conversation.createdByUserId, ...fields }, { trx });
168
+ inserted.push(await messagesRepository.create({ conversationId: id, workspaceId: conversation.workspaceId,
169
+ actorUserId: conversation.createdByUserId, ...fields }, { trx }));
118
170
  added += 1;
119
171
  }
120
172
  };
@@ -152,14 +204,22 @@ function createSqlConversationStorage({ conversationsRepository, messagesReposit
152
204
  ...(title !== conversation.title ? { title } : {}),
153
205
  ...(!isDeepStrictEqual(record.metadata, conversation.metadata) ? { metadata: record.metadata } : {}) };
154
206
  if (Object.keys(patch).length) await conversationsRepository.updateById(id, patch, { trx });
207
+ const summary = record.summary ? { text: record.summary.text, throughTurnId: record.summary.throughTurnId } : null;
208
+ if (!updated.size && !inserted.length && isDeepStrictEqual(summary, loaded.summary)) return;
209
+ const revision = randomUUID();
210
+ await conversationsRepository.writeTranscriptState(id, { revision, summary }, { trx });
211
+ const rows = loaded.rows.map(row => updated.get(row.id) || row).concat(inserted);
212
+ loaded.next = { revision, lastSeq: Math.max(loaded.lastSeq, ...inserted.map(row => row.seq)), rows,
213
+ projection: inspectSqlConversationRows(conversation, rows) };
155
214
  }
156
215
 
157
216
  async function access(id, operation, write) {
158
217
  for (let attempt = 0; ; attempt += 1) {
159
218
  let entered = false;
219
+ let loaded = null;
160
220
  try {
161
- return await conversationsRepository.transaction(async trx => {
162
- const loaded = await load(id, trx, write);
221
+ const result = await conversationsRepository.transaction(async trx => {
222
+ loaded = await load(id, trx, write);
163
223
  requireUnambiguous(loaded.projection);
164
224
  const storage = createConversationStorage({
165
225
  readRecord: () => loaded.projection.record,
@@ -168,6 +228,8 @@ function createSqlConversationStorage({ conversationsRepository, messagesReposit
168
228
  entered = true;
169
229
  return storage[write ? "write" : "read"](id, operation);
170
230
  }, { retries: 0 });
231
+ if (loaded.next) remember(id, loaded.next);
232
+ return result;
171
233
  } catch (error) {
172
234
  // SQLite cannot wait asynchronously for another connection's writer.
173
235
  // Retry acquiring the snapshot/lock only; never replay a caller or save,
@@ -178,15 +240,25 @@ function createSqlConversationStorage({ conversationsRepository, messagesReposit
178
240
  }
179
241
  }
180
242
 
243
+ function read(id, operation) {
244
+ return access(id, operation, false);
245
+ }
246
+
247
+ function write(id, operation) {
248
+ return access(id, operation, true);
249
+ }
250
+
251
+ function preflight(id) {
252
+ return conversationsRepository.transaction(async trx => {
253
+ const { rows, projection } = await load(id, trx);
254
+ return { conversationId: id, rowCount: rows.length, issues: projection.issues };
255
+ });
256
+ }
257
+
181
258
  return Object.freeze({
182
- read: (id, operation) => access(id, operation, false),
183
- write: (id, operation) => access(id, operation, true),
184
- preflight(id) {
185
- return conversationsRepository.transaction(async trx => {
186
- const { rows, projection } = await load(id, trx);
187
- return { conversationId: id, rowCount: rows.length, issues: projection.issues };
188
- });
189
- }
259
+ read,
260
+ write,
261
+ preflight
190
262
  });
191
263
  }
192
264
 
@@ -6,7 +6,7 @@ import {
6
6
  normalizeTimeoutMs
7
7
  } from "@jskit-ai/assistant-core/server";
8
8
 
9
- function resolveAssistantServerConfigMap(appConfig = {}) {
9
+ function resolveAssistantServerConfigMap(appConfig) {
10
10
  const source = normalizeObject(appConfig?.assistantServer);
11
11
  const resolved = {};
12
12
 
@@ -1,4 +1,5 @@
1
- // The stream writer contract shared by the chat route and the chat service.
1
+ // The stream writer contract: the chat route builds one writer with these methods, and the chat service sends each event
2
+ // through it.
2
3
  const ASSISTANT_STREAM_WRITER_METHODS = Object.freeze([
3
4
  "sendMeta",
4
5
  "sendAssistantDelta",
@@ -9,4 +10,9 @@ const ASSISTANT_STREAM_WRITER_METHODS = Object.freeze([
9
10
  "sendDone"
10
11
  ]);
11
12
 
13
+ /**
14
+ * The writer a chat stream sends its events through, one method per event kind.
15
+ * @typedef {Record<(typeof ASSISTANT_STREAM_WRITER_METHODS)[number], (event: object) => void>} AssistantStreamWriter
16
+ */
17
+
12
18
  export { ASSISTANT_STREAM_WRITER_METHODS };
@@ -12,7 +12,7 @@ function buildCatalogOptions(appConfig = {}, surfaceId = "") {
12
12
  });
13
13
  }
14
14
 
15
- function requireContextSurfaceId(context = {}) {
15
+ function requireContextSurfaceId(context) {
16
16
  const surfaceId = normalizeSurfaceId(context?.surface);
17
17
  if (surfaceId) {
18
18
  return surfaceId;
@@ -21,7 +21,19 @@ function requireContextSurfaceId(context = {}) {
21
21
  throw new Error("assistant surface-aware tool catalog requires context.surface.");
22
22
  }
23
23
 
24
+ /**
25
+ * The assistant's tool catalogue over an application's actions, resolved per surface. Each surface
26
+ * offers the actions its `assistantServer` configuration leaves in (its barred action ids and
27
+ * skipped prefixes taken out), and every call names its surface in `context.surface`. The assistant
28
+ * runtime builds its catalogue this way; an application test builds one the same way to see the
29
+ * tools a surface offers and to call them.
30
+ * @param {import("@jskit-ai/kernel/server/types").ActionCatalogue} actions The application's actions (`runtime.actions`).
31
+ * @param {object} [options]
32
+ * @param {Record<string, any>} [options.appConfig] The server configuration that holds `assistantServer`.
33
+ * @param {typeof createServiceToolCatalog} [options.createCatalog]
34
+ */
24
35
  function createSurfaceAwareToolCatalog(actions, { appConfig = {}, createCatalog = createServiceToolCatalog } = {}) {
36
+ // readable-check: allow method-probe - input validation: an application hands over its action catalogue
25
37
  if (!actions || typeof actions.listDefinitions !== "function") {
26
38
  throw new Error("createSurfaceAwareToolCatalog requires runtime.actions.");
27
39
  }
@@ -40,27 +52,33 @@ function createSurfaceAwareToolCatalog(actions, { appConfig = {}, createCatalog
40
52
  return nextCatalog;
41
53
  }
42
54
 
55
+ function resolveToolSet(context = {}, options) {
56
+ const surfaceId = requireContextSurfaceId(context);
57
+ return resolveCatalog(surfaceId).resolveToolSet(context, options);
58
+ }
59
+
60
+ function toOpenAiToolSchema(tool) {
61
+ if (!schemaCatalog) {
62
+ schemaCatalog = createCatalog(actions, buildCatalogOptions(normalizedAppConfig, ""));
63
+ }
64
+
65
+ return schemaCatalog.toOpenAiToolSchema(tool);
66
+ }
67
+
68
+ function executeToolCall(payload = {}) {
69
+ const context = payload.context && typeof payload.context === "object" ? payload.context : {};
70
+ const surfaceId = requireContextSurfaceId(context);
71
+ return resolveCatalog(surfaceId).executeToolCall(payload);
72
+ }
73
+
43
74
  return Object.freeze({
44
75
  get limits() {
45
76
  if (!schemaCatalog) schemaCatalog = createCatalog(actions, buildCatalogOptions(normalizedAppConfig, ""));
46
77
  return schemaCatalog.limits;
47
78
  },
48
- resolveToolSet(context = {}, options) {
49
- const surfaceId = requireContextSurfaceId(context);
50
- return resolveCatalog(surfaceId).resolveToolSet(context, options);
51
- },
52
- toOpenAiToolSchema(tool) {
53
- if (!schemaCatalog) {
54
- schemaCatalog = createCatalog(actions, buildCatalogOptions(normalizedAppConfig, ""));
55
- }
56
-
57
- return schemaCatalog.toOpenAiToolSchema(tool);
58
- },
59
- executeToolCall(payload = {}) {
60
- const context = payload?.context && typeof payload.context === "object" ? payload.context : {};
61
- const surfaceId = requireContextSurfaceId(context);
62
- return resolveCatalog(surfaceId).executeToolCall(payload);
63
- }
79
+ resolveToolSet,
80
+ toOpenAiToolSchema,
81
+ executeToolCall
64
82
  });
65
83
  }
66
84
 
@@ -0,0 +1,38 @@
1
+ // The type of the `assistant.runtime` capability, the contracts of the optional
2
+ // capabilities the assistant runtime reads, and the types its modules share.
3
+
4
+ /** @typedef {ReturnType<typeof import("./hostedAssistant.js").createHostedAssistant>} HostedAssistant The assistant over a conversation runtime that the application supplies as `assistant.conversations`. */
5
+ /** @typedef {ReturnType<typeof import("./createAssistantRuntime.js").createAssistantRuntime>} DatabaseAssistant The assistant over the package's own SQL tables. */
6
+ /** @typedef {HostedAssistant | DatabaseAssistant} AssistantRuntime The `assistant.runtime` capability. */
7
+
8
+ /** @typedef {import("@jskit-ai/assistant-core/server/types").ConversationRuntime} ConversationRuntime The `assistant.conversations` capability: the conversation runtime an application supplies. */
9
+ /** @typedef {ReturnType<typeof import("./services/assistantConfigService.js").createService>} AssistantConfigService The `assistant.settings` capability: an assistant surface's saved settings. */
10
+ /** @typedef {ReturnType<typeof import("./services/chatService.js").createChatService>} ChatService */
11
+ /** @typedef {ReturnType<typeof import("./services/transcriptService.js").createTranscriptService>} TranscriptService */
12
+ /** @typedef {ReturnType<typeof import("./createAssistantRuntime.js").createAssistantAiClientFactory>} AssistantAiClientFactory */
13
+ /** @typedef {ReturnType<typeof import("./support/createSurfaceAwareToolCatalog.js").createSurfaceAwareToolCatalog>} SurfaceAwareToolCatalog */
14
+ /** @typedef {ReturnType<typeof import("./repositories/assistantConfigRepository.js").createRepository>} AssistantConfigRepository */
15
+ /** @typedef {ReturnType<typeof import("./repositories/conversationsRepository.js").createRepository>} ConversationsRepository */
16
+ /** @typedef {ReturnType<typeof import("./repositories/messagesRepository.js").createRepository>} MessagesRepository */
17
+ /** @typedef {ReturnType<typeof import("./repositories/turnRequestsRepository.js").createRepository>} TurnRequestsRepository */
18
+
19
+ /** @typedef {import("@jskit-ai/workspaces-core/server/types").WorkspacesCore["scope"]} WorkspaceScopeSupport The workspace route support of `workspaces.core`: its route params, the action input they build and the workspace an action runs in. */
20
+
21
+ /**
22
+ * The `assistant.conversation.access` capability: a host's access owner for the
23
+ * conversation routes and actions.
24
+ * @typedef {object} ConversationAccess
25
+ * @property {{ register: import("@jskit-ai/kernel/server/types").RuntimeRouter["register"] }} router Registers a conversation route behind the host's access check.
26
+ * @property {(definition: object) => object} wrapAction Wraps a conversation action definition with the host's access policy.
27
+ * @property {string} subscribeActionId The fixed id of the host's conversation subscription action.
28
+ * @property {"authenticated" | "host"} requestPolicy How a realtime subscription reads its request.
29
+ */
30
+
31
+ /**
32
+ * The `assistant.attachments` capability: the application's authorized files for
33
+ * a message.
34
+ * @typedef {object} AssistantAttachments
35
+ * @property {(request: { attachmentIds: string[], context: object, workspace: object | null, conversation: object }) => Promise<unknown>} resolve
36
+ */
37
+
38
+ export {};
@@ -17,7 +17,7 @@ function defineAssistantSurface(surfaceId, { pagesRoot = "", requiresWorkspace =
17
17
  });
18
18
  }
19
19
 
20
- function resolveSurfaceDefinitions(appConfig = {}) {
20
+ function resolveSurfaceDefinitions(appConfig) {
21
21
  const source = normalizeObject(appConfig?.surfaceDefinitions);
22
22
  const resolved = {};
23
23
 
@@ -38,7 +38,7 @@ function resolveSurfaceDefinitions(appConfig = {}) {
38
38
  return Object.freeze(resolved);
39
39
  }
40
40
 
41
- function resolveAssistantSurfacesConfig(appConfig = {}) {
41
+ function resolveAssistantSurfacesConfig(appConfig) {
42
42
  const source = normalizeObject(appConfig?.assistantSurfaces);
43
43
  const resolved = {};
44
44
 
@@ -336,7 +336,7 @@ test("conversation routes keep authenticated ownership and reject unknown identi
336
336
  assert.equal(accepted.payload.status, "accepted");
337
337
  await f.dispatched[0].promise;
338
338
  const body = JSON.parse(f.requests[0].body);
339
- assert.equal(body.messages[0].content, "Answer briefly.");
339
+ assert.match(body.messages[0].content, /^Answer briefly\.\n\nCurrent state, from the system\. /u, "The system prompt, then the engine's state note");
340
340
  assert.equal(body.messages.at(-1).content, "Use the host configuration.");
341
341
  assert.equal((await f.invoke("GET")).payload.engine, "api");
342
342
  });
@@ -390,13 +390,13 @@ test("declared conversation data keeps the authored target without becoming auth
390
390
  const state = (await f.invoke("GET")).payload;
391
391
  assert.deepEqual(state.conversationLog[0].user.data, data);
392
392
  assert.equal(state.engine, "api");
393
- assert.equal(JSON.parse(f.requests[0].body).messages[0].content, "Answer briefly.");
393
+ assert.match(JSON.parse(f.requests[0].body).messages[0].content, /^Answer briefly\.\n\nCurrent state, from the system\. /u);
394
394
  const repeated = await f.invoke("POST", "/messages", { body: input });
395
395
  assert.equal(repeated.payload.duplicate, true);
396
396
  assert.equal(repeated.payload.turnId, accepted.payload.turnId);
397
397
  await assert.rejects(f.invoke("POST", "/messages", { body: { ...input,
398
398
  data: { ...data, focus: { projectSlug: "new-project" } }
399
- } }), { code: "conversation_message_conflict" });
399
+ } }), { code: "conversation_message_id_content_conflict" });
400
400
  assert.equal(f.requests.length, 1, "Retargeting a captured message cannot dispatch it again");
401
401
  });
402
402
 
@@ -407,7 +407,7 @@ test("goal operations retain capabilities and cannot forward native host or atta
407
407
  assert.equal(absentGoal.status, 204, "An absent native goal uses the client's existing no-content response contract");
408
408
  await assert.rejects(f.invoke("POST", "/goal", { body: {
409
409
  action: "set", messageId: "goal-one", expectedSegmentId: "current", objective: "Do this"
410
- } }), { code: "conversation_unsupported" });
410
+ } }), { code: "conversation_goals_unsupported" });
411
411
  assert.equal(f.requests.length, 0, "An unsupported goal never starts inference");
412
412
 
413
413
  const calls = [];
@@ -497,7 +497,7 @@ test("declared replacement keeps application policy and exact predecessor author
497
497
  await assert.rejects(call({ replacement: { ...replacement, ...extra } }), { code: "ACTION_VALIDATION_FAILED" });
498
498
  }
499
499
  await assert.rejects(call({ replacement: { ...replacement, expectedSegmentId: "stale-predecessor" } }),
500
- { code: "conversation_replacement_conflict" });
500
+ { code: "conversation_predecessor_changed" });
501
501
  const replaced = await call({ replacement, engine: "codex", host: { nativeTools: true }, context: { forged: true } });
502
502
  assert.equal(replaced.status, 200);
503
503
  assert.equal(replaced.payload.operationId, replacement.operationId);
@@ -1,12 +1,11 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
  import {
4
- buildHistory,
5
4
  interruptPendingToolEvents,
6
5
  mapTranscriptEntriesToAssistantState
7
6
  } from "../src/client/support/assistantRuntimeState.js";
8
7
 
9
- test("restored progress narration is neither rendered nor replayed", () => {
8
+ test("restored progress narration is not rendered", () => {
10
9
  const restored = mapTranscriptEntriesToAssistantState([
11
10
  {
12
11
  id: "1",
@@ -42,20 +41,6 @@ test("restored progress narration is neither rendered nor replayed", () => {
42
41
  "Let me check. The second booking is also confirmed."
43
42
  ]
44
43
  );
45
- assert.deepEqual(buildHistory(restored.messages), [
46
- {
47
- role: "user",
48
- content: "Find the booking."
49
- },
50
- {
51
- role: "assistant",
52
- content: "The booking is confirmed."
53
- },
54
- {
55
- role: "assistant",
56
- content: "Let me check. The second booking is also confirmed."
57
- }
58
- ]);
59
44
  });
60
45
 
61
46
  test("restored orphaned tool calls are marked interrupted", () => {