@jini-ai/chat 0.6.15-rc.2 → 0.6.15
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/dist/core/accepted-input.js +20 -11
- package/dist/core/ag-ui/entry.js +3 -1
- package/dist/core/agent-plugin-chat-state.js +3 -1
- package/dist/core/assistant-client/ag-ui-transport.js +1 -1
- package/dist/core/assistant-client/attachment-liveness.js +1 -1
- package/dist/core/assistant-client/durable-run-subscription.js +2 -1
- package/dist/core/assistant-client/transport.js +13 -7
- package/dist/core/assistant-content.js +2 -1
- package/dist/core/composer-history.js +2 -1
- package/dist/core/durable-projection.js +2 -1
- package/dist/core/durable-runs/continuation.js +2 -1
- package/dist/core/messages.d.ts +5 -1
- package/dist/core/partial-json.js +18 -9
- package/dist/core/persistence/title.d.ts +1 -1
- package/dist/core/persistence/title.js +25 -7
- package/dist/core/question-form/normalize.js +1 -1
- package/dist/core/question-form/scan.js +25 -15
- package/dist/core/question-form/secret-answers.js +2 -1
- package/dist/core/run-activity.js +8 -4
- package/dist/core/run-events/entry.js +6 -2
- package/dist/core/transcript.js +13 -6
- package/dist/core/turn-usage.js +7 -2
- package/dist/core/user-text-redaction.d.ts +2 -5
- package/dist/core/user-text-redaction.js +3 -3
- package/dist/core/util/manifest.js +13 -11
- package/dist/core/util/markdown-context.d.ts +14 -0
- package/dist/core/util/markdown-context.js +42 -4
- package/dist/core/util/markup-attributes.js +1 -1
- package/dist/core/util/parser.js +7 -7
- package/dist/core/util/pointer.js +22 -6
- package/dist/core/util/recover.js +5 -8
- package/dist/core/util/strip.js +12 -15
- package/dist/core/util/validate.js +30 -18
- package/dist/embed-widget/browser/lazy-loader.js +16 -12
- package/dist/embed-widget/react/dialog.hooks.js +2 -2
- package/dist/embed-widget/react/embed.js +1 -1
- package/dist/embed-widget/react/header.hooks.js +1 -1
- package/dist/embed-widget/transports/fetch-sse.js +2 -1
- package/dist/embed-widget/vanilla/index.js +19 -12
- package/dist/embed-widget/vanilla/state.js +4 -2
- package/dist/react/components/AttachmentPreviewModal.hooks.js +2 -1
- package/dist/react/components/ChatSettingsDialog.js +18 -9
- package/dist/react/components/Composer.js +5 -4
- package/dist/react/components/ConversationList.js +5 -4
- package/dist/react/components/Markdown.js +29 -26
- package/dist/react/components/McpUiSurfaceCard.js +2 -1
- package/dist/react/components/MessageList.hooks.js +3 -2
- package/dist/react/components/MessageRow.js +23 -7
- package/dist/react/components/MessageRow.usage.js +3 -1
- package/dist/react/components/QuestionForm.js +4 -5
- package/dist/react/components/SentAttachment.js +4 -1
- package/dist/react/components/TodoCard.js +9 -1
- package/dist/react/components/ToolCard.js +8 -7
- package/dist/react/components/composer-discovery.js +8 -8
- package/dist/react/features/chat-pane/components/AgentPluginUsage.js +1 -2
- package/dist/react/features/chat-pane/components/AgentRuntimePicker.d.ts +1 -1
- package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +15 -5
- package/dist/react/features/chat-pane/components/ChatPane.js +4 -2
- package/dist/react/features/chat-pane/components/RuntimeDropUp.js +1 -1
- package/dist/react/features/chat-pane/create-daemon-attachment-uploader.js +1 -1
- package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +2 -2
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +4 -2
- package/dist/react/features/chat-pane/hooks/useChatPaneAgentControl.hooks.js +2 -2
- package/dist/react/features/chat-pane/hooks/useChatPaneControlsHeight.hooks.js +1 -1
- package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +2 -2
- package/dist/react/features/chat-pane/hooks/useChatPaneWorkingDirectory.hooks.js +2 -2
- package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.js +2 -2
- package/dist/react/features/chat-pane/model-options.js +4 -2
- package/dist/react/features/chat-pane/styles.d.ts +1 -1
- package/dist/react/features/chat-pane/styles.js +26 -3
- package/dist/react/features/chat-pane/types.d.ts +8 -0
- package/dist/react/hooks/assistant-chats.js +22 -8
- package/dist/react/hooks/useArtifactStream.js +2 -1
- package/dist/react/hooks/useComposer.js +2 -2
- package/dist/react/hooks/useComposerHistoryKeys.hooks.js +2 -2
- package/dist/react/hooks/useConversation.js +9 -4
- package/dist/react/hooks/useExtEventGroups.js +4 -2
- package/dist/react/hooks/useLatestOperation.js +2 -1
- package/dist/react/hooks/useQuestionForms.js +32 -22
- package/dist/react/hooks/useRunStream.js +4 -2
- package/dist/react/message-blocks.js +4 -2
- package/dist/react/styles/reference.css +9 -0
- package/dist/server/durable-runs/recover.js +13 -5
- package/dist/server/run-finalizer/entry.js +1 -0
- package/dist/store/runtime/chat-history-store.js +9 -3
- package/dist/store/runtime/durable-run-store.js +34 -24
- package/dist/store/runtime/run-ledger.js +7 -4
- package/dist/store/sql/boundary.js +3 -1
- package/dist/store/sql/cursor.js +8 -3
- package/dist/store/sql/store.js +36 -28
- package/package.json +15 -15
|
@@ -52,7 +52,7 @@ export function useConversation(options) {
|
|
|
52
52
|
useEffect(() => {
|
|
53
53
|
const source = options.refreshSignal;
|
|
54
54
|
if (!source || run.isStreaming)
|
|
55
|
-
return;
|
|
55
|
+
return undefined;
|
|
56
56
|
let disposed = false, pending = false, timer;
|
|
57
57
|
const refresh = async () => {
|
|
58
58
|
if (pending)
|
|
@@ -61,7 +61,7 @@ export function useConversation(options) {
|
|
|
61
61
|
try {
|
|
62
62
|
const saved = await source.loadMessages();
|
|
63
63
|
if (!disposed)
|
|
64
|
-
setMessagesState(prev =>
|
|
64
|
+
setMessagesState(prev => mergeSavedMessages({ previous: prev, saved }));
|
|
65
65
|
}
|
|
66
66
|
catch (error) {
|
|
67
67
|
if (!disposed)
|
|
@@ -143,14 +143,14 @@ export function useConversation(options) {
|
|
|
143
143
|
return m;
|
|
144
144
|
// A plain if/else chain (not a nested ternary) — deliberately, so
|
|
145
145
|
// each of the four mapped statuses is its own independently
|
|
146
|
-
// trackable branch under coverage instrumentation. The
|
|
146
|
+
// trackable branch under coverage instrumentation. The fallback
|
|
147
147
|
// value is asserted non-null: this reconciliation effect only ever
|
|
148
148
|
// runs after `sendMessage`/`retry` has called `run.start()`/
|
|
149
149
|
// `reattach` (both set the message's `runStatus: 'queued'` via
|
|
150
150
|
// `activeAssistantIdRef` first), so `m.runStatus` is always defined
|
|
151
151
|
// and `run.status` is never `'idle'` by the time this callback
|
|
152
152
|
// fires — one of the four branches below always reassigns it.
|
|
153
|
-
let runStatus
|
|
153
|
+
let runStatus;
|
|
154
154
|
if (run.status === 'streaming')
|
|
155
155
|
runStatus = 'running';
|
|
156
156
|
else if (run.status === 'done')
|
|
@@ -159,6 +159,8 @@ export function useConversation(options) {
|
|
|
159
159
|
runStatus = 'failed';
|
|
160
160
|
else if (run.status === 'canceled')
|
|
161
161
|
runStatus = 'canceled';
|
|
162
|
+
else
|
|
163
|
+
runStatus = m.runStatus;
|
|
162
164
|
const nextRunId = run.runId ?? m.runId;
|
|
163
165
|
const events = projectAcceptedInputEvents({ events: mergeRunEvents({ saved: m.events, incoming: run.events }, {}) });
|
|
164
166
|
return {
|
|
@@ -274,4 +276,7 @@ export function useConversation(options) {
|
|
|
274
276
|
suppressScroll,
|
|
275
277
|
}), [messages, conversationId, run.isStreaming, run.error, sendRefused, scrollIntent, sendMessage, cancel, retry, setMessages, acknowledgeScroll, suppressScroll]);
|
|
276
278
|
}
|
|
279
|
+
function mergeSavedMessages({ previous, saved }) {
|
|
280
|
+
return [...new Map([...previous, ...saved].map(message => [message.id, message])).values()];
|
|
281
|
+
}
|
|
277
282
|
//# sourceMappingURL=useConversation.js.map
|
|
@@ -26,12 +26,14 @@ function addExtEvent(state, { name, data }) {
|
|
|
26
26
|
function visitEvent(state, ev) {
|
|
27
27
|
if (ev.kind === 'tool_use') {
|
|
28
28
|
state.openCalls.set(ev.id, { name: ev.name, input: ev.input });
|
|
29
|
+
return;
|
|
29
30
|
}
|
|
30
|
-
|
|
31
|
+
if (ev.kind === 'tool_result') {
|
|
31
32
|
state.openCalls.delete(ev.toolUseId);
|
|
32
33
|
state.results.set(ev.toolUseId, { content: ev.content, isError: ev.isError });
|
|
34
|
+
return;
|
|
33
35
|
}
|
|
34
|
-
|
|
36
|
+
if (ev.kind === 'ext') {
|
|
35
37
|
addExtEvent(state, ev);
|
|
36
38
|
}
|
|
37
39
|
}
|
|
@@ -48,7 +48,8 @@ export function useLatestOperation(_requiredArgs) {
|
|
|
48
48
|
generationRef.current += 1;
|
|
49
49
|
}, []);
|
|
50
50
|
const run = useCallback(async ({ body, onError }) => {
|
|
51
|
-
|
|
51
|
+
generationRef.current += 1;
|
|
52
|
+
const generation = generationRef.current;
|
|
52
53
|
const isCurrent = () => mountedRef.current && generation === generationRef.current;
|
|
53
54
|
const token = {
|
|
54
55
|
isCurrent,
|
|
@@ -66,23 +66,17 @@ export function parseSubmittedAnswers({ form, userMessageContent }) {
|
|
|
66
66
|
// `i < lines.length` is the loop condition, so this index is always
|
|
67
67
|
// in-bounds and defined — same reasoning as `header` above.
|
|
68
68
|
const line = lines[i];
|
|
69
|
-
const
|
|
70
|
-
if (!
|
|
69
|
+
const parsed = submittedAnswerLine({ line });
|
|
70
|
+
if (!parsed)
|
|
71
71
|
continue;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
// `id` only ever comes from `labelToId`, which was populated from this
|
|
81
|
-
// same `form.questions` array immediately above, so a question with
|
|
82
|
-
// this exact `id` is always found — the `!` reflects that invariant
|
|
83
|
-
// rather than gaming an untestable defensive branch.
|
|
84
|
-
const question = form.questions.find((x) => x.id === id);
|
|
85
|
-
answers[id] = decodeAnswerValue(question, value);
|
|
72
|
+
const id = labelToId.get(parsed.labelKey);
|
|
73
|
+
if (id) {
|
|
74
|
+
// `id` only ever comes from `labelToId`, populated from this same `form.questions` array,
|
|
75
|
+
// so this exact question exists; the assertion reflects that invariant rather than adding
|
|
76
|
+
// an untestable defensive branch.
|
|
77
|
+
const question = form.questions.find((x) => x.id === id);
|
|
78
|
+
answers[id] = decodeAnswerValue(question, parsed.value);
|
|
79
|
+
}
|
|
86
80
|
}
|
|
87
81
|
return Object.keys(answers).length > 0 ? answers : null;
|
|
88
82
|
}
|
|
@@ -97,12 +91,28 @@ function decodeAnswerValue(question, value) {
|
|
|
97
91
|
return value.toLowerCase() === '(skipped)' ? '' : formOptionValueForLabel({ question: question, labelOrValue: parseSubmittedOptionToken(value) });
|
|
98
92
|
}
|
|
99
93
|
function parseSubmittedOptionToken(raw) {
|
|
100
|
-
const
|
|
101
|
-
if (!
|
|
94
|
+
const trimmed = raw.trimEnd();
|
|
95
|
+
if (!trimmed.endsWith(']'))
|
|
96
|
+
return raw.trim();
|
|
97
|
+
const end = trimmed.length - 1;
|
|
98
|
+
// The value cannot cross an earlier ']'; search the remaining suffix once, from the start
|
|
99
|
+
// of each whitespace run, retaining the first eligible (possibly nested) value marker.
|
|
100
|
+
const base = trimmed.lastIndexOf(']', end - 1) + 1;
|
|
101
|
+
const marker = /(?<!\s)\s+\[value:/i.exec(trimmed.slice(base, end));
|
|
102
|
+
if (!marker)
|
|
102
103
|
return raw.trim();
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
const value = trimmed.slice(base + marker.index + marker[0].length, end);
|
|
105
|
+
return value.length > 0 ? value.trim() : raw.trim();
|
|
106
|
+
}
|
|
107
|
+
function submittedAnswerLine({ line }) {
|
|
108
|
+
const label = /^[-*]([^:]+):/.exec(line);
|
|
109
|
+
if (!label)
|
|
110
|
+
return null;
|
|
111
|
+
const value = /^(.*)$/.exec(line.slice(label[0].length).trimStart());
|
|
112
|
+
if (!value)
|
|
113
|
+
return null;
|
|
114
|
+
// Each capture is required in its own pattern and is defined after a successful match;
|
|
115
|
+
// TypeScript cannot express that invariant for RegExpExecArray's numeric capture slots.
|
|
116
|
+
return { labelKey: label[1].trim().toLowerCase(), value: value[1].trim() };
|
|
107
117
|
}
|
|
108
118
|
//# sourceMappingURL=useQuestionForms.js.map
|
|
@@ -118,7 +118,8 @@ export function useRunStream({ transport }) {
|
|
|
118
118
|
}, []);
|
|
119
119
|
const start = useCallback(async (input) => {
|
|
120
120
|
teardownSubscription();
|
|
121
|
-
|
|
121
|
+
generationRef.current += 1;
|
|
122
|
+
const generation = generationRef.current;
|
|
122
123
|
runIdRef.current = null;
|
|
123
124
|
const subscriptionAbort = new AbortController();
|
|
124
125
|
const cancelAbort = new AbortController();
|
|
@@ -150,7 +151,8 @@ export function useRunStream({ transport }) {
|
|
|
150
151
|
}, [makeHandlers, teardownSubscription, transport]);
|
|
151
152
|
const reattach = useCallback(async (runId, initialEvents = []) => {
|
|
152
153
|
teardownSubscription();
|
|
153
|
-
|
|
154
|
+
generationRef.current += 1;
|
|
155
|
+
const generation = generationRef.current;
|
|
154
156
|
runIdRef.current = runId;
|
|
155
157
|
const subscriptionAbort = new AbortController();
|
|
156
158
|
const cancelAbort = new AbortController();
|
|
@@ -100,9 +100,11 @@ function placeEvent({ state, ev }, { slotOf, includeThinking }) {
|
|
|
100
100
|
// Emitted per boundary, before any slot dedupe below, so every reply gets its own end marker.
|
|
101
101
|
if (isReplyBoundary({ event: ev }))
|
|
102
102
|
placeReplyEnd({ state });
|
|
103
|
-
if (ev.kind === 'ext')
|
|
103
|
+
if (ev.kind === 'ext') {
|
|
104
104
|
placeExt({ state, ev }, { slotOf });
|
|
105
|
-
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (ev.kind === 'tool_use')
|
|
106
108
|
placeToolUse({ state, id: ev.id });
|
|
107
109
|
}
|
|
108
110
|
/**
|
|
@@ -43,6 +43,15 @@
|
|
|
43
43
|
min-width: 0;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/* The picker, plus menu and send share this <div>; flex keeps them on one line, and it wraps
|
|
47
|
+
whole to its own line when the footer runs out of room. */
|
|
48
|
+
.jini-composer-footer-trailing {
|
|
49
|
+
display: flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
min-width: 0;
|
|
52
|
+
margin-left: auto;
|
|
53
|
+
}
|
|
54
|
+
|
|
46
55
|
/* The grouped add menu and slash autocomplete anchor to the composer rather than escaping the
|
|
47
56
|
host's layout. Visual tokens remain the host's responsibility. */
|
|
48
57
|
.jini-composer,
|
|
@@ -2,7 +2,7 @@ import { recoveredRunEvents } from "../../core/index.js";
|
|
|
2
2
|
import { classifyAgentServiceFailure } from "@jini-ai/agent-runtime";
|
|
3
3
|
import { continuationRequest } from "../../core/durable-runs/continuation.js";
|
|
4
4
|
import { isAwaitingAnswer } from "../../core/run-activity.js";
|
|
5
|
-
import { RECOVERY_LIMIT,
|
|
5
|
+
import { RECOVERY_LIMIT, RUN_STALENESS_MS, SAVED_WORK_NOTICE, AWAITING_ANSWER_NOTICE, EXHAUSTED_NOTICE } from "../../core/durable-runs/policies.js";
|
|
6
6
|
export { RECOVERY_LIMIT, RECOVERY_WINDOW_MS, RUN_STALENESS_MS, SAVED_WORK_NOTICE, AWAITING_ANSWER_NOTICE, EXHAUSTED_NOTICE } from "../../core/durable-runs/policies.js";
|
|
7
7
|
// Older saved run error frames hold only their message: the daemon's MCP_BRIDGE_UNAVAILABLE
|
|
8
8
|
// code was dropped, so the bridge failure is still matched by its text.
|
|
@@ -57,10 +57,8 @@ function attemptDiagnostics({ run }, _optional = {}) {
|
|
|
57
57
|
/** Quota/billing and bad requests need human repair. A 429 for rate throttling and a 5xx
|
|
58
58
|
* overload remain retryable; the provider's coarse RATE_LIMITED class also includes billing. */
|
|
59
59
|
function needsHumanRepair({ diagnostics }, _optional = {}) {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
return true;
|
|
63
|
-
return /another answer in this chat is still running|invalid[_ -]?request|context[_ -](length|window)|maximum context|model.*(?:not[_ -]found|does not exist|not supported)|unsupported|does not support|permission denied|forbidden|insufficient[_ -]?(quota|balance|credit|funds)|credit balance is too low|exceeded your current quota|quota[_ -]?exceeded|usage limit|billing|payment required/i.test(diagnostics);
|
|
60
|
+
const hasRejectedStatus = REQUEST_STATUS_PATTERNS.some(pattern => [...diagnostics.matchAll(pattern)].some(match => match[1] !== "408" && match[1] !== "429"));
|
|
61
|
+
return hasRejectedStatus || HUMAN_REPAIR_PATTERNS.some(pattern => pattern.test(diagnostics));
|
|
64
62
|
}
|
|
65
63
|
function attemptFailure({ diagnostics }, _optional = {}) {
|
|
66
64
|
// MCP_BRIDGE_UNAVAILABLE needs bridge repair; another attempt repeats the same failure.
|
|
@@ -282,4 +280,14 @@ export function createDurableRecovery(ports, { logger = console } = {}) {
|
|
|
282
280
|
},
|
|
283
281
|
};
|
|
284
282
|
}
|
|
283
|
+
const REQUEST_STATUS_PATTERNS = [
|
|
284
|
+
/\bhttp(?:[ /]?\d(?:\.\d)?)?[\s:=#-]*(4\d\d)\b/gi,
|
|
285
|
+
/\b(?:status|error|response)(?:[ _-]?code)?[\s:=#-]*(4\d\d)\b/gi,
|
|
286
|
+
/\bcode(?=\s*[:=#])[\s:=#-]*(4\d\d)\b/gi,
|
|
287
|
+
];
|
|
288
|
+
const HUMAN_REPAIR_PATTERNS = [
|
|
289
|
+
/another answer in this chat is still running|invalid[_ -]?request|context[_ -](length|window)|maximum context/i,
|
|
290
|
+
/model.*(?:not[_ -]found|does not exist|not supported)|unsupported|does not support|permission denied|forbidden/i,
|
|
291
|
+
/insufficient[_ -]?(quota|balance|credit|funds)|credit balance is too low|exceeded your current quota|quota[_ -]?exceeded|usage limit|billing|payment required/i,
|
|
292
|
+
];
|
|
285
293
|
//# sourceMappingURL=recover.js.map
|
|
@@ -124,6 +124,7 @@ export function createAssistantRunFinalizer(args, options = {}) {
|
|
|
124
124
|
await scheduler.sleep({ delayMs: reconnectDelayMs });
|
|
125
125
|
}
|
|
126
126
|
// Exhaustion alone is not evidence of a terminal run; the host can reattach or repair at boot.
|
|
127
|
+
return undefined;
|
|
127
128
|
}
|
|
128
129
|
return {
|
|
129
130
|
watch({ principalId, conversationId, message }) {
|
|
@@ -42,9 +42,8 @@ export function createChatHistoryStore({ kernel: hostKernel, scope, isDaemonRunI
|
|
|
42
42
|
const kernel = chatKernelTables({ kernel: hostKernel }, {});
|
|
43
43
|
const required = { kernel, scope };
|
|
44
44
|
const optional = { clock: { nowMs: now } };
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
: createPostgresChatStore(required, optional);
|
|
45
|
+
const createStore = storeFactory(kernel.transport);
|
|
46
|
+
const store = createStore(required, optional);
|
|
48
47
|
return { ...store,
|
|
49
48
|
// Admin reload and recovery use this full-transcript compatibility read. Preserve stored
|
|
50
49
|
// positions (and Jini's position-based page cursors); existing user databases need no migration.
|
|
@@ -53,4 +52,11 @@ export function createChatHistoryStore({ kernel: hostKernel, scope, isDaemonRunI
|
|
|
53
52
|
},
|
|
54
53
|
};
|
|
55
54
|
}
|
|
55
|
+
function storeFactory(transport) {
|
|
56
|
+
if (transport === "better-sqlite3")
|
|
57
|
+
return createSqliteChatStore;
|
|
58
|
+
if (transport === "pglite" || transport === "pglite-socket")
|
|
59
|
+
return createPgliteChatStore;
|
|
60
|
+
return createPostgresChatStore;
|
|
61
|
+
}
|
|
56
62
|
//# sourceMappingURL=chat-history-store.js.map
|
|
@@ -12,12 +12,18 @@ export class RunSlotBusyError extends Error {
|
|
|
12
12
|
constructor() { super(RUN_SLOT_BUSY); }
|
|
13
13
|
}
|
|
14
14
|
function isAcceptedUserShape({ message }, _optional = {}) {
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
if (!message || typeof message.id !== "string" || !message.id)
|
|
16
|
+
return false;
|
|
17
|
+
if (message.role !== "user" || typeof message.content !== "string")
|
|
18
|
+
return false;
|
|
19
|
+
return message.createdAt === undefined || Number.isFinite(message.createdAt);
|
|
17
20
|
}
|
|
18
21
|
function areAcceptedAttachments({ attachments }, _optional = {}) {
|
|
19
|
-
return Array.isArray(attachments) && attachments.every((attachment) =>
|
|
20
|
-
|
|
22
|
+
return Array.isArray(attachments) && attachments.every((attachment) => {
|
|
23
|
+
if (!attachment || typeof attachment.path !== "string" || typeof attachment.name !== "string")
|
|
24
|
+
return false;
|
|
25
|
+
return attachment.kind === "file" || attachment.kind === "image";
|
|
26
|
+
});
|
|
21
27
|
}
|
|
22
28
|
function acceptedUserMessage({ request, now }, _optional = {}) {
|
|
23
29
|
const context = JSON.parse(request.contextRef);
|
|
@@ -247,6 +253,10 @@ export function createDurableRunStore({ kernel: hostKernel, isDaemonRunId }, _op
|
|
|
247
253
|
return load({ messageId: required.messageId }, {});
|
|
248
254
|
});
|
|
249
255
|
}
|
|
256
|
+
async function writeAttemptMetadata({ values, updates }) {
|
|
257
|
+
await kernel.run(db => db.insertInto("assistant_run_attempts").values(values)
|
|
258
|
+
.onConflict(oc => oc.column("message_id").doUpdateSet(updates)).execute());
|
|
259
|
+
}
|
|
250
260
|
async function advance({ run, nextRunId, now, events }, _options = {}) {
|
|
251
261
|
return kernel.transaction(async () => {
|
|
252
262
|
await kernel.lockKey(runLockKey(run));
|
|
@@ -261,17 +271,17 @@ export function createDurableRunStore({ kernel: hostKernel, isDaemonRunId }, _op
|
|
|
261
271
|
.where("run_status", "in", ["queued", "running"]).executeTakeFirst());
|
|
262
272
|
if (Number(result.numUpdatedRows) === 0)
|
|
263
273
|
return false;
|
|
264
|
-
await
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
274
|
+
await writeAttemptMetadata({ values: {
|
|
275
|
+
message_id: run.messageId, engine: run.engine, accepted_json: JSON.stringify(run.request),
|
|
276
|
+
recovery_count: current.recoveryCount + 1, recovery_deadline: current.recoveryDeadline ?? now + RECOVERY_WINDOW_MS,
|
|
277
|
+
recovery_elapsed_ms: 0, attempt_started_at: now,
|
|
278
|
+
last_progress_at: current.lastProgressAt, cancel_reason: null, session_id: null,
|
|
279
|
+
session_confirmed: 0, child_pid: null, child_started_at: null, attempt_base_json: JSON.stringify(projection),
|
|
280
|
+
}, updates: {
|
|
281
|
+
recovery_count: current.recoveryCount + 1, recovery_deadline: current.recoveryDeadline ?? now + RECOVERY_WINDOW_MS,
|
|
282
|
+
attempt_started_at: now,
|
|
283
|
+
session_id: null, session_confirmed: 0, child_pid: null, child_started_at: null, attempt_base_json: JSON.stringify(projection),
|
|
284
|
+
} });
|
|
275
285
|
return true;
|
|
276
286
|
});
|
|
277
287
|
}
|
|
@@ -290,8 +300,7 @@ export function createDurableRunStore({ kernel: hostKernel, isDaemonRunId }, _op
|
|
|
290
300
|
const run = await lockedRun({ runId });
|
|
291
301
|
if (!run)
|
|
292
302
|
return;
|
|
293
|
-
await
|
|
294
|
-
.onConflict((oc) => oc.column("message_id").doUpdateSet({ cancel_reason: reason })).execute());
|
|
303
|
+
await writeAttemptMetadata({ values: { ...metadataForRun({ run }, {}), cancel_reason: reason }, updates: { cancel_reason: reason } });
|
|
295
304
|
});
|
|
296
305
|
}
|
|
297
306
|
async function cancelPending(required, _options = {}) {
|
|
@@ -313,19 +322,21 @@ export function createDurableRunStore({ kernel: hostKernel, isDaemonRunId }, _op
|
|
|
313
322
|
return load({ messageId: required.messageId }, {});
|
|
314
323
|
});
|
|
315
324
|
}
|
|
325
|
+
async function saveAgentSession({ run, sessionId }) {
|
|
326
|
+
await kernel.run((db) => db.insertInto("assistant_agent_sessions").values({ conversation_id: run.conversationId,
|
|
327
|
+
agent_id: run.request.agentId ?? "claude", session_id: sessionId, updated_at: Date.now() })
|
|
328
|
+
.onConflict((oc) => oc.columns(["conversation_id", "agent_id"]).doUpdateSet({ session_id: sessionId, updated_at: Date.now() })).execute());
|
|
329
|
+
}
|
|
316
330
|
async function captureSession({ runId, sessionId, child, confirmed = true }, _options = {}) {
|
|
317
331
|
return kernel.transaction(async () => {
|
|
318
332
|
const run = await lockedRun({ runId });
|
|
319
333
|
if (!run || !attemptCanExecute({ run }, {}))
|
|
320
334
|
return false;
|
|
321
335
|
const fields = { session_id: sessionId, session_confirmed: confirmed ? 1 : 0, ...(child ? { child_pid: child.pid, child_started_at: child.startedAt } : {}) };
|
|
322
|
-
await
|
|
323
|
-
.onConflict((oc) => oc.column("message_id").doUpdateSet(fields)).execute());
|
|
336
|
+
await writeAttemptMetadata({ values: { ...metadataForRun({ run }, {}), ...fields }, updates: fields });
|
|
324
337
|
// Session and generation share the same lock/transaction; an old callback cannot write
|
|
325
338
|
// the conversation's locator in the gap between a successful guard and a later CAS.
|
|
326
|
-
await
|
|
327
|
-
agent_id: run.request.agentId ?? "claude", session_id: sessionId, updated_at: Date.now() })
|
|
328
|
-
.onConflict((oc) => oc.columns(["conversation_id", "agent_id"]).doUpdateSet({ session_id: sessionId, updated_at: Date.now() })).execute());
|
|
339
|
+
await saveAgentSession({ run, sessionId });
|
|
329
340
|
return true;
|
|
330
341
|
});
|
|
331
342
|
}
|
|
@@ -384,8 +395,7 @@ export function createDurableRunStore({ kernel: hostKernel, isDaemonRunId }, _op
|
|
|
384
395
|
elapsed = Math.max(0, RECOVERY_WINDOW_MS - (deadline - now));
|
|
385
396
|
deadline = null;
|
|
386
397
|
}
|
|
387
|
-
await
|
|
388
|
-
.onConflict((oc) => oc.column("message_id").doUpdateSet({ recovery_deadline: deadline, recovery_elapsed_ms: elapsed })).execute());
|
|
398
|
+
await writeAttemptMetadata({ values: { ...row, recovery_deadline: deadline, recovery_elapsed_ms: elapsed }, updates: { recovery_deadline: deadline, recovery_elapsed_ms: elapsed } });
|
|
389
399
|
});
|
|
390
400
|
}
|
|
391
401
|
return { load, find, accept, advance, cancel, cancelPending, captureSession, clearSession, guardTool, completeTool, recoveryClock };
|
|
@@ -131,7 +131,7 @@ export function createChatRunLedger({ kernel: hostKernel, isDaemonRunId }, { now
|
|
|
131
131
|
return kernel.transaction(async () => {
|
|
132
132
|
await kernel.lockKey(runLockKey(settlement));
|
|
133
133
|
const saved = await kernel.run((db) => db.selectFrom("ai_chat_messages").select(["content", "events_json"])
|
|
134
|
-
.where((
|
|
134
|
+
.where(activeRunPredicate({ run: settlement })).executeTakeFirst());
|
|
135
135
|
if (!saved)
|
|
136
136
|
return false;
|
|
137
137
|
const content = settlement.content.length >= saved.content.length ? settlement.content : saved.content;
|
|
@@ -149,7 +149,7 @@ export function createChatRunLedger({ kernel: hostKernel, isDaemonRunId }, { now
|
|
|
149
149
|
run_status: settlement.status,
|
|
150
150
|
ended_at: settlement.endedAt,
|
|
151
151
|
})
|
|
152
|
-
.where((
|
|
152
|
+
.where(activeRunPredicate({ run: settlement }))
|
|
153
153
|
.executeTakeFirst());
|
|
154
154
|
if (Number(result.numUpdatedRows) === 0)
|
|
155
155
|
return false;
|
|
@@ -165,7 +165,7 @@ export function createChatRunLedger({ kernel: hostKernel, isDaemonRunId }, { now
|
|
|
165
165
|
return kernel.transaction(async () => {
|
|
166
166
|
await kernel.lockKey(runLockKey(progress));
|
|
167
167
|
const current = await kernel.run((db) => db.selectFrom("ai_chat_messages").select(["content", "events_json", "run_status"])
|
|
168
|
-
.where((
|
|
168
|
+
.where(activeRunPredicate({ run: progress })).executeTakeFirst());
|
|
169
169
|
if (!current || progress.content.length < current.content.length)
|
|
170
170
|
return false;
|
|
171
171
|
const { eventsJson, runStatus, grown, unchanged } = planCheckpoint({ current, progress });
|
|
@@ -178,7 +178,7 @@ export function createChatRunLedger({ kernel: hostKernel, isDaemonRunId }, { now
|
|
|
178
178
|
const result = await kernel.run((db) => db
|
|
179
179
|
.updateTable("ai_chat_messages")
|
|
180
180
|
.set({ content: progress.content, events_json: eventsJson, run_status: runStatus })
|
|
181
|
-
.where((
|
|
181
|
+
.where(activeRunPredicate({ run: progress }))
|
|
182
182
|
.executeTakeFirst());
|
|
183
183
|
if (Number(result.numUpdatedRows) > 0 && grown) {
|
|
184
184
|
// Progress earns a fresh recovery budget. Replayed prefixes and the coordinator's
|
|
@@ -246,4 +246,7 @@ export function createChatRunLedger({ kernel: hostKernel, isDaemonRunId }, { now
|
|
|
246
246
|
},
|
|
247
247
|
};
|
|
248
248
|
}
|
|
249
|
+
function activeRunPredicate({ run }) {
|
|
250
|
+
return (eb) => eb.and([isRunRow(eb, run), notTerminal(eb)]);
|
|
251
|
+
}
|
|
249
252
|
//# sourceMappingURL=run-ledger.js.map
|
|
@@ -6,7 +6,9 @@ export function validateKernel({ kernel, transports, dialect }) {
|
|
|
6
6
|
}
|
|
7
7
|
/** Copy primitive identity fields so subsequent caller mutation cannot rebind the store. */
|
|
8
8
|
export function copyScope({ scope }) {
|
|
9
|
-
if (!scope || typeof scope.scopeId !== 'string' || !scope.scopeId
|
|
9
|
+
if (!scope || typeof scope.scopeId !== 'string' || !scope.scopeId)
|
|
10
|
+
throw new ChatStoreError({ code: 'invalid-input' });
|
|
11
|
+
if (typeof scope.ownerId !== 'string' || !scope.ownerId || !['user', 'guest'].includes(scope.ownerKind)) {
|
|
10
12
|
throw new ChatStoreError({ code: 'invalid-input' });
|
|
11
13
|
}
|
|
12
14
|
return { scopeId: scope.scopeId, ownerKind: scope.ownerKind, ownerId: scope.ownerId };
|
package/dist/store/sql/cursor.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { trimCharsEnd } from '@jini-ai/core/primitives';
|
|
1
2
|
import { ChatStoreError } from '../errors.js';
|
|
2
3
|
/** Reject bad limits before accessing any storage. */
|
|
3
4
|
export function pageLimit({ options }) {
|
|
@@ -14,7 +15,7 @@ function ownerTuple(scope) {
|
|
|
14
15
|
export function encodeCursor({ kind, scope, conversation, anchor }) {
|
|
15
16
|
const bytes = new TextEncoder().encode(JSON.stringify([1, kind, ownerTuple(scope), conversation, anchor]));
|
|
16
17
|
const binary = Array.from(bytes, byte => String.fromCharCode(byte)).join('');
|
|
17
|
-
return btoa(binary).replaceAll('+', '-').replaceAll('/', '_')
|
|
18
|
+
return trimCharsEnd({ text: btoa(binary).replaceAll('+', '-').replaceAll('/', '_'), chars: '=' });
|
|
18
19
|
}
|
|
19
20
|
/** Every structural failure is the same refusal; callers never learn which check rejected it. */
|
|
20
21
|
function invalidCursor() {
|
|
@@ -38,7 +39,9 @@ function decodeBody({ cursor }) {
|
|
|
38
39
|
}
|
|
39
40
|
/** Version, exact length, read kind and conversation binding. */
|
|
40
41
|
function readEnvelope({ value, kind, conversation }) {
|
|
41
|
-
if (!Array.isArray(value) || value.length !== 5
|
|
42
|
+
if (!Array.isArray(value) || value.length !== 5)
|
|
43
|
+
invalidCursor();
|
|
44
|
+
if (value[0] !== 1 || value[1] !== kind || value[3] !== conversation)
|
|
42
45
|
invalidCursor();
|
|
43
46
|
return value;
|
|
44
47
|
}
|
|
@@ -50,7 +53,9 @@ function assertOwner({ owner, scope }) {
|
|
|
50
53
|
}
|
|
51
54
|
/** A [finite number, string] pair; message positions must also be non-negative safe integers. */
|
|
52
55
|
function readAnchor({ anchor, kind }) {
|
|
53
|
-
if (!Array.isArray(anchor) || anchor.length !== 2
|
|
56
|
+
if (!Array.isArray(anchor) || anchor.length !== 2)
|
|
57
|
+
invalidCursor();
|
|
58
|
+
if (typeof anchor[0] !== 'number' || !Number.isFinite(anchor[0]) || typeof anchor[1] !== 'string') {
|
|
54
59
|
invalidCursor();
|
|
55
60
|
}
|
|
56
61
|
if (kind === 'messages' && (!Number.isSafeInteger(anchor[0]) || anchor[0] < 0))
|
package/dist/store/sql/store.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const MESSAGE_POSITION = 'm.position';
|
|
2
|
+
const CONVERSATION_UPDATED_AT = 'c.updated_at';
|
|
1
3
|
/** One SQL query body upstreamed from the existing host adapter; no resource/schema ownership. */
|
|
2
4
|
// Legacy project/conversation CRUD trusts access to a local database file; this owner-bound store
|
|
3
5
|
// serves shared databases instead. Choose by trust model: borrowing a handle must not grant access
|
|
@@ -96,7 +98,9 @@ function runFields(row) {
|
|
|
96
98
|
}
|
|
97
99
|
/** Reject an unbounded or malformed history read before any query runs. */
|
|
98
100
|
function assertHistoryReadBounds({ limit, fromPosition }) {
|
|
99
|
-
if (!Number.isInteger(limit) || limit < 1 || limit > 100
|
|
101
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100)
|
|
102
|
+
throw new ChatStoreError({ code: 'invalid-input' });
|
|
103
|
+
if (fromPosition !== undefined && (!Number.isSafeInteger(fromPosition) || fromPosition < 0)) {
|
|
100
104
|
throw new ChatStoreError({ code: 'invalid-input' });
|
|
101
105
|
}
|
|
102
106
|
}
|
|
@@ -139,7 +143,7 @@ export function createSqlChatStore({ kernel, scope: inputScope }, { clock = crea
|
|
|
139
143
|
"c.title",
|
|
140
144
|
"c.title_source",
|
|
141
145
|
"c.created_at",
|
|
142
|
-
|
|
146
|
+
CONVERSATION_UPDATED_AT,
|
|
143
147
|
"c.expires_at",
|
|
144
148
|
eb
|
|
145
149
|
.selectFrom("ai_chat_messages as m")
|
|
@@ -182,7 +186,7 @@ export function createSqlChatStore({ kernel, scope: inputScope }, { clock = crea
|
|
|
182
186
|
}
|
|
183
187
|
/** Read owned messages in persisted position order; foreign IDs produce no rows. */
|
|
184
188
|
async function messages({ conversationId }, _optional = {}) {
|
|
185
|
-
const rows = await run(db => ownedMessages(db, conversationId).orderBy(
|
|
189
|
+
const rows = await run(db => ownedMessages(db, conversationId).orderBy(MESSAGE_POSITION).execute());
|
|
186
190
|
return rows.map(toMessage);
|
|
187
191
|
}
|
|
188
192
|
/** Writes and projects one message inside the borrowed transaction; null for absent/foreign IDs. */
|
|
@@ -226,18 +230,7 @@ export function createSqlChatStore({ kernel, scope: inputScope }, { clock = crea
|
|
|
226
230
|
started_at: m.startedAt ?? null,
|
|
227
231
|
ended_at: m.endedAt ?? null,
|
|
228
232
|
})
|
|
229
|
-
.onConflict(
|
|
230
|
-
.column("id")
|
|
231
|
-
.doUpdateSet((eb) => ({
|
|
232
|
-
content: eb.ref("excluded.content"),
|
|
233
|
-
events_json: eb.ref("excluded.events_json"),
|
|
234
|
-
attachments_json: eb.ref("excluded.attachments_json"),
|
|
235
|
-
run_id: eb.ref("excluded.run_id"),
|
|
236
|
-
run_status: eb.ref("excluded.run_status"),
|
|
237
|
-
started_at: eb.ref("excluded.started_at"),
|
|
238
|
-
ended_at: eb.ref("excluded.ended_at"),
|
|
239
|
-
}))
|
|
240
|
-
.where((eb) => eb("ai_chat_messages.conversation_id", "=", eb.ref("excluded.conversation_id"))))
|
|
233
|
+
.onConflict(updateOwnedMessageConflict)
|
|
241
234
|
.execute());
|
|
242
235
|
// A new message is activity: the list orders by `updated_at`.
|
|
243
236
|
await run((db) => db.updateTable("ai_chats").set({ updated_at: clock.nowMs() }).where("id", "=", conversationId).execute());
|
|
@@ -260,7 +253,9 @@ export function createSqlChatStore({ kernel, scope: inputScope }, { clock = crea
|
|
|
260
253
|
* @complexity The database scans owned content for search; only limit rows cross the port.
|
|
261
254
|
*/
|
|
262
255
|
async list(_required, { query, limit = 20 } = {}) {
|
|
263
|
-
if (!Number.isInteger(limit) || limit < 1 || limit > 50
|
|
256
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 50)
|
|
257
|
+
throw new ChatStoreError({ code: 'invalid-input' });
|
|
258
|
+
if (query !== undefined && (typeof query !== 'string' || query.length > 200)) {
|
|
264
259
|
throw new ChatStoreError({ code: 'invalid-input' });
|
|
265
260
|
}
|
|
266
261
|
const rows = await run(db => {
|
|
@@ -277,7 +272,7 @@ export function createSqlChatStore({ kernel, scope: inputScope }, { clock = crea
|
|
|
277
272
|
.where(sql `lower(found.content) like ${pattern} escape '!'`)),
|
|
278
273
|
]));
|
|
279
274
|
}
|
|
280
|
-
return statement.orderBy(
|
|
275
|
+
return statement.orderBy(CONVERSATION_UPDATED_AT, 'desc').orderBy(ordinalId(kernel.dialect, 'c.id')).limit(limit).execute();
|
|
281
276
|
});
|
|
282
277
|
return rows.map(row => ({ ...toConversation(row), firstUserLine: row.first_user_line ?? '' }));
|
|
283
278
|
},
|
|
@@ -286,12 +281,12 @@ export function createSqlChatStore({ kernel, scope: inputScope }, { clock = crea
|
|
|
286
281
|
assertHistoryReadBounds({ limit, fromPosition });
|
|
287
282
|
const rows = await run(db => {
|
|
288
283
|
let statement = ownedMessages(db, chatId).clearSelect().select([
|
|
289
|
-
'm.id', 'm.role', 'm.content',
|
|
284
|
+
'm.id', 'm.role', 'm.content', MESSAGE_POSITION, 'm.created_at',
|
|
290
285
|
]);
|
|
291
286
|
if (fromPosition !== undefined)
|
|
292
|
-
statement = statement.where(
|
|
287
|
+
statement = statement.where(MESSAGE_POSITION, '>=', fromPosition);
|
|
293
288
|
return statement.select(includeToolEvents ? sql `m.events_json`.as('events_json') : sql `null`.as('events_json'))
|
|
294
|
-
.orderBy(
|
|
289
|
+
.orderBy(MESSAGE_POSITION, fromPosition === undefined ? 'desc' : 'asc').limit(limit + 1).execute();
|
|
295
290
|
});
|
|
296
291
|
const page = rows.slice(0, limit);
|
|
297
292
|
if (fromPosition === undefined)
|
|
@@ -300,7 +295,7 @@ export function createSqlChatStore({ kernel, scope: inputScope }, { clock = crea
|
|
|
300
295
|
},
|
|
301
296
|
},
|
|
302
297
|
async list() {
|
|
303
|
-
const rows = await run((db) => ownedConversations(db).orderBy(
|
|
298
|
+
const rows = await run((db) => ownedConversations(db).orderBy(CONVERSATION_UPDATED_AT, "desc").execute());
|
|
304
299
|
return rows.map((row) => toConversation(row));
|
|
305
300
|
},
|
|
306
301
|
get,
|
|
@@ -387,10 +382,10 @@ export function createSqlChatStore({ kernel, scope: inputScope }, { clock = crea
|
|
|
387
382
|
let query = ownedConversations(db);
|
|
388
383
|
if (anchor)
|
|
389
384
|
query = query.where(eb => eb.or([
|
|
390
|
-
eb(
|
|
391
|
-
eb.and([eb(
|
|
385
|
+
eb(CONVERSATION_UPDATED_AT, '<', anchor[0]),
|
|
386
|
+
eb.and([eb(CONVERSATION_UPDATED_AT, '=', anchor[0]), eb(id, '>', anchor[1])]),
|
|
392
387
|
]));
|
|
393
|
-
return query.orderBy(
|
|
388
|
+
return query.orderBy(CONVERSATION_UPDATED_AT, 'desc').orderBy(id, 'asc').limit(limit + 1).execute();
|
|
394
389
|
});
|
|
395
390
|
const items = rows.slice(0, limit).map(row => toConversation(row));
|
|
396
391
|
const last = items.at(-1);
|
|
@@ -404,13 +399,13 @@ export function createSqlChatStore({ kernel, scope: inputScope }, { clock = crea
|
|
|
404
399
|
const id = ordinalId(kernel.dialect, 'm.id');
|
|
405
400
|
// One join, at most limit+1 rows, including the ownership check; no parent pre-read.
|
|
406
401
|
const rows = await run(db => {
|
|
407
|
-
let query = ownedMessages(db, input.conversationId).select(
|
|
402
|
+
let query = ownedMessages(db, input.conversationId).select(MESSAGE_POSITION);
|
|
408
403
|
if (anchor)
|
|
409
404
|
query = query.where(eb => eb.or([
|
|
410
|
-
eb(
|
|
411
|
-
eb.and([eb(
|
|
405
|
+
eb(MESSAGE_POSITION, '>', anchor[0]),
|
|
406
|
+
eb.and([eb(MESSAGE_POSITION, '=', anchor[0]), eb(id, '>', anchor[1])]),
|
|
412
407
|
]));
|
|
413
|
-
return query.orderBy(
|
|
408
|
+
return query.orderBy(MESSAGE_POSITION, 'asc').orderBy(id, 'asc').limit(limit + 1).execute();
|
|
414
409
|
});
|
|
415
410
|
const page = rows.slice(0, limit), last = page.at(-1);
|
|
416
411
|
return { items: page.map(toMessage), ...(rows.length > limit && last ? {
|
|
@@ -435,4 +430,17 @@ export function createSqlChatStore({ kernel, scope: inputScope }, { clock = crea
|
|
|
435
430
|
pageMessages: (required, optional = {}) => atStoreBoundary({ operation: () => store.pageMessages(required, optional) }),
|
|
436
431
|
};
|
|
437
432
|
}
|
|
433
|
+
function updateOwnedMessageConflict(oc) {
|
|
434
|
+
return oc.column("id")
|
|
435
|
+
.doUpdateSet((eb) => ({
|
|
436
|
+
content: eb.ref("excluded.content"),
|
|
437
|
+
events_json: eb.ref("excluded.events_json"),
|
|
438
|
+
attachments_json: eb.ref("excluded.attachments_json"),
|
|
439
|
+
run_id: eb.ref("excluded.run_id"),
|
|
440
|
+
run_status: eb.ref("excluded.run_status"),
|
|
441
|
+
started_at: eb.ref("excluded.started_at"),
|
|
442
|
+
ended_at: eb.ref("excluded.ended_at"),
|
|
443
|
+
}))
|
|
444
|
+
.where((eb) => eb("ai_chat_messages.conversation_id", "=", eb.ref("excluded.conversation_id")));
|
|
445
|
+
}
|
|
438
446
|
//# sourceMappingURL=store.js.map
|