@jini-ai/chat 0.6.6 → 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.
- package/CHANGELOG.md +4 -0
- package/dist/core/accepted-input.d.ts +54 -0
- package/dist/core/accepted-input.js +165 -0
- package/dist/core/agent-plugin-chat-state.d.ts +43 -0
- package/dist/core/agent-plugin-chat-state.js +48 -0
- package/dist/core/assistant-client/a2ui-action-poster.js +32 -28
- package/dist/core/assistant-client/messages.d.ts +1 -1
- package/dist/core/assistant-client/messages.js +2 -2
- package/dist/core/assistant-client/transport.d.ts +2 -2
- package/dist/core/assistant-client/transport.js +184 -97
- package/dist/core/assistant-content.d.ts +2 -1
- package/dist/core/assistant-content.js +9 -11
- package/dist/core/browser/page-actions.js +4 -2
- package/dist/core/composer-history.d.ts +16 -9
- package/dist/core/composer-history.js +28 -16
- package/dist/core/conversation-title.d.ts +3 -0
- package/dist/core/conversation-title.js +3 -0
- package/dist/core/durable-projection.js +20 -13
- package/dist/core/durable-runs/continuation.d.ts +4 -0
- package/dist/core/durable-runs/continuation.js +31 -6
- package/dist/core/durable-runs/ledger-contracts.d.ts +2 -1
- package/dist/core/durable-runs/ports.d.ts +6 -2
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +2 -0
- package/dist/core/messages.d.ts +2 -0
- package/dist/core/partial-json.d.ts +6 -7
- package/dist/core/partial-json.js +81 -47
- package/dist/core/persistence/title.js +4 -2
- package/dist/core/question-answer.d.ts +23 -0
- package/dist/core/question-answer.js +66 -0
- package/dist/core/question-form/format.d.ts +6 -6
- package/dist/core/question-form/format.js +16 -17
- package/dist/core/question-form/normalize.d.ts +18 -0
- package/dist/core/question-form/normalize.js +112 -122
- package/dist/core/question-form/scan.js +141 -203
- package/dist/core/run-activity.js +62 -49
- package/dist/core/run-events/entry.d.ts +10 -1
- package/dist/core/run-events/entry.js +117 -87
- package/dist/core/todos.d.ts +1 -1
- package/dist/core/todos.js +60 -85
- package/dist/core/tool-events.js +14 -12
- package/dist/core/transcript.js +62 -16
- package/dist/core/turn-usage.js +54 -32
- package/dist/core/user-text-redaction.js +3 -0
- package/dist/core/util/manifest.js +77 -82
- package/dist/core/util/markdown-context.js +51 -53
- package/dist/core/util/parser.js +136 -144
- package/dist/core/util/pointer.js +16 -16
- package/dist/core/util/strip.js +57 -67
- package/dist/core/util/validate.js +15 -17
- package/dist/embed-widget/browser/session-store.js +21 -12
- package/dist/embed-widget/transports/fetch-sse.js +108 -95
- package/dist/embed-widget/vanilla/index.js +81 -70
- package/dist/react/agent-bridge/frontend-session-bridge.js +80 -52
- package/dist/react/components/AttachmentPreviewModal.js +25 -1
- package/dist/react/components/ChatSettingsDialog.d.ts +13 -0
- package/dist/react/components/ChatSettingsDialog.hooks.d.ts +115 -0
- package/dist/react/components/ChatSettingsDialog.hooks.js +40 -0
- package/dist/react/components/ChatSettingsDialog.js +72 -0
- package/dist/react/components/ChatSettingsSegmented.d.ts +18 -0
- package/dist/react/components/ChatSettingsSegmented.js +11 -0
- package/dist/react/components/Composer.d.ts +5 -1
- package/dist/react/components/Composer.js +126 -82
- package/dist/react/components/ConversationList.js +25 -39
- package/dist/react/components/ConversationTitleEditor.d.ts +27 -0
- package/dist/react/components/ConversationTitleEditor.js +54 -0
- package/dist/react/components/Icon.d.ts +1 -1
- package/dist/react/components/Icon.js +25 -45
- package/dist/react/components/Markdown.js +134 -123
- package/dist/react/components/McpUiSurfaceCard.d.ts +2 -2
- package/dist/react/components/McpUiSurfaceCard.js +30 -9
- package/dist/react/components/MessageList.d.ts +13 -13
- package/dist/react/components/MessageList.hooks.d.ts +6 -1
- package/dist/react/components/MessageList.hooks.js +19 -3
- package/dist/react/components/MessageList.js +34 -10
- package/dist/react/components/MessageRow.js +47 -31
- package/dist/react/components/MessageRow.usage.js +16 -11
- package/dist/react/components/MidRunUserMessage.js +16 -3
- package/dist/react/components/QuestionForm.js +148 -76
- package/dist/react/components/QuestionsPanel.js +1 -1
- package/dist/react/components/TodoCard.js +12 -6
- package/dist/react/components/ToolCard.js +47 -40
- package/dist/react/components/composer-discovery.js +6 -1
- package/dist/react/components/mcp-ui-surface-state.d.ts +6 -11
- package/dist/react/components/mcp-ui-surface-state.js +23 -8
- package/dist/react/ext-event-renderer-registry.d.ts +8 -0
- package/dist/react/ext-event-renderer-registry.js +5 -2
- package/dist/react/features/chat-pane/components/AgentPluginUsage.d.ts +7 -0
- package/dist/react/features/chat-pane/components/AgentPluginUsage.js +45 -0
- package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +55 -19
- package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
- package/dist/react/features/chat-pane/components/ChatPane.js +192 -74
- package/dist/react/features/chat-pane/components/RuntimeDropUp.d.ts +29 -0
- package/dist/react/features/chat-pane/components/RuntimeDropUp.js +45 -0
- package/dist/react/features/chat-pane/create-daemon-attachment-uploader.js +80 -47
- package/dist/react/features/chat-pane/create-mcp-ui-tool-caller.js +44 -40
- package/dist/react/features/chat-pane/create-mid-run-message-poster.d.ts +6 -6
- package/dist/react/features/chat-pane/create-mid-run-message-poster.js +8 -8
- package/dist/react/features/chat-pane/create-typed-answer-poster.d.ts +3 -3
- package/dist/react/features/chat-pane/create-typed-answer-poster.js +3 -3
- package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.d.ts +6 -1
- package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +11 -5
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +12 -7
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +340 -149
- package/dist/react/features/chat-pane/hooks/useChatPaneControlsHeight.hooks.js +7 -6
- package/dist/react/features/chat-pane/hooks/useChatPaneNewTabLinks.hooks.js +28 -10
- package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.d.ts +3 -3
- package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +60 -12
- package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.d.ts +49 -0
- package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.js +143 -0
- package/dist/react/features/chat-pane/model-options.d.ts +14 -0
- package/dist/react/features/chat-pane/model-options.js +45 -25
- package/dist/react/features/chat-pane/rules.d.ts +8 -9
- package/dist/react/features/chat-pane/rules.js +84 -46
- package/dist/react/features/chat-pane/styles.d.ts +1 -1
- package/dist/react/features/chat-pane/styles.js +152 -36
- package/dist/react/features/chat-pane/types.d.ts +17 -8
- package/dist/react/hooks/assistant-chats.js +12 -5
- package/dist/react/hooks/composer-draft-cache.js +116 -118
- package/dist/react/hooks/composer-history-storage.js +5 -5
- package/dist/react/hooks/fab-position.js +7 -4
- package/dist/react/hooks/useAnsweredCards.d.ts +21 -4
- package/dist/react/hooks/useAnsweredCards.js +38 -11
- package/dist/react/hooks/useArtifactStream.js +28 -22
- package/dist/react/hooks/useComposer.d.ts +20 -0
- package/dist/react/hooks/useComposer.js +22 -9
- package/dist/react/hooks/useComposerHistory.hooks.d.ts +6 -1
- package/dist/react/hooks/useComposerHistory.hooks.js +39 -19
- package/dist/react/hooks/useComposerHistoryKeys.hooks.js +24 -14
- package/dist/react/hooks/useConversation.d.ts +7 -0
- package/dist/react/hooks/useConversation.js +23 -10
- package/dist/react/hooks/useExtEventGroups.js +39 -39
- package/dist/react/hooks/useQuestionForms.js +21 -26
- package/dist/react/hooks/useRunStream.js +10 -2
- package/dist/react/index.d.ts +7 -1
- package/dist/react/index.js +9 -0
- package/dist/react/message-blocks.d.ts +6 -4
- package/dist/react/message-blocks.js +94 -72
- package/dist/react/slots.d.ts +2 -2
- package/dist/react/tool-call-display.js +19 -17
- package/dist/server/durable-runs/recover.js +132 -80
- package/dist/server/run-finalizer/entry.js +25 -15
- package/dist/store/legacy/sqlite/conversations.d.ts +11 -11
- package/dist/store/legacy/sqlite/conversations.js +69 -98
- package/dist/store/legacy/sqlite/messages.d.ts +10 -2
- package/dist/store/legacy/sqlite/messages.js +113 -71
- package/dist/store/runtime/chat-history-store.js +9 -3
- package/dist/store/runtime/durable-run-store.d.ts +2 -2
- package/dist/store/runtime/durable-run-store.js +165 -117
- package/dist/store/runtime/history-tools.js +118 -82
- package/dist/store/runtime/run-ledger.d.ts +4 -1
- package/dist/store/runtime/run-ledger.js +56 -16
- package/dist/store/sql/cursor.js +38 -18
- package/dist/store/sql/store.d.ts +18 -0
- package/dist/store/sql/store.js +46 -15
- package/package.json +12 -10
|
@@ -18,8 +18,8 @@ export declare function listConversations({ db, projectId }: {
|
|
|
18
18
|
}): {
|
|
19
19
|
latestRun: {
|
|
20
20
|
durationMs?: number;
|
|
21
|
-
endedAt?: number
|
|
22
|
-
startedAt?: number
|
|
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
|
|
48
|
-
startedAt?: number
|
|
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
|
|
82
|
-
startedAt?: number
|
|
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
|
|
109
|
-
startedAt?: number
|
|
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
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
|
|
9
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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(${
|
|
46
|
+
SUM(${TERMINAL_RUN_DURATION_SQL}) AS totalDurationMs
|
|
49
47
|
FROM messages m
|
|
50
|
-
JOIN
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
126
|
-
return
|
|
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
|
|
147
|
-
|
|
148
|
-
|
|
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(
|
|
164
|
-
THEN CAST(
|
|
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(
|
|
176
|
-
THEN
|
|
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 (
|
|
159
|
+
if (typeof row.runStatus !== 'string')
|
|
191
160
|
return null;
|
|
192
|
-
const startedAt =
|
|
193
|
-
const endedAt =
|
|
194
|
-
const
|
|
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
|
-
:
|
|
165
|
+
: latestUsageDurationMs(row.eventsJson);
|
|
198
166
|
return {
|
|
199
167
|
status: row.runStatus,
|
|
200
|
-
...(
|
|
201
|
-
...(
|
|
202
|
-
...(
|
|
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
|
|
186
|
+
return [];
|
|
211
187
|
try {
|
|
212
188
|
const events = JSON.parse(eventsJson);
|
|
213
|
-
|
|
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
|
|
192
|
+
return [];
|
|
228
193
|
}
|
|
229
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
}):
|
|
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
|
-
}):
|
|
141
|
+
}): DbRow[] | null;
|
|
134
142
|
/**
|
|
135
143
|
* Hard-deletes a single message row by id.
|
|
136
144
|
* @param db - Open SQLite connection.
|
|
@@ -77,60 +77,92 @@ export function upsertMessage({ db, conversationId, m: incoming }, options = {})
|
|
|
77
77
|
? (options.redactUserText ?? redactUserText)({ text: incoming.content })
|
|
78
78
|
: null;
|
|
79
79
|
const m = redaction ? { ...incoming, content: redaction.text } : incoming;
|
|
80
|
-
const
|
|
81
|
-
const existing = db
|
|
82
|
-
.prepare(`SELECT conversation_id AS conversationId, position FROM messages WHERE id = ?`)
|
|
83
|
-
.get(m.id);
|
|
84
|
-
if (existing && existing.conversationId !== conversationId) {
|
|
85
|
-
throw new MessageConversationMismatchError({ messageId: m.id, actualConversationId: existing.conversationId, requestedConversationId: conversationId });
|
|
86
|
-
}
|
|
87
|
-
const now = Date.now();
|
|
88
|
-
if (existing) {
|
|
89
|
-
db.prepare(`UPDATE messages
|
|
90
|
-
SET role = ?, content = ?, agent_id = ?, agent_name = ?,
|
|
91
|
-
run_id = ?, run_status = ?, last_run_event_id = ?,
|
|
92
|
-
events_json = ?, attachments_json = ?,
|
|
93
|
-
session_mode = ?, run_context_json = ?,
|
|
94
|
-
telemetry_finalized_at = CASE
|
|
95
|
-
WHEN ? THEN COALESCE(telemetry_finalized_at, ?)
|
|
96
|
-
ELSE telemetry_finalized_at
|
|
97
|
-
END,
|
|
98
|
-
started_at = ?, ended_at = ?
|
|
99
|
-
WHERE id = ? AND conversation_id = ?`).run(m.role, m.content, m.agentId ?? null, m.agentName ?? null, m.runId ?? null, m.runStatus ?? null, m.lastRunEventId ?? null, m.events ? JSON.stringify(m.events) : null, m.attachments ? JSON.stringify(m.attachments) : null, normalizeMessageSessionModeForStorage(m.sessionMode), m.runContext ? JSON.stringify(m.runContext) : null, m.telemetryFinalized === true ? 1 : 0, now, m.startedAt ?? null, m.endedAt ?? null, m.id, conversationId);
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
102
|
-
const max = db
|
|
103
|
-
.prepare(`SELECT COALESCE(MAX(position), -1) AS m FROM messages WHERE conversation_id = ?`)
|
|
104
|
-
.get(conversationId);
|
|
105
|
-
const position = (max?.m ?? -1) + 1;
|
|
106
|
-
const createdAt = typeof m.createdAt === 'number' && Number.isFinite(m.createdAt)
|
|
107
|
-
? m.createdAt
|
|
108
|
-
: now;
|
|
109
|
-
// 18 values: id, conversation_id, role, content, agent_id, agent_name,
|
|
110
|
-
// run_id, run_status, last_run_event_id, events_json, attachments_json,
|
|
111
|
-
// session_mode, run_context_json, telemetry_finalized_at, started_at,
|
|
112
|
-
// ended_at, position, created_at.
|
|
113
|
-
db.prepare(`INSERT INTO messages
|
|
114
|
-
(id, conversation_id, role, content, agent_id, agent_name,
|
|
115
|
-
run_id, run_status, last_run_event_id, events_json,
|
|
116
|
-
attachments_json, session_mode, run_context_json,
|
|
117
|
-
telemetry_finalized_at, started_at, ended_at, position, created_at)
|
|
118
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(m.id, conversationId, m.role, m.content, m.agentId ?? null, m.agentName ?? null, m.runId ?? null, m.runStatus ?? null, m.lastRunEventId ?? null, m.events ? JSON.stringify(m.events) : null, m.attachments ? JSON.stringify(m.attachments) : null, normalizeMessageSessionModeForStorage(m.sessionMode), m.runContext ? JSON.stringify(m.runContext) : null, m.telemetryFinalized === true ? now : null, m.startedAt ?? null, m.endedAt ?? null, position, createdAt);
|
|
119
|
-
}
|
|
120
|
-
// Bump conversation activity so recency-sorted listings (e.g. `listConversations`) reflect this write.
|
|
121
|
-
db.prepare(`UPDATE conversations SET updated_at = ? WHERE id = ?`).run(now, conversationId);
|
|
122
|
-
const row = db
|
|
123
|
-
.prepare(`SELECT ${MESSAGE_COLS} FROM messages WHERE id = ? AND conversation_id = ?`)
|
|
124
|
-
.get(m.id, conversationId);
|
|
125
|
-
return row ? normalizeMessage(row) : null;
|
|
126
|
-
});
|
|
127
|
-
const saved = runUpsert.immediate();
|
|
80
|
+
const saved = db.transaction(() => writeMessage({ db, conversationId, m })).immediate();
|
|
128
81
|
if (!saved || !redaction?.secretRedacted)
|
|
129
82
|
return saved;
|
|
130
83
|
const signal = { secretRedacted: true, count: redaction.count };
|
|
131
84
|
options.onSecretRedacted?.(signal);
|
|
132
85
|
return { ...saved, secretRedaction: signal };
|
|
133
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* @internal `upsertMessage`'s transaction body: mismatch check, insert-or-update, conversation
|
|
89
|
+
* `updated_at` bump, and re-fetch. Must run inside the caller's `BEGIN IMMEDIATE` transaction.
|
|
90
|
+
*/
|
|
91
|
+
function writeMessage({ db, conversationId, m }) {
|
|
92
|
+
const existing = db
|
|
93
|
+
.prepare(`SELECT conversation_id AS conversationId FROM messages WHERE id = ?`)
|
|
94
|
+
.get(m.id);
|
|
95
|
+
if (existing && existing.conversationId !== conversationId) {
|
|
96
|
+
throw new MessageConversationMismatchError({ messageId: m.id, actualConversationId: existing.conversationId, requestedConversationId: conversationId });
|
|
97
|
+
}
|
|
98
|
+
const now = Date.now();
|
|
99
|
+
if (existing)
|
|
100
|
+
updateMessageRow({ db, conversationId, m, now });
|
|
101
|
+
else
|
|
102
|
+
insertMessageRow({ db, conversationId, m, now });
|
|
103
|
+
// Bump conversation activity so recency-sorted listings (e.g. `listConversations`) reflect this write.
|
|
104
|
+
db.prepare(`UPDATE conversations SET updated_at = ? WHERE id = ?`).run(now, conversationId);
|
|
105
|
+
const row = db
|
|
106
|
+
.prepare(`SELECT ${MESSAGE_COLS} FROM messages WHERE id = ? AND conversation_id = ?`)
|
|
107
|
+
.get(m.id, conversationId);
|
|
108
|
+
return row ? normalizeMessage(row) : null;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* @internal Values for the columns both write paths set, in this order: role, content, agent_id,
|
|
112
|
+
* agent_name, run_id, run_status, last_run_event_id, events_json, attachments_json, session_mode,
|
|
113
|
+
* run_context_json. JSON sub-fields are stringified; absent values become null.
|
|
114
|
+
*/
|
|
115
|
+
function messageFieldValues(m) {
|
|
116
|
+
return [
|
|
117
|
+
m.role,
|
|
118
|
+
m.content,
|
|
119
|
+
...[m.agentId, m.agentName, m.runId, m.runStatus, m.lastRunEventId].map(orNull),
|
|
120
|
+
jsonOrNull(m.events),
|
|
121
|
+
jsonOrNull(m.attachments),
|
|
122
|
+
normalizeMessageSessionMode(m.sessionMode) ?? null,
|
|
123
|
+
jsonOrNull(m.runContext),
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
/** @internal Maps undefined/null to null (SQLite-safe) and keeps every other value. */
|
|
127
|
+
function orNull(value) {
|
|
128
|
+
return value ?? null;
|
|
129
|
+
}
|
|
130
|
+
/** @internal Stringifies a truthy JSON sub-field; falsy values are stored as null. */
|
|
131
|
+
function jsonOrNull(value) {
|
|
132
|
+
return value ? JSON.stringify(value) : null;
|
|
133
|
+
}
|
|
134
|
+
/** @internal Rewrites an existing row in place; preserves `telemetry_finalized_at` once set (one-way latch). */
|
|
135
|
+
function updateMessageRow({ db, conversationId, m, now }) {
|
|
136
|
+
db.prepare(`UPDATE messages
|
|
137
|
+
SET role = ?, content = ?, agent_id = ?, agent_name = ?,
|
|
138
|
+
run_id = ?, run_status = ?, last_run_event_id = ?,
|
|
139
|
+
events_json = ?, attachments_json = ?,
|
|
140
|
+
session_mode = ?, run_context_json = ?,
|
|
141
|
+
telemetry_finalized_at = CASE
|
|
142
|
+
WHEN ? THEN COALESCE(telemetry_finalized_at, ?)
|
|
143
|
+
ELSE telemetry_finalized_at
|
|
144
|
+
END,
|
|
145
|
+
started_at = ?, ended_at = ?
|
|
146
|
+
WHERE id = ? AND conversation_id = ?`).run(...messageFieldValues(m), m.telemetryFinalized === true ? 1 : 0, now, orNull(m.startedAt), orNull(m.endedAt), m.id, conversationId);
|
|
147
|
+
}
|
|
148
|
+
/** @internal Inserts a new row at the end of the conversation's position sequence. */
|
|
149
|
+
function insertMessageRow({ db, conversationId, m, now }) {
|
|
150
|
+
const max = db
|
|
151
|
+
.prepare(`SELECT COALESCE(MAX(position), -1) AS m FROM messages WHERE conversation_id = ?`)
|
|
152
|
+
.get(conversationId);
|
|
153
|
+
const position = (max?.m ?? -1) + 1;
|
|
154
|
+
const createdAt = Number.isFinite(m.createdAt) ? m.createdAt : now;
|
|
155
|
+
// 18 values: id, conversation_id, role, content, agent_id, agent_name,
|
|
156
|
+
// run_id, run_status, last_run_event_id, events_json, attachments_json,
|
|
157
|
+
// session_mode, run_context_json, telemetry_finalized_at, started_at,
|
|
158
|
+
// ended_at, position, created_at.
|
|
159
|
+
db.prepare(`INSERT INTO messages
|
|
160
|
+
(id, conversation_id, role, content, agent_id, agent_name,
|
|
161
|
+
run_id, run_status, last_run_event_id, events_json,
|
|
162
|
+
attachments_json, session_mode, run_context_json,
|
|
163
|
+
telemetry_finalized_at, started_at, ended_at, position, created_at)
|
|
164
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(m.id, conversationId, ...messageFieldValues(m), m.telemetryFinalized === true ? now : null, orNull(m.startedAt), orNull(m.endedAt), position, createdAt);
|
|
165
|
+
}
|
|
134
166
|
/**
|
|
135
167
|
* Reads whether telemetry has been finalized for a message without loading the full row.
|
|
136
168
|
* Used to decide whether to enqueue a finalization job.
|
|
@@ -162,19 +194,14 @@ export function getMessageTelemetryFinalizationState({ db, messageId }) {
|
|
|
162
194
|
* @returns The updated events array, or null if the message does not exist or label is empty.
|
|
163
195
|
*/
|
|
164
196
|
export function appendMessageStatusEvent({ db, messageId, event }) {
|
|
165
|
-
const label =
|
|
166
|
-
const detail =
|
|
197
|
+
const label = trimmedString(event?.label);
|
|
198
|
+
const detail = trimmedString(event?.detail);
|
|
167
199
|
if (!label)
|
|
168
200
|
return null;
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
.get(messageId);
|
|
172
|
-
if (!row)
|
|
201
|
+
const events = readStoredEvents({ db, messageId });
|
|
202
|
+
if (!events)
|
|
173
203
|
return null;
|
|
174
|
-
|
|
175
|
-
const events = Array.isArray(parsed) ? parsed : [];
|
|
176
|
-
const last = events[events.length - 1];
|
|
177
|
-
if (last?.kind === 'status' && last.label === label && (last.detail ?? '') === detail) {
|
|
204
|
+
if (isRepeatedStatus(events[events.length - 1], { label, detail })) {
|
|
178
205
|
return events;
|
|
179
206
|
}
|
|
180
207
|
const nextEvent = detail
|
|
@@ -185,6 +212,24 @@ export function appendMessageStatusEvent({ db, messageId, event }) {
|
|
|
185
212
|
.run(JSON.stringify(next), messageId);
|
|
186
213
|
return next;
|
|
187
214
|
}
|
|
215
|
+
/** @internal Trimmed string value, or '' for anything that is not a string. */
|
|
216
|
+
function trimmedString(value) {
|
|
217
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
218
|
+
}
|
|
219
|
+
/** @internal True when `last` is a status event with the same label and (absent-as-empty) detail. */
|
|
220
|
+
function isRepeatedStatus(last, { label, detail }) {
|
|
221
|
+
return last?.kind === 'status' && last.label === label && (last.detail ?? '') === detail;
|
|
222
|
+
}
|
|
223
|
+
/** @internal Reads a message's stored events (non-array JSON reads as []), or null when the message does not exist. */
|
|
224
|
+
function readStoredEvents({ db, messageId }) {
|
|
225
|
+
const row = db
|
|
226
|
+
.prepare(`SELECT events_json AS eventsJson FROM messages WHERE id = ?`)
|
|
227
|
+
.get(messageId);
|
|
228
|
+
if (!row)
|
|
229
|
+
return null;
|
|
230
|
+
const parsed = parseJsonOrUndef(row.eventsJson);
|
|
231
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
232
|
+
}
|
|
188
233
|
/**
|
|
189
234
|
* Appends an arbitrary agent event to the message's events array, also streaming `text` deltas
|
|
190
235
|
* into the `content` column so the message text grows incrementally during a run.
|
|
@@ -195,18 +240,12 @@ export function appendMessageStatusEvent({ db, messageId, event }) {
|
|
|
195
240
|
* @returns The updated events array, or null if the message does not exist or kind is empty.
|
|
196
241
|
*/
|
|
197
242
|
export function appendMessageAgentEvent({ db, messageId, event }) {
|
|
198
|
-
|
|
199
|
-
return null;
|
|
200
|
-
const kind = typeof event.kind === 'string' ? event.kind : '';
|
|
243
|
+
const kind = agentEventKind(event);
|
|
201
244
|
if (!kind)
|
|
202
245
|
return null;
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
.get(messageId);
|
|
206
|
-
if (!row)
|
|
246
|
+
const events = readStoredEvents({ db, messageId });
|
|
247
|
+
if (!events)
|
|
207
248
|
return null;
|
|
208
|
-
const parsed = parseJsonOrUndef(row.eventsJson);
|
|
209
|
-
const events = Array.isArray(parsed) ? parsed : [];
|
|
210
249
|
const last = events[events.length - 1];
|
|
211
250
|
if (last && JSON.stringify(last) === JSON.stringify(event)) {
|
|
212
251
|
return events;
|
|
@@ -217,6 +256,13 @@ export function appendMessageAgentEvent({ db, messageId, event }) {
|
|
|
217
256
|
.run(textDelta, JSON.stringify(next), messageId);
|
|
218
257
|
return next;
|
|
219
258
|
}
|
|
259
|
+
/** @internal An agent event's `kind`, or '' when the event is not an object or has no string kind. */
|
|
260
|
+
function agentEventKind(event) {
|
|
261
|
+
if (!event || typeof event !== 'object')
|
|
262
|
+
return '';
|
|
263
|
+
const { kind } = event;
|
|
264
|
+
return typeof kind === 'string' ? kind : '';
|
|
265
|
+
}
|
|
220
266
|
/**
|
|
221
267
|
* Hard-deletes a single message row by id.
|
|
222
268
|
* @param db - Open SQLite connection.
|
|
@@ -249,8 +295,4 @@ function normalizeMessage(row) {
|
|
|
249
295
|
function normalizeMessageSessionMode(value) {
|
|
250
296
|
return value === 'chat' || value === 'design' || value === 'plan' ? value : undefined;
|
|
251
297
|
}
|
|
252
|
-
/** @internal Same validation as normalizeMessageSessionMode but returns null (SQLite-safe) for unrecognised values. */
|
|
253
|
-
function normalizeMessageSessionModeForStorage(value) {
|
|
254
|
-
return value === 'chat' || value === 'design' || value === 'plan' ? value : null;
|
|
255
|
-
}
|
|
256
298
|
//# sourceMappingURL=messages.js.map
|
|
@@ -3,6 +3,13 @@ import { createSqliteChatStore } from "../sqlite/index.js";
|
|
|
3
3
|
import { createPgliteChatStore } from "../pglite/index.js";
|
|
4
4
|
import { createPostgresChatStore } from "../postgres/index.js";
|
|
5
5
|
import { chatKernelTables } from "../sql/boundary.js";
|
|
6
|
+
function isOrphanDaemonAnswer({ previous, answer, isDaemonRunId }, _optional = {}) {
|
|
7
|
+
return previous?.role !== "user" && answer.role === "assistant" && Boolean(answer.runId && isDaemonRunId({ runId: answer.runId }));
|
|
8
|
+
}
|
|
9
|
+
function isEarlierOrEqualQuestion({ question, answer }, _optional = {}) {
|
|
10
|
+
return question.role === "user" && question.createdAt !== undefined && answer.createdAt !== undefined
|
|
11
|
+
&& question.createdAt <= answer.createdAt;
|
|
12
|
+
}
|
|
6
13
|
function repairLegacyTurnOrder({ messages, isDaemonRunId }, _optional = {}) {
|
|
7
14
|
const ordered = [];
|
|
8
15
|
for (let index = 0; index < messages.length; index++) {
|
|
@@ -11,9 +18,8 @@ function repairLegacyTurnOrder({ messages, isDaemonRunId }, _optional = {}) {
|
|
|
11
18
|
// Legacy acceptance saved no question ID. Repair only an orphan daemon answer immediately
|
|
12
19
|
// followed by an earlier/equal-clock user row; a preceding question already owns its answer.
|
|
13
20
|
// A global timestamp sort would reorder valid follow-ups when browser/server clocks disagree.
|
|
14
|
-
if (ordered.at(-1)
|
|
15
|
-
&&
|
|
16
|
-
&& question.createdAt <= answer.createdAt) {
|
|
21
|
+
if (isOrphanDaemonAnswer({ previous: ordered.at(-1), answer, isDaemonRunId }) && question !== undefined
|
|
22
|
+
&& isEarlierOrEqualQuestion({ question, answer })) {
|
|
17
23
|
ordered.push(question, answer);
|
|
18
24
|
index++;
|
|
19
25
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { RunIdPolicy } from "../../core/durable-runs/ports.js";
|
|
2
2
|
import { type AgentEvent } from "../../core/index.js";
|
|
3
3
|
import type { ChatKernel, ChatDatabase } from "./kernel.js";
|
|
4
|
-
import type {
|
|
4
|
+
import type { DurableRunHead, DurableRunStore } from "../../core/durable-runs/ports.js";
|
|
5
5
|
export declare const RUN_SLOT_BUSY = "Another answer in this chat is still running. This turn is queued in your browser.";
|
|
6
6
|
export declare class RunSlotBusyError extends Error {
|
|
7
7
|
constructor();
|
|
8
8
|
}
|
|
9
9
|
export declare function attemptCanExecute({ run }: {
|
|
10
|
-
run:
|
|
10
|
+
run: DurableRunHead | null;
|
|
11
11
|
}, _optional?: {}): boolean;
|
|
12
12
|
export declare function savedRunEvents({ json }: {
|
|
13
13
|
json: string | null;
|