@jini-ai/chat 0.6.17 → 0.6.21

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 (75) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/core/accepted-input.d.ts +10 -2
  3. package/dist/core/accepted-input.js +87 -33
  4. package/dist/core/assistant-client/durable-run-subscription.js +6 -0
  5. package/dist/core/assistant-client/entry.d.ts +1 -1
  6. package/dist/core/assistant-client/entry.js +1 -1
  7. package/dist/core/assistant-client/messages.d.ts +21 -0
  8. package/dist/core/assistant-client/messages.js +31 -0
  9. package/dist/core/checkpoint-interval.d.ts +25 -0
  10. package/dist/core/checkpoint-interval.js +38 -0
  11. package/dist/core/durable-runs/ledger-contracts.d.ts +27 -1
  12. package/dist/core/durable-runs/ports.d.ts +25 -1
  13. package/dist/core/events.d.ts +6 -0
  14. package/dist/core/index.d.ts +1 -0
  15. package/dist/core/index.js +1 -0
  16. package/dist/core/run-events/entry.js +15 -2
  17. package/dist/core/surface-recovery.d.ts +19 -0
  18. package/dist/core/surface-recovery.js +26 -0
  19. package/dist/core/tool-events.d.ts +11 -3
  20. package/dist/core/tool-events.js +75 -43
  21. package/dist/core/turn-usage.d.ts +1 -1
  22. package/dist/core/turn-usage.js +5 -3
  23. package/dist/embed-widget/browser/frontend-session-bridge.d.ts +14 -1
  24. package/dist/embed-widget/browser/frontend-session-bridge.js +120 -47
  25. package/dist/react/components/Composer.d.ts +3 -3
  26. package/dist/react/components/Composer.js +5 -4
  27. package/dist/react/components/ConversationList.js +2 -1
  28. package/dist/react/components/ConversationTitleEditor.js +2 -1
  29. package/dist/react/components/ExtEventErrorBoundary.js +2 -1
  30. package/dist/react/components/McpUiSurfaceCard.js +12 -5
  31. package/dist/react/components/MessageList.js +2 -1
  32. package/dist/react/components/MidRunUserMessage.js +1 -1
  33. package/dist/react/components/ToolCard.js +3 -2
  34. package/dist/react/components/ToolCard.rules.js +2 -1
  35. package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +5 -3
  36. package/dist/react/features/chat-pane/components/ChatPane.js +1 -1
  37. package/dist/react/features/chat-pane/create-mcp-ui-tool-caller.d.ts +15 -2
  38. package/dist/react/features/chat-pane/create-mcp-ui-tool-caller.js +19 -3
  39. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +5 -4
  40. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +17 -9
  41. package/dist/react/features/chat-pane/hooks/useChatPaneFileDrop.hooks.js +3 -2
  42. package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.js +20 -3
  43. package/dist/react/features/chat-pane/model-options.d.ts +10 -0
  44. package/dist/react/features/chat-pane/model-options.js +22 -3
  45. package/dist/react/features/chat-pane/styles.d.ts +1 -1
  46. package/dist/react/features/chat-pane/styles.js +3 -1
  47. package/dist/react/features/chat-pane/surface-recovery-stream.d.ts +12 -0
  48. package/dist/react/features/chat-pane/surface-recovery-stream.js +47 -0
  49. package/dist/react/hooks/composer-draft-cache.d.ts +6 -6
  50. package/dist/react/hooks/composer-draft-cache.js +18 -17
  51. package/dist/react/hooks/useComposer.js +52 -4
  52. package/dist/react/hooks/useConversation.js +41 -22
  53. package/dist/react/hooks/useRunStream.js +2 -1
  54. package/dist/react/hooks/useSurfaceRecovery.d.ts +23 -0
  55. package/dist/react/hooks/useSurfaceRecovery.js +84 -0
  56. package/dist/react/styles/reference.css +4 -5
  57. package/dist/server/durable-runs/recover.js +3 -3
  58. package/dist/store/ports.d.ts +17 -3
  59. package/dist/store/runtime/chat-expiry-sweep.js +3 -1
  60. package/dist/store/runtime/chat-history-store.js +6 -0
  61. package/dist/store/runtime/checkpoint-delta.d.ts +35 -0
  62. package/dist/store/runtime/checkpoint-delta.js +75 -0
  63. package/dist/store/runtime/durable-run-store.js +92 -25
  64. package/dist/store/runtime/events-json.d.ts +17 -0
  65. package/dist/store/runtime/events-json.js +19 -0
  66. package/dist/store/runtime/run-guarded-chat-store.js +8 -3
  67. package/dist/store/runtime/run-ledger.d.ts +2 -2
  68. package/dist/store/runtime/run-ledger.js +120 -45
  69. package/dist/store/sql/read-version.d.ts +15 -0
  70. package/dist/store/sql/read-version.js +21 -0
  71. package/dist/store/sql/store.d.ts +9 -0
  72. package/dist/store/sql/store.js +89 -4
  73. package/dist/store/sqlite/schema.d.ts +2 -2
  74. package/dist/store/sqlite/schema.js +4 -0
  75. package/package.json +12 -12
@@ -20,6 +20,15 @@ interface MessageRow {
20
20
  }
21
21
  /** Project only existing ai_* fields: resumable and lastRunEventId belong to run state. */
22
22
  export declare function toMessage(row: MessageRow): ChatMessage;
23
+ type RawMessageRow = MessageRow & {
24
+ events_json_valid: number | null;
25
+ };
26
+ /** Keep malformed-column tolerance without reserializing event trees. SQLite validates in SQL;
27
+ * PostgreSQL 14 lacks a safe built-in JSON try-cast, so validate and discard one row's parsed value.
28
+ * @complexity O(row JSON size) time; PG14 temporarily allocates one event tree, SQLite none. */
29
+ export declare function validatedEventsJson({ row }: {
30
+ row: Pick<RawMessageRow, "events_json" | "events_json_valid">;
31
+ }, _optional?: {}): string | undefined;
23
32
  /**
24
33
  * Returns a {@link ChatHistoryStore} that can only ever see `scope`'s own conversations.
25
34
  *
@@ -1,3 +1,4 @@
1
+ import { sqliteReadVersion } from './read-version.js';
1
2
  import { withFolderReceipt, foldersFromReceipts } from '../../core/accepted-input.js';
2
3
  const MESSAGE_POSITION = 'm.position';
3
4
  const CONVERSATION_UPDATED_AT = 'c.updated_at';
@@ -92,6 +93,61 @@ function jsonFields(row) {
92
93
  ...(attachments === undefined ? {} : { attachments }),
93
94
  };
94
95
  }
96
+ /** Project metadata without retaining assistant event trees. User folder receipts still hydrate
97
+ * through the existing mapper; unusual assistant receipts with folders need the same projection. */
98
+ function rawMessageMetadata({ row }, _optional = {}) {
99
+ const hasFolderReceipt = row.events_json?.includes(`"name":"${MID_RUN_USER_MESSAGE_EVENT_NAME}"`) && row.events_json.includes('"folders"');
100
+ const needsReceipts = row.role === 'user' || hasFolderReceipt;
101
+ const message = toMessage({ ...row, events_json: needsReceipts ? row.events_json : null });
102
+ delete message.events;
103
+ return message;
104
+ }
105
+ /** Keep malformed-column tolerance without reserializing event trees. SQLite validates in SQL;
106
+ * PostgreSQL 14 lacks a safe built-in JSON try-cast, so validate and discard one row's parsed value.
107
+ * @complexity O(row JSON size) time; PG14 temporarily allocates one event tree, SQLite none. */
108
+ export function validatedEventsJson({ row }, _optional = {}) {
109
+ if (!row.events_json)
110
+ return undefined;
111
+ if (row.events_json_valid !== null)
112
+ return Number(row.events_json_valid) === 1 ? row.events_json : undefined;
113
+ try {
114
+ JSON.parse(row.events_json);
115
+ return row.events_json;
116
+ }
117
+ catch {
118
+ return undefined;
119
+ }
120
+ }
121
+ /** Emit metadata and opaque event JSON separately, so neither an aggregate response string nor
122
+ * all parsed assistant events coexist. The borrowed query still holds O(total stored JSON) strings.
123
+ * @complexity O(total JSON bytes) time/retained raw strings; PG validation peaks at one event tree. */
124
+ async function* messageJsonChunks({ rows }, { orderMessages = messages => messages } = {}) {
125
+ const byId = new Map(rows.map(row => [row.id, row]));
126
+ const messages = orderMessages(rows.map(row => rawMessageMetadata({ row })));
127
+ yield '{"messages":[';
128
+ for (let index = 0; index < messages.length; index++) {
129
+ const message = messages[index];
130
+ const eventsJson = validatedEventsJson({ row: byId.get(message.id) });
131
+ if (index > 0)
132
+ yield ',';
133
+ const metadataJson = JSON.stringify(message);
134
+ if (eventsJson === undefined) {
135
+ yield metadataJson;
136
+ continue;
137
+ }
138
+ yield metadataJson.slice(0, -1) + ',"events":';
139
+ yield eventsJson;
140
+ yield '}';
141
+ }
142
+ yield ']}';
143
+ }
144
+ /** Receipt-bearing user rows retain their public metadata while large assistant histories stay
145
+ * outside recovery/acceptance projections. Both dialects evaluate the same CASE expression. */
146
+ function messageEventsColumn({ includeEvents }, _options = {}) {
147
+ if (includeEvents === 'user')
148
+ return sql `case when m.role = 'user' then m.events_json else null end`;
149
+ return includeEvents ? sql `m.events_json` : sql `null`;
150
+ }
95
151
  /** Empty run columns are omitted. */
96
152
  function runFields(row) {
97
153
  return {
@@ -164,7 +220,7 @@ export function createSqlChatStore({ kernel, scope: inputScope }, { clock = crea
164
220
  return row ? toConversation(row) : null;
165
221
  }
166
222
  /** One scoped projection for all message reads, including the row returned by an upsert. */
167
- function ownedMessages(db, conversationId) {
223
+ function ownedMessages(db, conversationId, { includeEvents = true } = {}) {
168
224
  return db
169
225
  .selectFrom("ai_chat_messages as m")
170
226
  .innerJoin("ai_chats as c", "c.id", "m.conversation_id")
@@ -174,7 +230,6 @@ export function createSqlChatStore({ kernel, scope: inputScope }, { clock = crea
174
230
  "m.content",
175
231
  "m.agent_id",
176
232
  "m.agent_name",
177
- "m.events_json",
178
233
  "m.attachments_json",
179
234
  "m.run_id",
180
235
  "m.run_status",
@@ -182,14 +237,15 @@ export function createSqlChatStore({ kernel, scope: inputScope }, { clock = crea
182
237
  "m.started_at",
183
238
  "m.ended_at",
184
239
  ])
240
+ .select(messageEventsColumn({ includeEvents }).as('events_json'))
185
241
  .where("m.conversation_id", "=", conversationId)
186
242
  .where("c.scope_id", "=", scopeId)
187
243
  .where("c.owner_kind", "=", ownerKind)
188
244
  .where("c.owner_id", "=", ownerId);
189
245
  }
190
246
  /** Read owned messages in persisted position order; foreign IDs produce no rows. */
191
- async function messages({ conversationId }, _optional = {}) {
192
- const rows = await run(db => ownedMessages(db, conversationId).orderBy(MESSAGE_POSITION).execute());
247
+ async function messages({ conversationId }, { includeEvents = true } = {}) {
248
+ const rows = await run(db => ownedMessages(db, conversationId, { includeEvents }).orderBy(MESSAGE_POSITION).execute());
193
249
  return rows.map(toMessage);
194
250
  }
195
251
  /** Writes and projects one message inside the borrowed transaction; null for absent/foreign IDs. */
@@ -369,6 +425,32 @@ export function createSqlChatStore({ kernel, scope: inputScope }, { clock = crea
369
425
  .execute());
370
426
  },
371
427
  messages,
428
+ /** One indexed, owner-scoped row; browser save fences do not need the conversation's history. */
429
+ async message({ conversationId, messageId }, _optional = {}) {
430
+ const row = await run(db => ownedMessages(db, conversationId).where('m.id', '=', messageId).executeTakeFirst());
431
+ return row ? toMessage(row) : null;
432
+ },
433
+ /** SQLite's connection/external-commit counters avoid reading message rows at all. PG uses
434
+ * MVCC versions from only this conversation's indexed metadata, never large event columns.
435
+ * @complexity O(1) SQLite; O(n) PG message metadata for n messages; O(1) returned bytes.
436
+ */
437
+ async messagesVersion({ conversationId }, _optional = {}) {
438
+ const row = await run(db => db.selectFrom('ai_chats as c').select(eb => {
439
+ const version = kernel.dialect === 'sqlite' ? sqliteReadVersion({ kernel })
440
+ : sql `md5(coalesce(${eb.selectFrom('ai_chat_messages as m')
441
+ .select(sql `string_agg(m.id || ':' || cast(m.xmin as text), ',' order by m.position)`.as('versions'))
442
+ .whereRef('m.conversation_id', '=', 'c.id')}, ''))`;
443
+ return version.as('version');
444
+ })
445
+ .where('c.id', '=', conversationId).where('c.scope_id', '=', scopeId)
446
+ .where('c.owner_kind', '=', ownerKind).where('c.owner_id', '=', ownerId).executeTakeFirst());
447
+ return row?.version ?? null;
448
+ },
449
+ async messagesJson({ conversationId }, optional = {}) {
450
+ const valid = kernel.dialect === 'sqlite' ? sql `json_valid(m.events_json)` : sql `null`;
451
+ const rows = await run(db => ownedMessages(db, conversationId).select(valid.as('events_json_valid')).orderBy(MESSAGE_POSITION).execute());
452
+ return messageJsonChunks({ rows }, optional);
453
+ },
372
454
  async appendMessage({ conversationId, message }, _optional = {}) {
373
455
  const prepared = redactUserMessage({ message }, { ...(redactionOptions.redactUserText ? { redactUserText: redactionOptions.redactUserText } : {}) });
374
456
  const saved = await writeMessage(conversationId, prepared.message);
@@ -429,6 +511,9 @@ export function createSqlChatStore({ kernel, scope: inputScope }, { clock = crea
429
511
  touch: (required, optional = {}) => atStoreBoundary({ operation: () => store.touch(required, optional) }),
430
512
  delete: (required, optional = {}) => atStoreBoundary({ operation: () => store.delete(required, optional) }),
431
513
  messages: (required, optional = {}) => atStoreBoundary({ operation: () => store.messages(required, optional) }),
514
+ message: (required, optional = {}) => atStoreBoundary({ operation: () => store.message(required, optional) }),
515
+ messagesVersion: (required, optional = {}) => atStoreBoundary({ operation: () => store.messagesVersion(required, optional) }),
516
+ messagesJson: (required, optional = {}) => atStoreBoundary({ operation: () => store.messagesJson(required, optional) }),
432
517
  appendMessage: (required, optional = {}) => atStoreBoundary({ operation: () => store.appendMessage(required, optional) }),
433
518
  pageConversations: (required, optional = {}) => atStoreBoundary({ operation: () => store.pageConversations(required, optional) }),
434
519
  pageMessages: (required, optional = {}) => atStoreBoundary({ operation: () => store.pageMessages(required, optional) }),
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @module store/sqlite/schema
3
3
  *
4
- * Canonical unchanged DDL for ai_chats and ai_chat_messages. Hosts own their database,
4
+ * Canonical DDL for ai_chats and ai_chat_messages. Hosts own their database,
5
5
  * migration ledger and connection configuration. A host with migrations retains its frozen
6
6
  * migration statements; a fresh fixture or host without a migrator may call the explicit helper.
7
7
  * Store construction never invokes it, chooses a file, or sets connection-wide pragmas.
@@ -30,7 +30,7 @@ import type { SqliteClient } from '@jini-ai/db/sqlite';
30
30
  * `expires_at IS NOT NULL`, so authenticated history is out of its reach by construction, not
31
31
  * by a predicate that could drift.
32
32
  */
33
- export declare const CHAT_HISTORY_DDL = "\nCREATE TABLE IF NOT EXISTS ai_chats (\n id TEXT PRIMARY KEY,\n scope_id TEXT NOT NULL,\n owner_kind TEXT NOT NULL CHECK (owner_kind IN ('user','guest')),\n owner_id TEXT NOT NULL,\n title TEXT,\n title_source TEXT NOT NULL DEFAULT 'fallback'\n CHECK (title_source IN ('fallback','generated','manual')),\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL,\n expires_at INTEGER\n);\n\nCREATE INDEX IF NOT EXISTS idx_ai_chats_owner\n ON ai_chats(scope_id, owner_kind, owner_id, updated_at DESC);\n\nCREATE INDEX IF NOT EXISTS idx_ai_chats_expiry\n ON ai_chats(expires_at) WHERE expires_at IS NOT NULL;\n\nCREATE TABLE IF NOT EXISTS ai_chat_messages (\n id TEXT PRIMARY KEY,\n conversation_id TEXT NOT NULL REFERENCES ai_chats(id) ON DELETE CASCADE,\n role TEXT NOT NULL CHECK (role IN ('user','assistant')),\n content TEXT NOT NULL,\n agent_id TEXT,\n agent_name TEXT,\n events_json TEXT,\n attachments_json TEXT,\n run_id TEXT,\n run_status TEXT,\n position INTEGER NOT NULL,\n created_at INTEGER NOT NULL,\n started_at INTEGER,\n ended_at INTEGER,\n UNIQUE (conversation_id, position)\n);\n\nCREATE INDEX IF NOT EXISTS idx_ai_chat_messages_order\n ON ai_chat_messages(conversation_id, position);\n";
33
+ export declare const CHAT_HISTORY_DDL = "\nCREATE TABLE IF NOT EXISTS ai_chats (\n id TEXT PRIMARY KEY,\n scope_id TEXT NOT NULL,\n owner_kind TEXT NOT NULL CHECK (owner_kind IN ('user','guest')),\n owner_id TEXT NOT NULL,\n title TEXT,\n title_source TEXT NOT NULL DEFAULT 'fallback'\n CHECK (title_source IN ('fallback','generated','manual')),\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL,\n expires_at INTEGER\n);\n\nCREATE INDEX IF NOT EXISTS idx_ai_chats_owner\n ON ai_chats(scope_id, owner_kind, owner_id, updated_at DESC);\n\nCREATE INDEX IF NOT EXISTS idx_ai_chats_expiry\n ON ai_chats(expires_at) WHERE expires_at IS NOT NULL;\n\nCREATE TABLE IF NOT EXISTS ai_chat_messages (\n id TEXT PRIMARY KEY,\n conversation_id TEXT NOT NULL REFERENCES ai_chats(id) ON DELETE CASCADE,\n role TEXT NOT NULL CHECK (role IN ('user','assistant')),\n content TEXT NOT NULL,\n agent_id TEXT,\n agent_name TEXT,\n events_json TEXT,\n attachments_json TEXT,\n run_id TEXT,\n run_status TEXT,\n position INTEGER NOT NULL,\n created_at INTEGER NOT NULL,\n started_at INTEGER,\n ended_at INTEGER,\n UNIQUE (conversation_id, position)\n);\n\nCREATE INDEX IF NOT EXISTS idx_ai_chat_messages_order\n ON ai_chat_messages(conversation_id, position);\n\n-- Idle probes read only this covering index, never the overflowing event rows.\nCREATE INDEX IF NOT EXISTS idx_ai_chat_messages_active\n ON ai_chat_messages(role, run_status, id) WHERE run_status IN ('queued', 'running');\n";
34
34
  /**
35
35
  * Creates the chat-history tables in the given database if they are absent.
36
36
  *
@@ -61,6 +61,10 @@ CREATE TABLE IF NOT EXISTS ai_chat_messages (
61
61
 
62
62
  CREATE INDEX IF NOT EXISTS idx_ai_chat_messages_order
63
63
  ON ai_chat_messages(conversation_id, position);
64
+
65
+ -- Idle probes read only this covering index, never the overflowing event rows.
66
+ CREATE INDEX IF NOT EXISTS idx_ai_chat_messages_active
67
+ ON ai_chat_messages(role, run_status, id) WHERE run_status IN ('queued', 'running');
64
68
  `;
65
69
  /**
66
70
  * Creates the chat-history tables in the given database if they are absent.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jini-ai/chat",
3
- "version": "0.6.17",
3
+ "version": "0.6.21",
4
4
  "description": "Jini's chat/artifact frontend, consolidated into one package with two subpaths: framework-free chat vocabulary and pure parsers in `/core`, headless hooks + presentational components + slots in `/react`. Import only the layer you need.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -214,10 +214,10 @@
214
214
  },
215
215
  "dependencies": {
216
216
  "kysely": "^0.29.6",
217
- "@jini-ai/core": "^0.6.17",
218
- "@jini-ai/agentic": "^0.6.17",
219
- "@jini-ai/db": "^0.6.17",
220
- "@jini-ai/diagnostics": "^0.6.17"
217
+ "@jini-ai/core": "^0.6.21",
218
+ "@jini-ai/diagnostics": "^0.6.19",
219
+ "@jini-ai/agentic": "^0.6.19",
220
+ "@jini-ai/db": "^0.6.19"
221
221
  },
222
222
  "peerDependencies": {
223
223
  "react": "^18.3.0 || ^19.0.0",
@@ -261,13 +261,13 @@
261
261
  "pg": "8.23.0",
262
262
  "@ag-ui/core": "0.0.58",
263
263
  "fast-check": "4.3.0",
264
- "@jini-ai/agentic": "^0.6.17",
265
- "@jini-ai/agent-runtime": "0.6.17",
266
- "@jini-ai/db": "0.6.17",
267
- "@jini-ai/protocol": "^0.6.17",
268
- "@jini-ai/core": "^0.6.17",
269
- "@jini-ai/diagnostics": "^0.6.17",
270
- "@jini-ai/ui": "^0.6.17"
264
+ "@jini-ai/agentic": "^0.6.19",
265
+ "@jini-ai/core": "^0.6.21",
266
+ "@jini-ai/agent-runtime": "0.6.19",
267
+ "@jini-ai/diagnostics": "^0.6.19",
268
+ "@jini-ai/db": "0.6.19",
269
+ "@jini-ai/protocol": "^0.6.19",
270
+ "@jini-ai/ui": "^0.6.19"
271
271
  },
272
272
  "typesVersions": {
273
273
  "*": {