@jini-ai/chat 0.6.10 → 0.6.12

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 (33) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/core/accepted-input.d.ts +2 -0
  3. package/dist/core/accepted-input.js +34 -3
  4. package/dist/core/assistant-client/durable-run-subscription.d.ts +4 -2
  5. package/dist/core/assistant-client/durable-run-subscription.js +36 -7
  6. package/dist/core/persistence/title.d.ts +8 -16
  7. package/dist/core/persistence/title.js +20 -63
  8. package/dist/core/question-answer.js +2 -2
  9. package/dist/core/run-activity.js +4 -0
  10. package/dist/core/run-events/entry.d.ts +1 -1
  11. package/dist/core/run-events/entry.js +2 -1
  12. package/dist/core/turn-usage.js +19 -2
  13. package/dist/react/components/Markdown.js +2 -2
  14. package/dist/react/components/McpUiSurfaceCard.js +10 -7
  15. package/dist/react/components/MessageList.hooks.js +5 -1
  16. package/dist/react/components/MessageRow.js +13 -8
  17. package/dist/react/components/MessageRow.usage.d.ts +9 -1
  18. package/dist/react/components/MessageRow.usage.js +22 -5
  19. package/dist/react/components/MidRunUserMessage.d.ts +1 -1
  20. package/dist/react/components/MidRunUserMessage.js +8 -5
  21. package/dist/react/components/ToolCallGroup.hooks.d.ts +3 -1
  22. package/dist/react/components/ToolCallGroup.hooks.js +2 -1
  23. package/dist/react/components/ToolCallGroup.js +1 -1
  24. package/dist/react/features/chat-pane/create-mid-run-message-poster.d.ts +3 -3
  25. package/dist/react/features/chat-pane/create-mid-run-message-poster.js +8 -5
  26. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +88 -27
  27. package/dist/react/features/chat-pane/rules.d.ts +5 -2
  28. package/dist/react/features/chat-pane/rules.js +8 -2
  29. package/dist/react/features/chat-pane/types.d.ts +7 -2
  30. package/dist/server/durable-runs/recover.js +59 -23
  31. package/dist/store/sql/store.d.ts +2 -2
  32. package/dist/store/sql/store.js +11 -3
  33. package/package.json +9 -9
@@ -17,22 +17,29 @@ function lastIndexWhere({ events, match }, _optional = {}) {
17
17
  return index;
18
18
  return -1;
19
19
  }
20
- /** One saved event's contribution to the executor diagnostics. */
21
- function diagnosticLines({ event }, _optional = {}) {
22
- if (event.kind === "status")
23
- return [event.label, event.detail ?? ""];
24
- if (event.kind !== "raw")
25
- return [];
20
+ /** One complete stdout line's contribution to executor diagnostics. */
21
+ function diagnosticLine({ line }, _optional = {}) {
26
22
  try {
27
- const result = JSON.parse(event.line);
23
+ const result = JSON.parse(line);
28
24
  // Successful CLI summaries contain usage metadata and answer prose, not failure evidence.
29
25
  if (result?.type === "result" && result.is_error === false)
30
26
  return [];
27
+ // A subagent report can discuss billing/auth in completed work; it is not executor evidence.
28
+ if (result?.type === "system" && result.subtype === "task_notification")
29
+ return [];
31
30
  }
32
31
  catch {
33
32
  // Non-JSON stdout can still contain the executor's failure reason.
34
33
  }
35
- return [event.line];
34
+ return [line];
35
+ }
36
+ /** A saved raw event may hold several JSONL lines; filter each before failure classification. */
37
+ function diagnosticLines({ event }, _optional = {}) {
38
+ if (event.kind === "status")
39
+ return [event.label, event.detail ?? ""];
40
+ if (event.kind !== "raw")
41
+ return [];
42
+ return event.line.split(/\r?\n/).flatMap(line => diagnosticLine({ line }));
36
43
  }
37
44
  /** Retryability is independent of session resumability. Inspect executor diagnostics, never
38
45
  * answer text or tool results: those can quote unrelated failures from completed work. Old
@@ -118,8 +125,8 @@ function savedWork({ run }, _optional = {}) {
118
125
  return saved;
119
126
  }
120
127
  /** A dead attempt continues now; a fresh queued one waits (see the caller's comment). */
121
- function continuesNow({ run, probe, trigger, now }, _optional = {}) {
122
- return probe === "dead" && (trigger === "attempt-failed" || now - run.attemptStartedAt >= 10_000);
128
+ function continuesNow({ run, trigger, now }, _optional = {}) {
129
+ return trigger === "attempt-failed" || now - run.attemptStartedAt >= 10_000;
123
130
  }
124
131
  /** Recovery coordinator behind explicit host ports. One CAS owns both the attempt and its conversation slot.
125
132
  * Each recovery decision logs one `logger.info` line (default console) of run metadata. */
@@ -193,37 +200,66 @@ export function createDurableRecovery(ports, { logger = console } = {}) {
193
200
  if (exclusion(run))
194
201
  return { decision: await finalize(run, STOPPED_NOTICE, { canceled: true }) };
195
202
  const probe = await probeAttempt({ run, trigger, liveRunId });
203
+ // HTTP/storage awaits can cross cancellation, settlement, or an attempt-generation change.
204
+ // Re-read before attaching or applying diagnostics to the probed attempt.
205
+ const current = await reload(run);
206
+ if (typeof current === "string")
207
+ return { probe, decision: current };
196
208
  if (probe === "live")
197
- return { probe, decision: await reattach(run) };
209
+ return { probe, decision: await reattach(current) };
210
+ if (probe === "uncertain") {
211
+ // Transport uncertainty is not proof of death and spends no restart/time budget. The
212
+ // existing watcher waits and probes again; even an expired budget cannot kill a live child.
213
+ await reattach(current);
214
+ return { probe, decision: "waiting" };
215
+ }
216
+ return decideDead(current, trigger);
217
+ }
218
+ async function decideDead(run, trigger) {
219
+ const probe = "dead";
198
220
  // A pending human answer takes precedence over executor exit diagnostics.
199
- if (probe === "dead" && awaitingAnswer({ run }))
200
- return { probe, decision: await resolveMissing(run, probe, trigger) };
221
+ if (awaitingAnswer({ run }))
222
+ return { probe, decision: await resolveMissing(run, trigger) };
201
223
  // Diagnostics decide only the fate of an attempt that is no longer running. Read before the
202
224
  // probe, raw stdout quoting a tool description ("OAuth-authenticated") canceled a live turn
203
225
  // as "Not logged in" (luvira, 2026-10-08).
204
226
  const diagnostics = attemptDiagnostics({ run });
205
227
  const failure = attemptFailure({ diagnostics });
206
- const decision = failure === "retryable" ? await resolveMissing(run, probe, trigger) : await finalizeFailure({ run, failure, diagnostics });
228
+ const decision = failure === "retryable" ? await resolveMissing(run, trigger) : await finalizeFailure({ run, failure, diagnostics });
207
229
  return { probe, failure, decision };
208
230
  }
209
- async function resolveMissing(run, probe, trigger) {
231
+ async function resolveMissing(run, trigger) {
210
232
  await ports.store.recoveryClock({ runId: run.runId, now: ports.now(), active: true }, {});
211
- const timed = await ports.store.load({ messageId: run.messageId }, {}) ?? run;
233
+ const timed = await reload(run);
234
+ return typeof timed === "string" ? timed : resolveDead(timed, trigger);
235
+ }
236
+ /** The saved row after an await, or the outcome that ends this decision. O(1) store reads. */
237
+ async function reload(run) {
238
+ const current = await ports.store.load({ messageId: run.messageId }, {});
239
+ if (!current)
240
+ return "gone";
212
241
  // A probe only speaks about the generation it inspected. Never apply its dead result to
213
242
  // a newer attempt committed by another caller while this one was awaiting storage/HTTP.
214
- if (timed.runId !== run.runId)
243
+ if (current.runId !== run.runId)
215
244
  return "superseded";
245
+ if (current.message.runStatus !== "queued" && current.message.runStatus !== "running")
246
+ return "terminal";
247
+ if (exclusion(current))
248
+ return finalize(current, STOPPED_NOTICE, { canceled: true });
249
+ return current;
250
+ }
251
+ async function resolveDead(run, trigger) {
216
252
  // A dead process cannot resume its question. End this turn with the existing card intact,
217
253
  // rather than minting an attempt that runs preflight and asks the same question again.
218
- if (probe === "dead" && awaitingAnswer({ run: timed }))
219
- return finalize(timed, AWAITING_ANSWER_NOTICE);
220
- const notice = budgetNotice(timed, ports.now());
254
+ if (awaitingAnswer({ run }))
255
+ return finalize(run, AWAITING_ANSWER_NOTICE);
256
+ const notice = budgetNotice(run, ports.now());
221
257
  if (notice)
222
- return finalize(run, notice, { detail: notice === EXHAUSTED_NOTICE ? exhaustedDetail({ run: timed }) : undefined });
258
+ return finalize(run, notice, { detail: notice === EXHAUSTED_NOTICE ? exhaustedDetail({ run }) : undefined });
223
259
  // A queued attempt may be between durable acceptance and the daemon's start transaction.
224
260
  // Give that idempotent launch time to appear before a 404 can advance its generation.
225
- if (continuesNow({ run, probe, trigger, now: ports.now() }))
226
- return continueDead(timed);
261
+ if (continuesNow({ run, trigger, now: ports.now() }))
262
+ return continueDead(run);
227
263
  ports.attach(run, {});
228
264
  return "waiting";
229
265
  }
@@ -28,8 +28,8 @@ export declare function toMessage(row: MessageRow): ChatMessage;
28
28
  * @param scope the isolation predicate, resolved from the host's authentication. For a guest,
29
29
  * `ownerId` must already be a hash of the session key (`tenant-scope.ts`).
30
30
  * @param optional Optional core Clock for deterministic host/tests; defaults to system wall time.
31
- * @complexity each method is one to four indexed statements; `list` is O(owned conversations),
32
- * `messages` O(messages in the conversation).
31
+ * @complexity each method is one to four indexed statements; conversation summaries scan their
32
+ * messages and embedded events in SQL; `messages` is O(messages in the conversation).
33
33
  */
34
34
  export declare function createSqlChatStore<DB extends ChatDatabase>({ kernel, scope: inputScope }: {
35
35
  kernel: StorageKernel<DB>;
@@ -13,6 +13,7 @@ import { sql } from 'kysely';
13
13
  import { createSystemClock } from '@jini-ai/core/primitives';
14
14
  import { isUniqueViolation } from '@jini-ai/db/kernel';
15
15
  import { redactUserMessage } from '../../core/user-text-redaction.js';
16
+ import { MID_RUN_USER_MESSAGE_EVENT_NAME } from '../../core/turn-usage.js';
16
17
  import { ChatStoreError } from '../errors.js';
17
18
  import { copyScope, atStoreBoundary, runChatQuery } from './boundary.js';
18
19
  import { pageLimit, decodeCursor, encodeCursor } from './cursor.js';
@@ -113,8 +114,8 @@ function toHistoryMessage(row, { includeToolEvents }) {
113
114
  * @param scope the isolation predicate, resolved from the host's authentication. For a guest,
114
115
  * `ownerId` must already be a hash of the session key (`tenant-scope.ts`).
115
116
  * @param optional Optional core Clock for deterministic host/tests; defaults to system wall time.
116
- * @complexity each method is one to four indexed statements; `list` is O(owned conversations),
117
- * `messages` O(messages in the conversation).
117
+ * @complexity each method is one to four indexed statements; conversation summaries scan their
118
+ * messages and embedded events in SQL; `messages` is O(messages in the conversation).
118
119
  */
119
120
  export function createSqlChatStore({ kernel, scope: inputScope }, { clock = createSystemClock(), ...redactionOptions } = {}) {
120
121
  const scope = copyScope({ scope: inputScope });
@@ -122,6 +123,13 @@ export function createSqlChatStore({ kernel, scope: inputScope }, { clock = crea
122
123
  // The single subset boundary: DB must contain both required table definitions. Extra host tables
123
124
  // stay on the exact borrowed kernel; its transaction context is never projected or recreated.
124
125
  const run = (body) => runChatQuery({ kernel: kernel, body: body });
126
+ // Count embedded user messages in SQL: list/get still return summaries, never whole transcripts.
127
+ // Writers store `JSON.stringify` output (no whitespace, as appendedEventsJson also relies on), so
128
+ // each mid-run event holds this exact text once; text inside a JSON string has escaped quotes and
129
+ // cannot match, and invalid JSON stays readable (it counts what it holds, never throws). Plain
130
+ // length/replace run on SQLite and PostgreSQL 14+, unlike `IS JSON` (PostgreSQL 16+).
131
+ const needle = `"name":"${MID_RUN_USER_MESSAGE_EVENT_NAME}"`;
132
+ const midRunCount = sql `((length(coalesce(m.events_json, '')) - length(replace(coalesce(m.events_json, ''), ${sql.lit(needle)}, ''))) / ${sql.lit(needle.length)})`;
125
133
  /** This scope's conversations, projected as {@link ConversationRow}s with their message count. */
126
134
  function ownedConversations(db) {
127
135
  return db
@@ -135,7 +143,7 @@ export function createSqlChatStore({ kernel, scope: inputScope }, { clock = crea
135
143
  "c.expires_at",
136
144
  eb
137
145
  .selectFrom("ai_chat_messages as m")
138
- .select((sb) => sb.fn.countAll().as("n"))
146
+ .select(sql `coalesce(sum(1 + case when m.role = 'assistant' then ${midRunCount} else 0 end), 0)`.as("n"))
139
147
  .whereRef("m.conversation_id", "=", "c.id")
140
148
  .as("message_count"),
141
149
  ])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jini-ai/chat",
3
- "version": "0.6.10",
3
+ "version": "0.6.12",
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": {
@@ -196,7 +196,7 @@
196
196
  "registry": "https://registry.npmjs.org"
197
197
  },
198
198
  "dependencies": {
199
- "@jini-ai/core": "^0.6.10"
199
+ "@jini-ai/core": "^0.6.12"
200
200
  },
201
201
  "peerDependencies": {
202
202
  "react": "^18.3.0 || ^19.0.0",
@@ -303,13 +303,13 @@
303
303
  "pg": "8.23.0",
304
304
  "@ag-ui/core": "0.0.58",
305
305
  "fast-check": "4.3.0",
306
- "@jini-ai/agentic": "^0.6.10",
307
- "@jini-ai/db": "0.6.10",
308
- "@jini-ai/agent-runtime": "0.6.10",
309
- "@jini-ai/ui": "^0.6.10",
310
- "@jini-ai/core": "^0.6.10",
311
- "@jini-ai/diagnostics": "^0.6.10",
312
- "@jini-ai/protocol": "^0.6.10"
306
+ "@jini-ai/db": "0.6.12",
307
+ "@jini-ai/agentic": "^0.6.12",
308
+ "@jini-ai/agent-runtime": "0.6.12",
309
+ "@jini-ai/diagnostics": "^0.6.12",
310
+ "@jini-ai/ui": "^0.6.12",
311
+ "@jini-ai/core": "^0.6.12",
312
+ "@jini-ai/protocol": "^0.6.12"
313
313
  },
314
314
  "typesVersions": {
315
315
  "*": {