@jini-ai/chat 0.6.5 → 0.6.7

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 (161) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/core/accepted-input.d.ts +54 -0
  3. package/dist/core/accepted-input.js +165 -0
  4. package/dist/core/agent-plugin-chat-state.d.ts +43 -0
  5. package/dist/core/agent-plugin-chat-state.js +48 -0
  6. package/dist/core/assistant-client/a2ui-action-poster.js +32 -28
  7. package/dist/core/assistant-client/messages.d.ts +1 -1
  8. package/dist/core/assistant-client/messages.js +2 -2
  9. package/dist/core/assistant-client/transport.d.ts +2 -2
  10. package/dist/core/assistant-client/transport.js +184 -97
  11. package/dist/core/assistant-content.d.ts +2 -1
  12. package/dist/core/assistant-content.js +9 -11
  13. package/dist/core/browser/page-actions.js +4 -2
  14. package/dist/core/composer-history.d.ts +16 -9
  15. package/dist/core/composer-history.js +28 -16
  16. package/dist/core/conversation-title.d.ts +3 -0
  17. package/dist/core/conversation-title.js +3 -0
  18. package/dist/core/durable-projection.js +20 -13
  19. package/dist/core/durable-runs/continuation.d.ts +4 -0
  20. package/dist/core/durable-runs/continuation.js +35 -6
  21. package/dist/core/durable-runs/ledger-contracts.d.ts +2 -1
  22. package/dist/core/durable-runs/policies.d.ts +1 -0
  23. package/dist/core/durable-runs/policies.js +1 -0
  24. package/dist/core/durable-runs/ports.d.ts +6 -2
  25. package/dist/core/index.d.ts +2 -0
  26. package/dist/core/index.js +2 -0
  27. package/dist/core/messages.d.ts +2 -0
  28. package/dist/core/partial-json.d.ts +6 -7
  29. package/dist/core/partial-json.js +81 -47
  30. package/dist/core/persistence/title.js +4 -2
  31. package/dist/core/question-answer.d.ts +23 -0
  32. package/dist/core/question-answer.js +66 -0
  33. package/dist/core/question-form/format.d.ts +6 -6
  34. package/dist/core/question-form/format.js +16 -17
  35. package/dist/core/question-form/normalize.d.ts +18 -0
  36. package/dist/core/question-form/normalize.js +112 -122
  37. package/dist/core/question-form/scan.js +141 -203
  38. package/dist/core/run-activity.js +62 -49
  39. package/dist/core/run-events/entry.d.ts +10 -1
  40. package/dist/core/run-events/entry.js +117 -87
  41. package/dist/core/todos.d.ts +1 -1
  42. package/dist/core/todos.js +60 -85
  43. package/dist/core/tool-events.js +14 -12
  44. package/dist/core/transcript.js +62 -16
  45. package/dist/core/turn-usage.d.ts +2 -1
  46. package/dist/core/turn-usage.js +56 -27
  47. package/dist/core/user-text-redaction.js +3 -0
  48. package/dist/core/util/manifest.js +77 -82
  49. package/dist/core/util/markdown-context.js +51 -53
  50. package/dist/core/util/parser.js +136 -144
  51. package/dist/core/util/pointer.js +16 -16
  52. package/dist/core/util/strip.js +57 -67
  53. package/dist/core/util/validate.js +15 -17
  54. package/dist/embed-widget/browser/session-store.js +21 -12
  55. package/dist/embed-widget/transports/fetch-sse.js +108 -95
  56. package/dist/embed-widget/vanilla/index.js +81 -70
  57. package/dist/react/agent-bridge/frontend-session-bridge.js +80 -52
  58. package/dist/react/components/AttachmentPreviewModal.js +25 -1
  59. package/dist/react/components/ChatSettingsDialog.d.ts +13 -0
  60. package/dist/react/components/ChatSettingsDialog.hooks.d.ts +115 -0
  61. package/dist/react/components/ChatSettingsDialog.hooks.js +40 -0
  62. package/dist/react/components/ChatSettingsDialog.js +72 -0
  63. package/dist/react/components/ChatSettingsSegmented.d.ts +18 -0
  64. package/dist/react/components/ChatSettingsSegmented.js +11 -0
  65. package/dist/react/components/Composer.d.ts +5 -1
  66. package/dist/react/components/Composer.js +126 -82
  67. package/dist/react/components/ConversationList.js +25 -39
  68. package/dist/react/components/ConversationTitleEditor.d.ts +27 -0
  69. package/dist/react/components/ConversationTitleEditor.js +54 -0
  70. package/dist/react/components/Icon.d.ts +1 -1
  71. package/dist/react/components/Icon.js +25 -45
  72. package/dist/react/components/Markdown.js +134 -123
  73. package/dist/react/components/McpUiSurfaceCard.d.ts +2 -2
  74. package/dist/react/components/McpUiSurfaceCard.js +30 -9
  75. package/dist/react/components/MessageList.d.ts +13 -13
  76. package/dist/react/components/MessageList.hooks.d.ts +6 -1
  77. package/dist/react/components/MessageList.hooks.js +19 -3
  78. package/dist/react/components/MessageList.js +34 -10
  79. package/dist/react/components/MessageRow.js +48 -32
  80. package/dist/react/components/MessageRow.usage.d.ts +5 -2
  81. package/dist/react/components/MessageRow.usage.js +20 -11
  82. package/dist/react/components/MidRunUserMessage.js +16 -3
  83. package/dist/react/components/QuestionForm.js +148 -76
  84. package/dist/react/components/QuestionsPanel.js +1 -1
  85. package/dist/react/components/TodoCard.js +12 -6
  86. package/dist/react/components/ToolCard.js +47 -40
  87. package/dist/react/components/composer-discovery.js +6 -1
  88. package/dist/react/components/mcp-ui-surface-state.d.ts +6 -11
  89. package/dist/react/components/mcp-ui-surface-state.js +23 -8
  90. package/dist/react/ext-event-renderer-registry.d.ts +8 -0
  91. package/dist/react/ext-event-renderer-registry.js +5 -2
  92. package/dist/react/features/chat-pane/components/AgentPluginUsage.d.ts +7 -0
  93. package/dist/react/features/chat-pane/components/AgentPluginUsage.js +45 -0
  94. package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +55 -19
  95. package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
  96. package/dist/react/features/chat-pane/components/ChatPane.js +192 -74
  97. package/dist/react/features/chat-pane/components/RuntimeDropUp.d.ts +29 -0
  98. package/dist/react/features/chat-pane/components/RuntimeDropUp.js +45 -0
  99. package/dist/react/features/chat-pane/create-daemon-attachment-uploader.js +80 -47
  100. package/dist/react/features/chat-pane/create-mcp-ui-tool-caller.js +44 -40
  101. package/dist/react/features/chat-pane/create-mid-run-message-poster.d.ts +6 -6
  102. package/dist/react/features/chat-pane/create-mid-run-message-poster.js +8 -8
  103. package/dist/react/features/chat-pane/create-typed-answer-poster.d.ts +3 -3
  104. package/dist/react/features/chat-pane/create-typed-answer-poster.js +3 -3
  105. package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.d.ts +6 -1
  106. package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +11 -5
  107. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +12 -7
  108. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +340 -149
  109. package/dist/react/features/chat-pane/hooks/useChatPaneControlsHeight.hooks.js +7 -6
  110. package/dist/react/features/chat-pane/hooks/useChatPaneNewTabLinks.hooks.js +28 -10
  111. package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.d.ts +3 -3
  112. package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +60 -12
  113. package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.d.ts +49 -0
  114. package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.js +143 -0
  115. package/dist/react/features/chat-pane/model-options.d.ts +18 -1
  116. package/dist/react/features/chat-pane/model-options.js +49 -20
  117. package/dist/react/features/chat-pane/rules.d.ts +8 -9
  118. package/dist/react/features/chat-pane/rules.js +84 -46
  119. package/dist/react/features/chat-pane/styles.d.ts +1 -1
  120. package/dist/react/features/chat-pane/styles.js +156 -37
  121. package/dist/react/features/chat-pane/types.d.ts +17 -8
  122. package/dist/react/hooks/assistant-chats.js +12 -5
  123. package/dist/react/hooks/composer-draft-cache.js +116 -118
  124. package/dist/react/hooks/composer-history-storage.js +5 -5
  125. package/dist/react/hooks/fab-position.js +7 -4
  126. package/dist/react/hooks/useAnsweredCards.d.ts +21 -4
  127. package/dist/react/hooks/useAnsweredCards.js +38 -11
  128. package/dist/react/hooks/useArtifactStream.js +28 -22
  129. package/dist/react/hooks/useComposer.d.ts +20 -0
  130. package/dist/react/hooks/useComposer.js +22 -9
  131. package/dist/react/hooks/useComposerHistory.hooks.d.ts +6 -1
  132. package/dist/react/hooks/useComposerHistory.hooks.js +39 -19
  133. package/dist/react/hooks/useComposerHistoryKeys.hooks.js +24 -14
  134. package/dist/react/hooks/useConversation.d.ts +7 -0
  135. package/dist/react/hooks/useConversation.js +23 -10
  136. package/dist/react/hooks/useExtEventGroups.js +39 -39
  137. package/dist/react/hooks/useQuestionForms.js +21 -26
  138. package/dist/react/hooks/useRunStream.js +10 -2
  139. package/dist/react/index.d.ts +7 -1
  140. package/dist/react/index.js +9 -0
  141. package/dist/react/message-blocks.d.ts +6 -4
  142. package/dist/react/message-blocks.js +94 -72
  143. package/dist/react/slots.d.ts +2 -2
  144. package/dist/react/tool-call-display.js +19 -17
  145. package/dist/server/durable-runs/recover.d.ts +1 -1
  146. package/dist/server/durable-runs/recover.js +139 -76
  147. package/dist/server/run-finalizer/entry.js +25 -15
  148. package/dist/store/legacy/sqlite/conversations.d.ts +11 -11
  149. package/dist/store/legacy/sqlite/conversations.js +69 -98
  150. package/dist/store/legacy/sqlite/messages.d.ts +10 -2
  151. package/dist/store/legacy/sqlite/messages.js +113 -71
  152. package/dist/store/runtime/chat-history-store.js +9 -3
  153. package/dist/store/runtime/durable-run-store.d.ts +2 -2
  154. package/dist/store/runtime/durable-run-store.js +165 -117
  155. package/dist/store/runtime/history-tools.js +118 -82
  156. package/dist/store/runtime/run-ledger.d.ts +4 -1
  157. package/dist/store/runtime/run-ledger.js +56 -16
  158. package/dist/store/sql/cursor.js +38 -18
  159. package/dist/store/sql/store.d.ts +18 -0
  160. package/dist/store/sql/store.js +46 -15
  161. package/package.json +12 -10
@@ -1,5 +1,9 @@
1
1
  import { isTerminalRunStatus } from '../../core/messages.js';
2
2
  import { readSseFrames, runContentFromEvents, runEventsForSave, runInterruptedNotice, translateRunFrame } from '../../core/run-events/entry.js';
3
+ /** An error frame keeps a later successful end from settling as a success. */
4
+ function endStatus({ failed, terminal }, _optional = {}) {
5
+ return failed && terminal === "succeeded" ? "failed" : terminal;
6
+ }
3
7
  /** Follow replayable run streams and save through atomic host ledger operations.
4
8
  * The host retains authorization, owner scope, kernel transactions, boot reconciliation, HTTP
5
9
  * credentials and excluded run-ID prefixes. Replay starts at zero; error-before-success and
@@ -22,11 +26,14 @@ export function createAssistantRunFinalizer(args, options = {}) {
22
26
  catch { /* The host owns reporting failures. */ }
23
27
  }
24
28
  const active = new Map();
25
- async function settle(watch, status, events) {
26
- watch.terminal = true;
29
+ function clearTrailing(watch) {
27
30
  if (watch.trailing)
28
31
  scheduler.cancel({ handle: watch.trailing.handle });
29
32
  watch.trailing = undefined;
33
+ }
34
+ async function settle(watch, status, events) {
35
+ watch.terminal = true;
36
+ clearTrailing(watch);
30
37
  await args.ledger.settle({
31
38
  conversationId: watch.conversationId,
32
39
  messageId: watch.messageId,
@@ -57,9 +64,7 @@ export function createAssistantRunFinalizer(args, options = {}) {
57
64
  }
58
65
  return;
59
66
  }
60
- if (watch.trailing)
61
- scheduler.cancel({ handle: watch.trailing.handle });
62
- watch.trailing = undefined;
67
+ clearTrailing(watch);
63
68
  watch.checkpointedEvents = watch.events.length;
64
69
  watch.checkpointedAt = now();
65
70
  watch.checkpointPending = true;
@@ -77,29 +82,36 @@ export function createAssistantRunFinalizer(args, options = {}) {
77
82
  void checkpoint(watch);
78
83
  });
79
84
  }
80
- async function readStream(watch) {
85
+ /** The connection's event body, or why it cannot be followed. Non-success bodies are cancelled. */
86
+ async function openStream(watch) {
81
87
  const response = await daemon.openEvents({ runId: watch.runId, principalId: watch.principalId });
82
- if (response.status === 404 || !response.ok || !response.body) {
83
- await response.body?.cancel().catch(() => undefined);
84
- return response.status === 404 ? { kind: "gone" } : { kind: "dropped" };
85
- }
88
+ if (response.ok && response.body)
89
+ return { kind: "open", body: response.body };
90
+ await response.body?.cancel().catch(() => undefined);
91
+ return response.status === 404 ? { kind: "gone" } : { kind: "dropped" };
92
+ }
93
+ async function consumeFrames(watch, body) {
86
94
  // Each connection replays the complete history, rather than appending duplicate deltas.
87
95
  watch.events = [];
88
96
  watch.failed = false;
89
97
  const carry = {};
90
- for await (const frame of readSseFrames({ body: response.body })) {
98
+ for await (const frame of readSseFrames({ body })) {
91
99
  const outcome = translateRunFrame({ kind: frame.event, raw: frame.data, notices: args.notices }, { carry });
92
100
  watch.events.push(...outcome.events);
93
101
  if (outcome.error)
94
102
  watch.failed = true;
95
103
  if (outcome.terminal) {
96
104
  watch.terminal = true;
97
- return { kind: "ended", status: watch.failed && outcome.terminal === "succeeded" ? "failed" : outcome.terminal };
105
+ return { kind: "ended", status: endStatus({ failed: watch.failed, terminal: outcome.terminal }) };
98
106
  }
99
107
  void checkpoint(watch);
100
108
  }
101
109
  return { kind: "dropped" };
102
110
  }
111
+ async function readStream(watch) {
112
+ const opened = await openStream(watch);
113
+ return opened.kind === "open" ? consumeFrames(watch, opened.body) : opened;
114
+ }
103
115
  async function follow(watch) {
104
116
  for (let attempt = 0; attempt <= maxReconnects; attempt += 1) {
105
117
  const result = await readStream(watch).catch(() => ({ kind: "dropped" }));
@@ -140,9 +152,7 @@ export function createAssistantRunFinalizer(args, options = {}) {
140
152
  })
141
153
  .finally(() => {
142
154
  watch.terminal = true;
143
- if (watch.trailing)
144
- scheduler.cancel({ handle: watch.trailing.handle });
145
- watch.trailing = undefined;
155
+ clearTrailing(watch);
146
156
  active.delete(runId);
147
157
  });
148
158
  active.set(runId, { watch, done });
@@ -18,8 +18,8 @@ export declare function listConversations({ db, projectId }: {
18
18
  }): {
19
19
  latestRun: {
20
20
  durationMs?: number;
21
- endedAt?: number | undefined;
22
- startedAt?: number | undefined;
21
+ endedAt?: number;
22
+ startedAt?: number;
23
23
  status: string;
24
24
  } | undefined;
25
25
  totalDurationMs?: number;
@@ -41,13 +41,13 @@ export declare function getConversation({ db, id }: {
41
41
  db: SqliteDb;
42
42
  id: string;
43
43
  }): {
44
- totalDurationMs?: number;
45
44
  latestRun: {
46
45
  durationMs?: number;
47
- endedAt?: number | undefined;
48
- startedAt?: number | undefined;
46
+ endedAt?: number;
47
+ startedAt?: number;
49
48
  status: string;
50
49
  } | undefined;
50
+ totalDurationMs?: number;
51
51
  id: any;
52
52
  projectId: any;
53
53
  title: any;
@@ -75,13 +75,13 @@ export declare function insertConversation({ db, c }: {
75
75
  db: SqliteDb;
76
76
  c: DbRow;
77
77
  }): {
78
- totalDurationMs?: number;
79
78
  latestRun: {
80
79
  durationMs?: number;
81
- endedAt?: number | undefined;
82
- startedAt?: number | undefined;
80
+ endedAt?: number;
81
+ startedAt?: number;
83
82
  status: string;
84
83
  } | undefined;
84
+ totalDurationMs?: number;
85
85
  id: any;
86
86
  projectId: any;
87
87
  title: any;
@@ -102,13 +102,13 @@ export declare function updateConversation({ db, id, patch }: {
102
102
  id: string;
103
103
  patch: DbRow;
104
104
  }): {
105
- totalDurationMs?: number;
106
105
  latestRun: {
107
106
  durationMs?: number;
108
- endedAt?: number | undefined;
109
- startedAt?: number | undefined;
107
+ endedAt?: number;
108
+ startedAt?: number;
110
109
  status: string;
111
110
  } | undefined;
111
+ totalDurationMs?: number;
112
112
  id: any;
113
113
  projectId: any;
114
114
  title: any;
@@ -1,17 +1,15 @@
1
1
  import { rows } from '@jini-ai/db/core';
2
2
  /**
3
- * Returns all conversations for a project, newest first, each enriched with message count,
4
- * the latest assistant run summary, and total accumulated run duration across all terminal runs.
5
- * @param db Open SQLite database handle.
6
- * @param projectId The project whose conversations to list.
3
+ * @internal Builds the summary query shared by `listConversations` and `getConversation`: every
4
+ * conversation matching `filterColumn = ?`, each enriched with message count, the latest assistant
5
+ * run summary, and total accumulated run duration across all terminal runs.
7
6
  */
8
- export function listConversations({ db, projectId }) {
9
- return rows(db
10
- .prepare(`WITH project_conversations AS (
7
+ function conversationSummarySql(filterColumn) {
8
+ return `WITH selected_conversations AS (
11
9
  SELECT id, project_id AS projectId, title, session_mode AS sessionMode,
12
10
  created_at AS createdAt, updated_at AS updatedAt
13
11
  FROM conversations
14
- WHERE project_id = ?
12
+ WHERE ${filterColumn} = ?
15
13
  ),
16
14
  latest_runs AS (
17
15
  SELECT conversation_id AS conversationId,
@@ -30,7 +28,7 @@ export function listConversations({ db, projectId }) {
30
28
  ORDER BY m.position DESC
31
29
  ) AS rn
32
30
  FROM messages m
33
- JOIN project_conversations c ON c.id = m.conversation_id
31
+ JOIN selected_conversations c ON c.id = m.conversation_id
34
32
  WHERE m.role = 'assistant'
35
33
  AND m.run_status IS NOT NULL
36
34
  )
@@ -40,14 +38,14 @@ export function listConversations({ db, projectId }) {
40
38
  SELECT m.conversation_id AS conversationId,
41
39
  COUNT(*) AS messageCount
42
40
  FROM messages m
43
- JOIN project_conversations c ON c.id = m.conversation_id
41
+ JOIN selected_conversations c ON c.id = m.conversation_id
44
42
  GROUP BY m.conversation_id
45
43
  ),
46
44
  total_run_durations AS (
47
45
  SELECT m.conversation_id AS conversationId,
48
- SUM(${terminalRunDurationSql('m')}) AS totalDurationMs
46
+ SUM(${TERMINAL_RUN_DURATION_SQL}) AS totalDurationMs
49
47
  FROM messages m
50
- JOIN project_conversations c ON c.id = m.conversation_id
48
+ JOIN selected_conversations c ON c.id = m.conversation_id
51
49
  WHERE m.role = 'assistant'
52
50
  AND m.run_status IN ('succeeded', 'failed', 'canceled')
53
51
  GROUP BY m.conversation_id
@@ -57,11 +55,20 @@ export function listConversations({ db, projectId }) {
57
55
  lr.latestRunStatus, lr.latestRunStartedAt,
58
56
  lr.latestRunEndedAt, lr.latestRunEventsJson,
59
57
  trd.totalDurationMs
60
- FROM project_conversations c
58
+ FROM selected_conversations c
61
59
  LEFT JOIN latest_runs lr ON lr.conversationId = c.id
62
60
  LEFT JOIN message_counts mc ON mc.conversationId = c.id
63
- LEFT JOIN total_run_durations trd ON trd.conversationId = c.id
64
- ORDER BY c.updatedAt DESC`)
61
+ LEFT JOIN total_run_durations trd ON trd.conversationId = c.id`;
62
+ }
63
+ /**
64
+ * Returns all conversations for a project, newest first, each enriched with message count,
65
+ * the latest assistant run summary, and total accumulated run duration across all terminal runs.
66
+ * @param db Open SQLite database handle.
67
+ * @param projectId The project whose conversations to list.
68
+ */
69
+ export function listConversations({ db, projectId }) {
70
+ return rows(db
71
+ .prepare(`${conversationSummarySql('project_id')}\n ORDER BY c.updatedAt DESC`)
65
72
  .all(projectId)).map(normalizeConversation);
66
73
  }
67
74
  /**
@@ -71,19 +78,8 @@ export function listConversations({ db, projectId }) {
71
78
  * @param id Conversation id.
72
79
  */
73
80
  export function getConversation({ db, id }) {
74
- const r = db
75
- .prepare(`SELECT id, project_id AS projectId, title, session_mode AS sessionMode,
76
- created_at AS createdAt, updated_at AS updatedAt,
77
- (SELECT COUNT(*) FROM messages WHERE conversation_id = conversations.id) AS messageCount
78
- FROM conversations WHERE id = ?`)
79
- .get(id);
80
- if (!r)
81
- return null;
82
- return {
83
- ...normalizeConversation(r),
84
- latestRun: latestConversationRunSummary(db, r.id) ?? undefined,
85
- ...numberProperty('totalDurationMs', totalConversationRunDurationMs(db, r.id)),
86
- };
81
+ const r = db.prepare(conversationSummarySql('id')).get(id);
82
+ return r ? normalizeConversation(r) : null;
87
83
  }
88
84
  /** @internal Shapes a raw SQLite row into the canonical conversation object, including run summary. */
89
85
  function normalizeConversation(r) {
@@ -114,6 +110,11 @@ function normalizeConversation(r) {
114
110
  export function normalizeConversationSessionMode({ value }) {
115
111
  return value === 'chat' || value === 'plan' ? value : 'design';
116
112
  }
113
+ /** @internal Converts a nullable SQLite value to a finite number, or undefined when absent or non-finite. */
114
+ function finiteNumber(value) {
115
+ const n = value == null ? undefined : Number(value);
116
+ return n !== undefined && Number.isFinite(n) ? n : undefined;
117
+ }
117
118
  /**
118
119
  * @internal Produces a single-key object `{ [key]: number }` only when value is a finite number,
119
120
  * omitting the key entirely otherwise. Generic over `key` (rather than plain `string`) so the
@@ -122,46 +123,15 @@ export function normalizeConversationSessionMode({ value }) {
122
123
  * through at the call site.
123
124
  */
124
125
  function numberProperty(key, value) {
125
- const n = value == null ? undefined : Number(value);
126
- return typeof n === 'number' && Number.isFinite(n)
127
- ? { [key]: n }
128
- : {};
129
- }
130
- /** @internal Queries the most recent assistant run summary for a conversation (used by getConversation's per-row fetch path). */
131
- function latestConversationRunSummary(db, conversationId) {
132
- const row = db
133
- .prepare(`SELECT run_status AS runStatus,
134
- started_at AS startedAt,
135
- ended_at AS endedAt,
136
- events_json AS eventsJson
137
- FROM messages
138
- WHERE conversation_id = ?
139
- AND role = 'assistant'
140
- AND run_status IS NOT NULL
141
- ORDER BY position DESC
142
- LIMIT 1`)
143
- .get(conversationId);
144
- return conversationRunSummaryFromRow(row);
126
+ const n = finiteNumber(value);
127
+ return n === undefined ? {} : { [key]: n };
145
128
  }
146
- /** @internal Sums terminal-run duration across all succeeded/failed/canceled assistant messages in a conversation. */
147
- function totalConversationRunDurationMs(db, conversationId) {
148
- const row = db
149
- .prepare(`SELECT SUM(${terminalRunDurationSql()}) AS totalDurationMs
150
- FROM messages
151
- WHERE conversation_id = ?
152
- AND role = 'assistant'
153
- AND run_status IN ('succeeded', 'failed', 'canceled')`)
154
- .get(conversationId);
155
- return row?.totalDurationMs == null ? undefined : Number(row.totalDurationMs);
156
- }
157
- /** @internal Emits the SQL CASE expression that resolves a terminal run's duration: wall-clock diff when both timestamps exist, otherwise the latest usage event's durationMs. */
158
- function terminalRunDurationSql(alias) {
159
- const p = alias ? `${alias}.` : '';
160
- return `CASE
161
- WHEN ${p}started_at IS NOT NULL AND ${p}ended_at IS NOT NULL THEN
129
+ /** @internal The SQL CASE expression that resolves a terminal run's duration (over messages alias `m`): wall-clock diff when both timestamps exist, otherwise the latest usage event's durationMs. */
130
+ const TERMINAL_RUN_DURATION_SQL = `CASE
131
+ WHEN m.started_at IS NOT NULL AND m.ended_at IS NOT NULL THEN
162
132
  CASE
163
- WHEN CAST(${p}ended_at AS INTEGER) >= CAST(${p}started_at AS INTEGER)
164
- THEN CAST(${p}ended_at AS INTEGER) - CAST(${p}started_at AS INTEGER)
133
+ WHEN CAST(m.ended_at AS INTEGER) >= CAST(m.started_at AS INTEGER)
134
+ THEN CAST(m.ended_at AS INTEGER) - CAST(m.started_at AS INTEGER)
165
135
  ELSE 0
166
136
  END
167
137
  ELSE (
@@ -172,8 +142,8 @@ function terminalRunDurationSql(alias) {
172
142
  END
173
143
  FROM json_each(
174
144
  CASE
175
- WHEN json_valid(${p}events_json) AND json_type(${p}events_json) = 'array'
176
- THEN ${p}events_json
145
+ WHEN json_valid(m.events_json) AND json_type(m.events_json) = 'array'
146
+ THEN m.events_json
177
147
  ELSE '[]'
178
148
  END
179
149
  ) AS usage_event
@@ -184,49 +154,50 @@ function terminalRunDurationSql(alias) {
184
154
  LIMIT 1
185
155
  )
186
156
  END`;
187
- }
188
157
  /** @internal Converts a raw message row (runStatus/startedAt/endedAt/eventsJson) into a run summary object, or null when no run status is present. */
189
158
  function conversationRunSummaryFromRow(row) {
190
- if (!row || typeof row.runStatus !== 'string')
159
+ if (typeof row.runStatus !== 'string')
191
160
  return null;
192
- const startedAt = row.startedAt == null ? undefined : Number(row.startedAt);
193
- const endedAt = row.endedAt == null ? undefined : Number(row.endedAt);
194
- const usageDurationMs = latestUsageDurationMs(row.eventsJson);
195
- const durationMs = Number.isFinite(startedAt) && Number.isFinite(endedAt)
161
+ const startedAt = finiteNumber(row.startedAt);
162
+ const endedAt = finiteNumber(row.endedAt);
163
+ const durationMs = startedAt !== undefined && endedAt !== undefined
196
164
  ? Math.max(0, endedAt - startedAt)
197
- : usageDurationMs;
165
+ : latestUsageDurationMs(row.eventsJson);
198
166
  return {
199
167
  status: row.runStatus,
200
- ...(Number.isFinite(startedAt) ? { startedAt } : {}),
201
- ...(Number.isFinite(endedAt) ? { endedAt } : {}),
202
- ...(typeof durationMs === 'number' && Number.isFinite(durationMs)
203
- ? { durationMs }
204
- : {}),
168
+ ...numberProperty('startedAt', startedAt),
169
+ ...numberProperty('endedAt', endedAt),
170
+ ...numberProperty('durationMs', durationMs),
205
171
  };
206
172
  }
207
173
  /** @internal Parses events_json to find the most recent usage event's durationMs; returns undefined when events_json is absent or malformed. */
208
174
  function latestUsageDurationMs(eventsJson) {
175
+ const events = parseEventsArray(eventsJson);
176
+ for (let i = events.length - 1; i >= 0; i -= 1) {
177
+ const durationMs = usageEventDurationMs(events[i]);
178
+ if (durationMs !== undefined)
179
+ return Math.max(0, durationMs);
180
+ }
181
+ return undefined;
182
+ }
183
+ /** @internal Parses events_json as an array; absent, malformed, or non-array JSON yields an empty list. */
184
+ function parseEventsArray(eventsJson) {
209
185
  if (typeof eventsJson !== 'string' || eventsJson.length === 0)
210
- return undefined;
186
+ return [];
211
187
  try {
212
188
  const events = JSON.parse(eventsJson);
213
- if (!Array.isArray(events))
214
- return undefined;
215
- for (let i = events.length - 1; i >= 0; i -= 1) {
216
- const event = events[i];
217
- if (event &&
218
- typeof event === 'object' &&
219
- event.kind === 'usage' &&
220
- typeof event.durationMs === 'number' &&
221
- Number.isFinite(event.durationMs)) {
222
- return Math.max(0, event.durationMs);
223
- }
224
- }
189
+ return Array.isArray(events) ? events : [];
225
190
  }
226
191
  catch {
227
- return undefined;
192
+ return [];
228
193
  }
229
- return undefined;
194
+ }
195
+ /** @internal Returns a usage event's finite durationMs, or undefined for any other event shape. */
196
+ function usageEventDurationMs(event) {
197
+ if (!event || typeof event !== 'object')
198
+ return undefined;
199
+ const { kind, durationMs } = event;
200
+ return kind === 'usage' && typeof durationMs === 'number' && Number.isFinite(durationMs) ? durationMs : undefined;
230
201
  }
231
202
  /**
232
203
  * Inserts a new conversation row and returns the full conversation record (including run summary).
@@ -238,7 +209,7 @@ export function insertConversation({ db, c }) {
238
209
  db.prepare(`INSERT INTO conversations
239
210
  (id, project_id, title, session_mode, created_at, updated_at)
240
211
  VALUES (?, ?, ?, ?, ?, ?)`).run(c.id, c.projectId, c.title ?? null, normalizeConversationSessionMode({ value: c.sessionMode }), c.createdAt, c.updatedAt);
241
- return getConversation({ db: db, id: c.id });
212
+ return getConversation({ db, id: c.id });
242
213
  }
243
214
  /**
244
215
  * Applies a partial patch to an existing conversation, merging with the current record.
@@ -248,7 +219,7 @@ export function insertConversation({ db, c }) {
248
219
  * @param patch Fields to overwrite; sessionMode and updatedAt are normalized before persisting.
249
220
  */
250
221
  export function updateConversation({ db, id, patch }) {
251
- const existing = getConversation({ db: db, id: id });
222
+ const existing = getConversation({ db, id });
252
223
  if (!existing)
253
224
  return null;
254
225
  const merged = {
@@ -261,7 +232,7 @@ export function updateConversation({ db, id, patch }) {
261
232
  };
262
233
  db.prepare(`UPDATE conversations
263
234
  SET title = ?, session_mode = ?, updated_at = ? WHERE id = ?`).run(merged.title ?? null, merged.sessionMode, merged.updatedAt, id);
264
- return getConversation({ db: db, id: id });
235
+ return getConversation({ db, id });
265
236
  }
266
237
  /**
267
238
  * Removes a conversation row by id. Cascading deletes on messages are handled by the schema.
@@ -116,7 +116,15 @@ export declare function appendMessageStatusEvent({ db, messageId, event }: {
116
116
  db: SqliteDb;
117
117
  messageId: string;
118
118
  event: DbRow;
119
- }): any[] | null;
119
+ }): (DbRow | {
120
+ kind: string;
121
+ label: string;
122
+ detail: string;
123
+ } | {
124
+ kind: string;
125
+ label: string;
126
+ detail?: never;
127
+ })[] | null;
120
128
  /**
121
129
  * Appends an arbitrary agent event to the message's events array, also streaming `text` deltas
122
130
  * into the `content` column so the message text grows incrementally during a run.
@@ -130,7 +138,7 @@ export declare function appendMessageAgentEvent({ db, messageId, event }: {
130
138
  db: SqliteDb;
131
139
  messageId: string;
132
140
  event: DbRow;
133
- }): any[] | null;
141
+ }): DbRow[] | null;
134
142
  /**
135
143
  * Hard-deletes a single message row by id.
136
144
  * @param db - Open SQLite connection.