@jini-ai/chat 0.3.11 → 0.5.0
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 +126 -0
- package/README.md +152 -6
- package/dist/core/ag-ui/entry.d.ts +53 -0
- package/dist/core/ag-ui/entry.js +135 -0
- package/dist/core/agentic/chat-capabilities.d.ts +1 -1
- package/dist/core/agentic/index.d.ts +1 -1
- package/dist/core/agentic/index.js +1 -1
- package/dist/core/assistant-content.d.ts +6 -2
- package/dist/core/assistant-content.js +2 -2
- package/dist/core/compact-events.d.ts +3 -1
- package/dist/core/compact-events.js +1 -1
- package/dist/core/events.d.ts +1 -1
- package/dist/core/events.js +1 -1
- package/dist/core/index.d.ts +3 -3
- package/dist/core/index.js +3 -3
- package/dist/core/messages.d.ts +4 -2
- package/dist/core/messages.js +2 -2
- package/dist/core/partial-json.d.ts +6 -2
- package/dist/core/partial-json.js +3 -3
- package/dist/core/persistence/index.d.ts +1 -1
- package/dist/core/persistence/index.js +1 -1
- package/dist/core/persistence/ports.d.ts +2 -123
- package/dist/core/persistence/title.d.ts +3 -1
- package/dist/core/persistence/title.js +1 -1
- package/dist/core/question-form/format.d.ts +16 -7
- package/dist/core/question-form/format.js +3 -3
- package/dist/core/question-form/normalize.d.ts +7 -2
- package/dist/core/question-form/normalize.js +5 -5
- package/dist/core/question-form/scan.d.ts +18 -6
- package/dist/core/question-form/scan.js +14 -14
- package/dist/core/run-activity.d.ts +16 -4
- package/dist/core/run-activity.js +11 -11
- package/dist/core/run-events/entry.d.ts +121 -0
- package/dist/core/run-events/entry.js +290 -0
- package/dist/core/todos.d.ts +20 -8
- package/dist/core/todos.js +11 -11
- package/dist/core/tool-events.d.ts +3 -1
- package/dist/core/tool-events.js +1 -1
- package/dist/core/tool-output.d.ts +3 -1
- package/dist/core/tool-output.js +1 -1
- package/dist/core/tools.d.ts +14 -3
- package/dist/core/tools.js +3 -3
- package/dist/core/transcript.d.ts +13 -5
- package/dist/core/transcript.js +5 -5
- package/dist/core/util/manifest.d.ts +9 -3
- package/dist/core/util/manifest.js +3 -3
- package/dist/core/util/markdown-context.d.ts +11 -3
- package/dist/core/util/markdown-context.js +3 -3
- package/dist/core/util/markup-attributes.d.ts +3 -1
- package/dist/core/util/markup-attributes.js +1 -1
- package/dist/core/util/parser.d.ts +9 -5
- package/dist/core/util/parser.js +13 -13
- package/dist/core/util/recover.d.ts +6 -2
- package/dist/core/util/recover.js +6 -6
- package/dist/core/util/strip.d.ts +19 -5
- package/dist/core/util/strip.js +17 -17
- package/dist/core/util/validate.d.ts +3 -1
- package/dist/core/util/validate.js +1 -1
- package/dist/embed-widget/browser/page-actions.d.ts +121 -0
- package/dist/embed-widget/browser/page-actions.js +113 -0
- package/dist/embed-widget/browser/session-store.d.ts +44 -0
- package/dist/embed-widget/browser/session-store.js +103 -0
- package/dist/embed-widget/react/embed.d.ts +45 -0
- package/dist/embed-widget/react/embed.js +87 -0
- package/dist/embed-widget/react/header.d.ts +21 -0
- package/dist/embed-widget/react/header.js +18 -0
- package/dist/embed-widget/transports/fetch-sse.d.ts +75 -0
- package/dist/embed-widget/transports/fetch-sse.js +175 -0
- package/dist/react/agent-bridge/frontend-session-bridge.d.ts +15 -3
- package/dist/react/agent-bridge/frontend-session-bridge.js +13 -6
- package/dist/react/artifact-types.d.ts +3 -3
- package/dist/react/artifact-types.js +3 -3
- package/dist/react/components/A2uiSurfaceCard.js +34 -13
- package/dist/react/components/AttachmentPreviewModal.hooks.d.ts +7 -2
- package/dist/react/components/AttachmentPreviewModal.hooks.js +5 -5
- package/dist/react/components/AttachmentPreviewModal.js +2 -2
- package/dist/react/components/AttachmentTray.d.ts +3 -1
- package/dist/react/components/AttachmentTray.js +2 -2
- package/dist/react/components/ChatFab.js +1 -1
- package/dist/react/components/Composer.js +9 -9
- package/dist/react/components/ConversationList.js +1 -1
- package/dist/react/components/McpUiSurfaceCard.d.ts +4 -2
- package/dist/react/components/McpUiSurfaceCard.js +5 -5
- package/dist/react/components/MessageList.js +1 -1
- package/dist/react/components/MessageRow.js +9 -9
- package/dist/react/components/QuestionForm.js +5 -5
- package/dist/react/components/ToolCard.js +9 -9
- package/dist/react/components/composer-discovery.d.ts +29 -8
- package/dist/react/components/composer-discovery.js +10 -10
- package/dist/react/ext-event-renderer-registry.d.ts +15 -5
- package/dist/react/ext-event-renderer-registry.js +4 -4
- package/dist/react/features/chat-pane/components/AgentRuntimePicker.d.ts +3 -1
- package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +3 -3
- package/dist/react/features/chat-pane/components/ChatPane.js +43 -43
- package/dist/react/features/chat-pane/create-daemon-attachment-uploader.d.ts +4 -1
- package/dist/react/features/chat-pane/create-daemon-attachment-uploader.js +5 -35
- package/dist/react/features/chat-pane/create-mcp-ui-tool-caller.d.ts +5 -2
- package/dist/react/features/chat-pane/create-mcp-ui-tool-caller.js +1 -1
- package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.d.ts +38 -10
- package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +15 -15
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +31 -31
- package/dist/react/features/chat-pane/hooks/useChatPaneAgentControl.hooks.d.ts +3 -1
- package/dist/react/features/chat-pane/hooks/useChatPaneAgentControl.hooks.js +3 -3
- package/dist/react/features/chat-pane/hooks/useChatPaneComposerPlaceholder.hooks.d.ts +4 -1
- package/dist/react/features/chat-pane/hooks/useChatPaneComposerPlaceholder.hooks.js +1 -1
- package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +30 -30
- package/dist/react/features/chat-pane/hooks/useChatPaneWorkingDirectory.hooks.js +57 -57
- package/dist/react/features/chat-pane/rules.d.ts +13 -4
- package/dist/react/features/chat-pane/rules.js +5 -5
- package/dist/react/features/chat-pane/types.d.ts +1 -1
- package/dist/react/hooks/attachment-preview-cache.d.ts +7 -2
- package/dist/react/hooks/attachment-preview-cache.js +2 -2
- package/dist/react/hooks/composer-draft-cache.d.ts +17 -5
- package/dist/react/hooks/composer-draft-cache.js +6 -6
- package/dist/react/hooks/useArtifactStream.d.ts +5 -1
- package/dist/react/hooks/useArtifactStream.js +3 -3
- package/dist/react/hooks/useChatFabDrag.d.ts +9 -4
- package/dist/react/hooks/useChatFabDrag.js +4 -4
- package/dist/react/hooks/useComposer.js +8 -8
- package/dist/react/hooks/useConversation.js +56 -4
- package/dist/react/hooks/useExtEventGroups.d.ts +5 -1
- package/dist/react/hooks/useExtEventGroups.js +1 -1
- package/dist/react/hooks/useLatestOperation.d.ts +8 -3
- package/dist/react/hooks/useLatestOperation.js +4 -4
- package/dist/react/hooks/usePinnedTodos.d.ts +3 -1
- package/dist/react/hooks/usePinnedTodos.js +3 -3
- package/dist/react/hooks/useQuestionForms.d.ts +7 -2
- package/dist/react/hooks/useQuestionForms.js +7 -7
- package/dist/react/hooks/useRunActivity.d.ts +6 -1
- package/dist/react/hooks/useRunActivity.js +3 -3
- package/dist/react/hooks/useRunStream.d.ts +3 -1
- package/dist/react/hooks/useRunStream.js +1 -1
- package/dist/react/hooks/useToolTimeline.d.ts +3 -1
- package/dist/react/hooks/useToolTimeline.js +3 -3
- package/dist/react/index.d.ts +4 -4
- package/dist/react/index.js +6 -6
- package/dist/react/message-blocks.d.ts +10 -4
- package/dist/react/message-blocks.js +2 -2
- package/dist/react/styles/reference.css +3 -3
- package/dist/react/tool-renderer-registry.d.ts +8 -3
- package/dist/react/tool-renderer-registry.js +3 -3
- package/dist/react/util/defined-props.d.ts +3 -1
- package/dist/react/util/defined-props.js +1 -1
- package/dist/server/run-finalizer/entry.d.ts +47 -0
- package/dist/server/run-finalizer/entry.js +156 -0
- package/dist/server/run-finalizer/ports.d.ts +52 -0
- package/dist/store/contracts/index.d.ts +5 -0
- package/dist/store/contracts/index.js +5 -0
- package/dist/store/errors.d.ts +12 -0
- package/dist/store/errors.js +18 -0
- package/dist/store/index.d.ts +4 -0
- package/dist/store/index.js +2 -0
- package/dist/store/legacy/index.d.ts +3 -0
- package/dist/store/legacy/index.js +2 -0
- package/dist/store/legacy/sqlite/conversations.d.ts +129 -0
- package/dist/store/legacy/sqlite/conversations.js +274 -0
- package/dist/store/legacy/sqlite/index.d.ts +6 -0
- package/dist/store/legacy/sqlite/index.js +5 -0
- package/dist/store/legacy/sqlite/messages.d.ts +156 -0
- package/dist/store/legacy/sqlite/messages.js +246 -0
- package/dist/store/legacy/sqlite/schema.d.ts +3 -0
- package/dist/store/legacy/sqlite/schema.js +40 -0
- package/dist/store/pglite/index.d.ts +21 -0
- package/dist/store/pglite/index.js +17 -0
- package/dist/store/ports.d.ts +121 -0
- package/dist/store/ports.js +2 -0
- package/dist/store/postgres/index.d.ts +21 -0
- package/dist/store/postgres/index.js +17 -0
- package/dist/store/sql/boundary.d.ts +28 -0
- package/dist/store/sql/boundary.js +32 -0
- package/dist/store/sql/cursor.d.ts +24 -0
- package/dist/store/sql/cursor.js +49 -0
- package/dist/store/sql/maintenance.d.ts +9 -0
- package/dist/store/sql/maintenance.js +16 -0
- package/dist/store/sql/store.d.ts +22 -0
- package/dist/store/sql/store.js +340 -0
- package/dist/store/sql/tables.d.ts +34 -0
- package/dist/store/sql/tables.js +2 -0
- package/dist/store/sqlite/index.d.ts +36 -0
- package/dist/store/sqlite/index.js +29 -0
- package/dist/store/sqlite/schema.d.ts +49 -0
- package/dist/store/sqlite/schema.js +80 -0
- package/package.json +211 -14
- package/dist/react/features/model-picker/components/CredentialStatusBadge.d.ts +0 -7
- package/dist/react/features/model-picker/components/CredentialStatusBadge.js +0 -20
- package/dist/react/features/model-picker/components/ModelPicker.d.ts +0 -9
- package/dist/react/features/model-picker/components/ModelPicker.js +0 -34
- package/dist/react/features/model-picker/constants.d.ts +0 -6
- package/dist/react/features/model-picker/constants.js +0 -9
- package/dist/react/features/model-picker/dependencies.d.ts +0 -11
- package/dist/react/features/model-picker/dependencies.js +0 -10
- package/dist/react/features/model-picker/hooks/useModelPicker.hooks.d.ts +0 -27
- package/dist/react/features/model-picker/hooks/useModelPicker.hooks.js +0 -62
- package/dist/react/features/model-picker/index.d.ts +0 -32
- package/dist/react/features/model-picker/index.js +0 -29
- package/dist/react/features/model-picker/ports.d.ts +0 -11
- package/dist/react/features/model-picker/rules.d.ts +0 -26
- package/dist/react/features/model-picker/rules.js +0 -84
- package/dist/react/features/model-picker/types.d.ts +0 -38
- package/dist/react/features/model-picker/types.js +0 -2
- package/dist/react/poc-mcp-ui-official/PocMcpUiCard.d.ts +0 -9
- package/dist/react/poc-mcp-ui-official/PocMcpUiCard.js +0 -29
- package/dist/react/poc-mcp-ui-official/create-poc-resource.d.ts +0 -30
- package/dist/react/poc-mcp-ui-official/create-poc-resource.js +0 -44
- /package/dist/{react/features/model-picker → server/run-finalizer}/ports.js +0 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createSqlChatStore } from '../sql/store.js';
|
|
2
|
+
import { createSqlChatMaintenance } from '../sql/maintenance.js';
|
|
3
|
+
import { validateKernel } from '../sql/boundary.js';
|
|
4
|
+
/** Borrow an existing kernel, with no configuration, migration or connection lifecycle effects.
|
|
5
|
+
* @example createPostgresChatStore({ kernel, scope }, { clock: { nowMs: () => 42 } })
|
|
6
|
+
* @throws ChatStoreError invalid-input for a mismatched dialect/transport or owner scope.
|
|
7
|
+
*/
|
|
8
|
+
export function createPostgresChatStore(required, optional = {}) {
|
|
9
|
+
validateKernel({ kernel: required.kernel, transports: ['node-postgres'], dialect: 'postgres' });
|
|
10
|
+
return createSqlChatStore(required, optional);
|
|
11
|
+
}
|
|
12
|
+
/** Privileged bounded expiry maintenance; never hand this collaborator to request handlers. */
|
|
13
|
+
export function createPostgresChatMaintenance(required) {
|
|
14
|
+
validateKernel({ kernel: required.kernel, transports: ['node-postgres'], dialect: 'postgres' });
|
|
15
|
+
return createSqlChatMaintenance(required);
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** SQL boundary validation and opaque driver error translation. */
|
|
2
|
+
import type { StorageKernel, StorageTransport } from '@jini-ai/db/kernel';
|
|
3
|
+
import type { Kysely } from 'kysely';
|
|
4
|
+
import type { ChatDatabase } from './tables.js';
|
|
5
|
+
import type { ChatOwnerScope } from '../ports.js';
|
|
6
|
+
/** Construction is validation-only: never query, migrate, reconfigure, open or close a kernel. */
|
|
7
|
+
export declare function validateKernel<DB>({ kernel, transports, dialect }: {
|
|
8
|
+
kernel: StorageKernel<DB>;
|
|
9
|
+
transports: readonly StorageTransport[];
|
|
10
|
+
dialect: 'sqlite' | 'postgres';
|
|
11
|
+
}): void;
|
|
12
|
+
/** Copy primitive identity fields so subsequent caller mutation cannot rebind the store. */
|
|
13
|
+
export declare function copyScope({ scope }: {
|
|
14
|
+
scope: ChatOwnerScope;
|
|
15
|
+
}): ChatOwnerScope;
|
|
16
|
+
/** Preserve structured errors and causes; no driver details appear in public messages. */
|
|
17
|
+
export declare function atStoreBoundary<T>({ operation }: {
|
|
18
|
+
operation: () => Promise<T>;
|
|
19
|
+
}): Promise<T>;
|
|
20
|
+
/** The single audited table-subset projection. Required table types are proven by DB's bound;
|
|
21
|
+
* host extra tables and all transaction/locking effects stay on the original kernel object.
|
|
22
|
+
* Kysely's invariant DB parameter requires this one erasure; it never bridges driver/version copies.
|
|
23
|
+
*/
|
|
24
|
+
export declare function runChatQuery<DB extends ChatDatabase, T>({ kernel, body }: {
|
|
25
|
+
kernel: StorageKernel<DB>;
|
|
26
|
+
body: (db: Kysely<ChatDatabase>) => T | Promise<T>;
|
|
27
|
+
}): Promise<T>;
|
|
28
|
+
//# sourceMappingURL=boundary.d.ts.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ChatStoreError } from '../errors.js';
|
|
2
|
+
/** Construction is validation-only: never query, migrate, reconfigure, open or close a kernel. */
|
|
3
|
+
export function validateKernel({ kernel, transports, dialect }) {
|
|
4
|
+
if (!kernel || kernel.dialect !== dialect || !transports.includes(kernel.transport))
|
|
5
|
+
throw new ChatStoreError({ code: 'invalid-input' });
|
|
6
|
+
}
|
|
7
|
+
/** Copy primitive identity fields so subsequent caller mutation cannot rebind the store. */
|
|
8
|
+
export function copyScope({ scope }) {
|
|
9
|
+
if (!scope || typeof scope.scopeId !== 'string' || !scope.scopeId || typeof scope.ownerId !== 'string' || !scope.ownerId || !['user', 'guest'].includes(scope.ownerKind)) {
|
|
10
|
+
throw new ChatStoreError({ code: 'invalid-input' });
|
|
11
|
+
}
|
|
12
|
+
return { scopeId: scope.scopeId, ownerKind: scope.ownerKind, ownerId: scope.ownerId };
|
|
13
|
+
}
|
|
14
|
+
/** Preserve structured errors and causes; no driver details appear in public messages. */
|
|
15
|
+
export async function atStoreBoundary({ operation }) {
|
|
16
|
+
try {
|
|
17
|
+
return await operation();
|
|
18
|
+
}
|
|
19
|
+
catch (cause) {
|
|
20
|
+
if (cause instanceof ChatStoreError)
|
|
21
|
+
throw cause;
|
|
22
|
+
throw new ChatStoreError({ code: 'unavailable' }, { cause });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** The single audited table-subset projection. Required table types are proven by DB's bound;
|
|
26
|
+
* host extra tables and all transaction/locking effects stay on the original kernel object.
|
|
27
|
+
* Kysely's invariant DB parameter requires this one erasure; it never bridges driver/version copies.
|
|
28
|
+
*/
|
|
29
|
+
export function runChatQuery({ kernel, body }) {
|
|
30
|
+
return kernel.run(db => body(db));
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=boundary.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Versioned keyset continuations. Encoding grants no authority; all queries remain scoped. */
|
|
2
|
+
import type { ChatOwnerScope, ChatPageOptions } from '../ports.js';
|
|
3
|
+
type ReadKind = 'conversations' | 'messages';
|
|
4
|
+
export type Anchor = readonly [number, string];
|
|
5
|
+
/** Reject bad limits before accessing any storage. */
|
|
6
|
+
export declare function pageLimit({ options }: {
|
|
7
|
+
options: ChatPageOptions | undefined;
|
|
8
|
+
}): number;
|
|
9
|
+
/** UTF-8/base64url preserves arbitrary opaque IDs; cursors represent live views, not snapshots. */
|
|
10
|
+
export declare function encodeCursor({ kind, scope, conversation, anchor }: {
|
|
11
|
+
kind: ReadKind;
|
|
12
|
+
scope: ChatOwnerScope;
|
|
13
|
+
conversation: string | null;
|
|
14
|
+
anchor: Anchor;
|
|
15
|
+
}): string;
|
|
16
|
+
/** Validate version, exact structure, scope and read bindings before a query can run. */
|
|
17
|
+
export declare function decodeCursor({ cursor, kind, scope, conversation }: {
|
|
18
|
+
cursor: string | undefined;
|
|
19
|
+
kind: ReadKind;
|
|
20
|
+
scope: ChatOwnerScope;
|
|
21
|
+
conversation: string | null;
|
|
22
|
+
}): Anchor | undefined;
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=cursor.d.ts.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { ChatStoreError } from '../errors.js';
|
|
2
|
+
/** Reject bad limits before accessing any storage. */
|
|
3
|
+
export function pageLimit({ options }) {
|
|
4
|
+
const limit = options?.limit === undefined ? 50 : options.limit;
|
|
5
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 200)
|
|
6
|
+
throw new ChatStoreError({ code: 'invalid-input' });
|
|
7
|
+
return limit;
|
|
8
|
+
}
|
|
9
|
+
/** Scope binding uses the host's already-hashed guest ID, never a bearer token. */
|
|
10
|
+
function ownerTuple(scope) {
|
|
11
|
+
return [scope.scopeId, scope.ownerKind, scope.ownerId];
|
|
12
|
+
}
|
|
13
|
+
/** UTF-8/base64url preserves arbitrary opaque IDs; cursors represent live views, not snapshots. */
|
|
14
|
+
export function encodeCursor({ kind, scope, conversation, anchor }) {
|
|
15
|
+
const bytes = new TextEncoder().encode(JSON.stringify([1, kind, ownerTuple(scope), conversation, anchor]));
|
|
16
|
+
const binary = Array.from(bytes, byte => String.fromCharCode(byte)).join('');
|
|
17
|
+
return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
|
|
18
|
+
}
|
|
19
|
+
/** Validate version, exact structure, scope and read bindings before a query can run. */
|
|
20
|
+
export function decodeCursor({ cursor, kind, scope, conversation }) {
|
|
21
|
+
if (cursor === undefined)
|
|
22
|
+
return undefined;
|
|
23
|
+
if (typeof cursor !== 'string' || cursor.length === 0 || cursor.length > 8192 || !/^[A-Za-z0-9_-]+$/.test(cursor)) {
|
|
24
|
+
throw new ChatStoreError({ code: 'invalid-cursor' });
|
|
25
|
+
}
|
|
26
|
+
let value;
|
|
27
|
+
try {
|
|
28
|
+
const binary = atob(cursor.replaceAll('-', '+').replaceAll('_', '/'));
|
|
29
|
+
const bytes = Uint8Array.from(binary, char => char.charCodeAt(0));
|
|
30
|
+
value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
31
|
+
}
|
|
32
|
+
catch (cause) {
|
|
33
|
+
throw new ChatStoreError({ code: 'invalid-cursor' }, { cause });
|
|
34
|
+
}
|
|
35
|
+
if (!Array.isArray(value) || value.length !== 5 || value[0] !== 1 || value[1] !== kind || value[3] !== conversation) {
|
|
36
|
+
throw new ChatStoreError({ code: 'invalid-cursor' });
|
|
37
|
+
}
|
|
38
|
+
const owner = value[2], anchor = value[4];
|
|
39
|
+
if (!Array.isArray(owner) || owner.length !== 3 || owner.some((part, i) => part !== ownerTuple(scope)[i])) {
|
|
40
|
+
throw new ChatStoreError({ code: 'invalid-cursor' });
|
|
41
|
+
}
|
|
42
|
+
if (!Array.isArray(anchor) || anchor.length !== 2 || typeof anchor[0] !== 'number' || !Number.isFinite(anchor[0]) || typeof anchor[1] !== 'string') {
|
|
43
|
+
throw new ChatStoreError({ code: 'invalid-cursor' });
|
|
44
|
+
}
|
|
45
|
+
if (kind === 'messages' && (!Number.isSafeInteger(anchor[0]) || anchor[0] < 0))
|
|
46
|
+
throw new ChatStoreError({ code: 'invalid-cursor' });
|
|
47
|
+
return [anchor[0], anchor[1]];
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=cursor.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Privileged bounded retention, kept separate from every request-scoped store. */
|
|
2
|
+
import type { StorageKernel } from '@jini-ai/db/kernel';
|
|
3
|
+
import type { ChatDatabase } from './tables.js';
|
|
4
|
+
import type { ChatHistoryMaintenance } from '../ports.js';
|
|
5
|
+
/** Delete at most 500 expired parents; the host schema/foreign-key policy supplies cascades. */
|
|
6
|
+
export declare function createSqlChatMaintenance<DB extends ChatDatabase>({ kernel }: {
|
|
7
|
+
kernel: StorageKernel<DB>;
|
|
8
|
+
}): ChatHistoryMaintenance;
|
|
9
|
+
//# sourceMappingURL=maintenance.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ChatStoreError } from '../errors.js';
|
|
2
|
+
import { atStoreBoundary, runChatQuery } from './boundary.js';
|
|
3
|
+
/** Delete at most 500 expired parents; the host schema/foreign-key policy supplies cascades. */
|
|
4
|
+
export function createSqlChatMaintenance({ kernel }) {
|
|
5
|
+
return {
|
|
6
|
+
sweepExpired: ({ now }, { limit = 500 } = {}) => atStoreBoundary({ operation: async () => {
|
|
7
|
+
// Drain a backlog in chunks: an unbounded delete can hold the write lock until concurrent
|
|
8
|
+
// inserts exhaust their busy timeout. NULL expiry remains structurally outside the sweep.
|
|
9
|
+
if (!Number.isFinite(now) || !Number.isInteger(limit) || limit < 1 || limit > 500)
|
|
10
|
+
throw new ChatStoreError({ code: 'invalid-input' });
|
|
11
|
+
const result = await runChatQuery({ kernel: kernel, body: db => db.deleteFrom('ai_chats').where('id', 'in', db.selectFrom('ai_chats').select('id').where('expires_at', 'is not', null).where('expires_at', '<=', now).limit(limit)).executeTakeFirst() });
|
|
12
|
+
return Number(result.numDeletedRows);
|
|
13
|
+
} }),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=maintenance.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type Clock } from '@jini-ai/core/primitives';
|
|
2
|
+
import { type StorageKernel } from '@jini-ai/db/kernel';
|
|
3
|
+
import type { ChatStore, ChatOwnerScope } from '../ports.js';
|
|
4
|
+
import type { ChatDatabase } from './tables.js';
|
|
5
|
+
/**
|
|
6
|
+
* Returns a {@link ChatHistoryStore} that can only ever see `scope`'s own conversations.
|
|
7
|
+
*
|
|
8
|
+
* @param kernel the borrowed kernel. Nothing here opens, closes or configures a database; SQLite's
|
|
9
|
+
* `ON DELETE CASCADE` needs `foreign_keys = ON`, which the host must enable.
|
|
10
|
+
* @param scope the isolation predicate, resolved from the host's authentication. For a guest,
|
|
11
|
+
* `ownerId` must already be a hash of the session key (`tenant-scope.ts`).
|
|
12
|
+
* @param optional Optional core Clock for deterministic host/tests; defaults to system wall time.
|
|
13
|
+
* @complexity each method is one to four indexed statements; `list` is O(owned conversations),
|
|
14
|
+
* `messages` O(messages in the conversation).
|
|
15
|
+
*/
|
|
16
|
+
export declare function createSqlChatStore<DB extends ChatDatabase>({ kernel, scope: inputScope }: {
|
|
17
|
+
kernel: StorageKernel<DB>;
|
|
18
|
+
scope: ChatOwnerScope;
|
|
19
|
+
}, { clock }?: {
|
|
20
|
+
clock?: Clock;
|
|
21
|
+
}): ChatStore;
|
|
22
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/** One SQL query body upstreamed from the existing host adapter; no resource/schema ownership. */
|
|
2
|
+
// Legacy project/conversation CRUD trusts access to a local database file; this owner-bound store
|
|
3
|
+
// serves shared databases instead. Choose by trust model: borrowing a handle must not grant access
|
|
4
|
+
// to every tenant, and this adapter neither chooses that handle's file nor owns its lifetime.
|
|
5
|
+
/**
|
|
6
|
+
* Every read/write uses the authenticated (scope_id, owner_kind, owner_id) tuple; store construction
|
|
7
|
+
* must not turn a shared database into access to other owners' conversations. Append reads and
|
|
8
|
+
* assigns position in one transaction under the conversation lock: PostgreSQL READ COMMITTED alone
|
|
9
|
+
* cannot serialize two appends as SQLite BEGIN IMMEDIATE does. The unique conversation/position
|
|
10
|
+
* constraint remains the backstop, and nested appends join the host's existing transaction.
|
|
11
|
+
*/
|
|
12
|
+
import { sql } from 'kysely';
|
|
13
|
+
import { createSystemClock } from '@jini-ai/core/primitives';
|
|
14
|
+
import { isUniqueViolation } from '@jini-ai/db/kernel';
|
|
15
|
+
import { ChatStoreError } from '../errors.js';
|
|
16
|
+
import { copyScope, atStoreBoundary, runChatQuery } from './boundary.js';
|
|
17
|
+
import { pageLimit, decodeCursor, encodeCursor } from './cursor.js';
|
|
18
|
+
/** UTF-8 byte ordinal ordering, identical on SQLite BINARY and PostgreSQL C collation. */
|
|
19
|
+
function ordinalId(dialect, column) {
|
|
20
|
+
return dialect === 'sqlite' ? sql `${sql.ref(column)} collate BINARY` : sql `${sql.ref(column)} collate "C"`;
|
|
21
|
+
}
|
|
22
|
+
/** The lock every position-assigning append to one conversation takes. */
|
|
23
|
+
function conversationLockKey(conversationId) {
|
|
24
|
+
return `chat-conversation:${conversationId}`;
|
|
25
|
+
}
|
|
26
|
+
function toConversation(row) {
|
|
27
|
+
return {
|
|
28
|
+
id: row.id,
|
|
29
|
+
title: row.title ?? null,
|
|
30
|
+
titleSource: (row.title_source ?? "fallback"),
|
|
31
|
+
messageCount: Number(row.message_count ?? 0),
|
|
32
|
+
createdAt: Number(row.created_at),
|
|
33
|
+
updatedAt: Number(row.updated_at),
|
|
34
|
+
...(row.expires_at === null ? {} : { expiresAt: Number(row.expires_at) }),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A malformed JSON column yields `undefined` rather than throwing: one half-written `events_json`
|
|
39
|
+
* must not make the whole conversation unreadable (same rule as Jini's store).
|
|
40
|
+
*/
|
|
41
|
+
function safeParse(value) {
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(value);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Project only existing ai_* fields: resumable and lastRunEventId belong to run state. */
|
|
50
|
+
// Both fields disappear on a transcript round trip today. Adding columns alone would let the
|
|
51
|
+
// browser persist claims about a run it does not own. First associate the run with its conversation
|
|
52
|
+
// and let the daemon persist authoritative stream/terminal state, then extend storage for it.
|
|
53
|
+
// The legacy messages store does retain last_run_event_id; that is a separate trust model, not
|
|
54
|
+
// evidence that this projection preserves it. A schema extension also needs host-owned follow-up
|
|
55
|
+
// migrations, because changing the DDL constant cannot update databases already migrated.
|
|
56
|
+
function toMessage(row) {
|
|
57
|
+
const events = row.events_json ? safeParse(row.events_json) : undefined;
|
|
58
|
+
const attachments = row.attachments_json ? safeParse(row.attachments_json) : undefined;
|
|
59
|
+
return {
|
|
60
|
+
id: row.id, role: row.role, content: row.content,
|
|
61
|
+
...(row.agent_id ? { agentId: row.agent_id } : {}),
|
|
62
|
+
...(row.agent_name ? { agentName: row.agent_name } : {}),
|
|
63
|
+
...(events === undefined ? {} : { events }),
|
|
64
|
+
...(attachments === undefined ? {} : { attachments }),
|
|
65
|
+
...(row.run_id ? { runId: row.run_id } : {}),
|
|
66
|
+
...(row.run_status ? { runStatus: row.run_status } : {}),
|
|
67
|
+
...(row.created_at === null ? {} : { createdAt: Number(row.created_at) }),
|
|
68
|
+
...(row.started_at === null ? {} : { startedAt: Number(row.started_at) }),
|
|
69
|
+
...(row.ended_at === null ? {} : { endedAt: Number(row.ended_at) }),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Returns a {@link ChatHistoryStore} that can only ever see `scope`'s own conversations.
|
|
74
|
+
*
|
|
75
|
+
* @param kernel the borrowed kernel. Nothing here opens, closes or configures a database; SQLite's
|
|
76
|
+
* `ON DELETE CASCADE` needs `foreign_keys = ON`, which the host must enable.
|
|
77
|
+
* @param scope the isolation predicate, resolved from the host's authentication. For a guest,
|
|
78
|
+
* `ownerId` must already be a hash of the session key (`tenant-scope.ts`).
|
|
79
|
+
* @param optional Optional core Clock for deterministic host/tests; defaults to system wall time.
|
|
80
|
+
* @complexity each method is one to four indexed statements; `list` is O(owned conversations),
|
|
81
|
+
* `messages` O(messages in the conversation).
|
|
82
|
+
*/
|
|
83
|
+
export function createSqlChatStore({ kernel, scope: inputScope }, { clock = createSystemClock() } = {}) {
|
|
84
|
+
const scope = copyScope({ scope: inputScope });
|
|
85
|
+
const { scopeId, ownerKind, ownerId } = scope;
|
|
86
|
+
// The single subset boundary: DB must contain both required table definitions. Extra host tables
|
|
87
|
+
// stay on the exact borrowed kernel; its transaction context is never projected or recreated.
|
|
88
|
+
const run = (body) => runChatQuery({ kernel: kernel, body: body });
|
|
89
|
+
/** This scope's conversations, projected as {@link ConversationRow}s with their message count. */
|
|
90
|
+
function ownedConversations(db) {
|
|
91
|
+
return db
|
|
92
|
+
.selectFrom("ai_chats as c")
|
|
93
|
+
.select((eb) => [
|
|
94
|
+
"c.id",
|
|
95
|
+
"c.title",
|
|
96
|
+
"c.title_source",
|
|
97
|
+
"c.created_at",
|
|
98
|
+
"c.updated_at",
|
|
99
|
+
"c.expires_at",
|
|
100
|
+
eb
|
|
101
|
+
.selectFrom("ai_chat_messages as m")
|
|
102
|
+
.select((sb) => sb.fn.countAll().as("n"))
|
|
103
|
+
.whereRef("m.conversation_id", "=", "c.id")
|
|
104
|
+
.as("message_count"),
|
|
105
|
+
])
|
|
106
|
+
.where("c.scope_id", "=", scopeId)
|
|
107
|
+
.where("c.owner_kind", "=", ownerKind)
|
|
108
|
+
.where("c.owner_id", "=", ownerId);
|
|
109
|
+
}
|
|
110
|
+
/** One owner-filtered indexed lookup; missing and foreign IDs both return null. */
|
|
111
|
+
async function get({ id }, _optional = {}) {
|
|
112
|
+
const row = await run((db) => ownedConversations(db).where("c.id", "=", id).executeTakeFirst());
|
|
113
|
+
return row ? toConversation(row) : null;
|
|
114
|
+
}
|
|
115
|
+
/** One scoped projection for all message reads, including the row returned by an upsert. */
|
|
116
|
+
function ownedMessages(db, conversationId) {
|
|
117
|
+
return db
|
|
118
|
+
.selectFrom("ai_chat_messages as m")
|
|
119
|
+
.innerJoin("ai_chats as c", "c.id", "m.conversation_id")
|
|
120
|
+
.select([
|
|
121
|
+
"m.id",
|
|
122
|
+
"m.role",
|
|
123
|
+
"m.content",
|
|
124
|
+
"m.agent_id",
|
|
125
|
+
"m.agent_name",
|
|
126
|
+
"m.events_json",
|
|
127
|
+
"m.attachments_json",
|
|
128
|
+
"m.run_id",
|
|
129
|
+
"m.run_status",
|
|
130
|
+
"m.created_at",
|
|
131
|
+
"m.started_at",
|
|
132
|
+
"m.ended_at",
|
|
133
|
+
])
|
|
134
|
+
.where("m.conversation_id", "=", conversationId)
|
|
135
|
+
.where("c.scope_id", "=", scopeId)
|
|
136
|
+
.where("c.owner_kind", "=", ownerKind)
|
|
137
|
+
.where("c.owner_id", "=", ownerId);
|
|
138
|
+
}
|
|
139
|
+
/** Read owned messages in persisted position order; foreign IDs produce no rows. */
|
|
140
|
+
async function messages({ conversationId }, _optional = {}) {
|
|
141
|
+
const rows = await run(db => ownedMessages(db, conversationId).orderBy('m.position').execute());
|
|
142
|
+
return rows.map(toMessage);
|
|
143
|
+
}
|
|
144
|
+
/** Writes and projects one message inside the borrowed transaction; null for absent/foreign IDs. */
|
|
145
|
+
function writeMessage(conversationId, m) {
|
|
146
|
+
return kernel.transaction(async () => {
|
|
147
|
+
await kernel.lockKey(conversationLockKey(conversationId));
|
|
148
|
+
const owned = await run((db) => db
|
|
149
|
+
.selectFrom("ai_chats")
|
|
150
|
+
.select("id")
|
|
151
|
+
.where("id", "=", conversationId)
|
|
152
|
+
.where("scope_id", "=", scopeId)
|
|
153
|
+
.where("owner_kind", "=", ownerKind)
|
|
154
|
+
.where("owner_id", "=", ownerId)
|
|
155
|
+
.executeTakeFirst());
|
|
156
|
+
if (!owned)
|
|
157
|
+
return null;
|
|
158
|
+
const existing = await run((db) => db
|
|
159
|
+
.selectFrom("ai_chat_messages")
|
|
160
|
+
.select("position")
|
|
161
|
+
.where("id", "=", m.id)
|
|
162
|
+
.where("conversation_id", "=", conversationId)
|
|
163
|
+
.executeTakeFirst());
|
|
164
|
+
const position = existing ? Number(existing.position) : await nextPosition(conversationId);
|
|
165
|
+
// The conflict target is `id`, the GLOBAL key: the `where` keeps an id that already lives in
|
|
166
|
+
// another conversation from updating THAT row (the contract's "colliding id to its OWN chat" case).
|
|
167
|
+
await run((db) => db
|
|
168
|
+
.insertInto("ai_chat_messages")
|
|
169
|
+
.values({
|
|
170
|
+
id: m.id,
|
|
171
|
+
conversation_id: conversationId,
|
|
172
|
+
role: m.role,
|
|
173
|
+
content: m.content,
|
|
174
|
+
agent_id: m.agentId ?? null,
|
|
175
|
+
agent_name: m.agentName ?? null,
|
|
176
|
+
events_json: m.events ? JSON.stringify(m.events) : null,
|
|
177
|
+
attachments_json: m.attachments ? JSON.stringify(m.attachments) : null,
|
|
178
|
+
run_id: m.runId ?? null,
|
|
179
|
+
run_status: m.runStatus ?? null,
|
|
180
|
+
position,
|
|
181
|
+
created_at: m.createdAt ?? clock.nowMs(),
|
|
182
|
+
started_at: m.startedAt ?? null,
|
|
183
|
+
ended_at: m.endedAt ?? null,
|
|
184
|
+
})
|
|
185
|
+
.onConflict((oc) => oc
|
|
186
|
+
.column("id")
|
|
187
|
+
.doUpdateSet((eb) => ({
|
|
188
|
+
content: eb.ref("excluded.content"),
|
|
189
|
+
events_json: eb.ref("excluded.events_json"),
|
|
190
|
+
attachments_json: eb.ref("excluded.attachments_json"),
|
|
191
|
+
run_id: eb.ref("excluded.run_id"),
|
|
192
|
+
run_status: eb.ref("excluded.run_status"),
|
|
193
|
+
started_at: eb.ref("excluded.started_at"),
|
|
194
|
+
ended_at: eb.ref("excluded.ended_at"),
|
|
195
|
+
}))
|
|
196
|
+
.where((eb) => eb("ai_chat_messages.conversation_id", "=", eb.ref("excluded.conversation_id"))))
|
|
197
|
+
.execute());
|
|
198
|
+
// A new message is activity: the list orders by `updated_at`.
|
|
199
|
+
await run((db) => db.updateTable("ai_chats").set({ updated_at: clock.nowMs() }).where("id", "=", conversationId).execute());
|
|
200
|
+
// Re-read only the written row, while still in the same transaction and conversation lock.
|
|
201
|
+
const saved = await run(db => ownedMessages(db, conversationId).where('m.id', '=', m.id).executeTakeFirst());
|
|
202
|
+
return saved ? toMessage(saved) : null;
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
async function nextPosition(conversationId) {
|
|
206
|
+
const row = await run((db) => db
|
|
207
|
+
.selectFrom("ai_chat_messages")
|
|
208
|
+
.select((eb) => eb.fn.max("position").as("max"))
|
|
209
|
+
.where("conversation_id", "=", conversationId)
|
|
210
|
+
.executeTakeFirst());
|
|
211
|
+
return row?.max === null || row?.max === undefined ? 0 : Number(row.max) + 1;
|
|
212
|
+
}
|
|
213
|
+
const store = {
|
|
214
|
+
async list() {
|
|
215
|
+
const rows = await run((db) => ownedConversations(db).orderBy("c.updated_at", "desc").execute());
|
|
216
|
+
return rows.map((row) => toConversation(row));
|
|
217
|
+
},
|
|
218
|
+
get,
|
|
219
|
+
async create(input, _optional = {}) {
|
|
220
|
+
const ts = clock.nowMs();
|
|
221
|
+
try {
|
|
222
|
+
await run((db) => db
|
|
223
|
+
.insertInto("ai_chats")
|
|
224
|
+
.values({
|
|
225
|
+
id: input.id,
|
|
226
|
+
scope_id: scopeId,
|
|
227
|
+
owner_kind: ownerKind,
|
|
228
|
+
owner_id: ownerId,
|
|
229
|
+
title: input.title ?? null,
|
|
230
|
+
title_source: input.titleSource ?? "fallback",
|
|
231
|
+
created_at: ts,
|
|
232
|
+
updated_at: ts,
|
|
233
|
+
expires_at: input.expiresAt ?? null,
|
|
234
|
+
})
|
|
235
|
+
.execute());
|
|
236
|
+
}
|
|
237
|
+
catch (cause) {
|
|
238
|
+
if (isUniqueViolation(cause))
|
|
239
|
+
throw new ChatStoreError({ code: 'conflict' }, { cause });
|
|
240
|
+
throw cause;
|
|
241
|
+
}
|
|
242
|
+
const created = await get({ id: input.id });
|
|
243
|
+
// Unreachable: the insert used this exact scope. Asserted so a future change to either
|
|
244
|
+
// statement fails loudly instead of returning a malformed conversation.
|
|
245
|
+
if (!created)
|
|
246
|
+
throw new Error(`ai_chats: created conversation ${input.id} was not readable in its own scope`);
|
|
247
|
+
return created;
|
|
248
|
+
},
|
|
249
|
+
async rename({ id, title }, { source = "manual" } = {}) {
|
|
250
|
+
// A generated title never overwrites a typed one: in the WHERE clause, so a concurrent
|
|
251
|
+
// rename cannot slip between a read and this write.
|
|
252
|
+
await run((db) => db
|
|
253
|
+
.updateTable("ai_chats")
|
|
254
|
+
.set({ title, title_source: source, updated_at: clock.nowMs() })
|
|
255
|
+
.where("id", "=", id)
|
|
256
|
+
.where("scope_id", "=", scopeId)
|
|
257
|
+
.where("owner_kind", "=", ownerKind)
|
|
258
|
+
.where("owner_id", "=", ownerId)
|
|
259
|
+
.$if(source === "generated", (qb) => qb.where("title_source", "<>", "manual"))
|
|
260
|
+
.execute());
|
|
261
|
+
return get({ id });
|
|
262
|
+
},
|
|
263
|
+
async touch({ id }, options = {}) {
|
|
264
|
+
const ts = clock.nowMs();
|
|
265
|
+
await run((db) => db
|
|
266
|
+
.updateTable("ai_chats")
|
|
267
|
+
.set(options?.expiresAt === undefined ? { updated_at: ts } : { updated_at: ts, expires_at: options.expiresAt })
|
|
268
|
+
.where("id", "=", id)
|
|
269
|
+
.where("scope_id", "=", scopeId)
|
|
270
|
+
.where("owner_kind", "=", ownerKind)
|
|
271
|
+
.where("owner_id", "=", ownerId)
|
|
272
|
+
.execute());
|
|
273
|
+
},
|
|
274
|
+
async delete({ id }, _optional = {}) {
|
|
275
|
+
// Messages, agent sessions and tool approvals go with it through `ON DELETE CASCADE`.
|
|
276
|
+
await run((db) => db
|
|
277
|
+
.deleteFrom("ai_chats")
|
|
278
|
+
.where("id", "=", id)
|
|
279
|
+
.where("scope_id", "=", scopeId)
|
|
280
|
+
.where("owner_kind", "=", ownerKind)
|
|
281
|
+
.where("owner_id", "=", ownerId)
|
|
282
|
+
.execute());
|
|
283
|
+
},
|
|
284
|
+
messages,
|
|
285
|
+
async appendMessage({ conversationId, message }, _optional = {}) {
|
|
286
|
+
return writeMessage(conversationId, message);
|
|
287
|
+
},
|
|
288
|
+
async pageConversations(_required, options = {}) {
|
|
289
|
+
const limit = pageLimit({ options: options });
|
|
290
|
+
const anchor = decodeCursor({ cursor: options?.cursor, kind: 'conversations', scope: scope, conversation: null });
|
|
291
|
+
const id = ordinalId(kernel.dialect, 'c.id');
|
|
292
|
+
const rows = await run(db => {
|
|
293
|
+
let query = ownedConversations(db);
|
|
294
|
+
if (anchor)
|
|
295
|
+
query = query.where(eb => eb.or([
|
|
296
|
+
eb('c.updated_at', '<', anchor[0]),
|
|
297
|
+
eb.and([eb('c.updated_at', '=', anchor[0]), eb(id, '>', anchor[1])]),
|
|
298
|
+
]));
|
|
299
|
+
return query.orderBy('c.updated_at', 'desc').orderBy(id, 'asc').limit(limit + 1).execute();
|
|
300
|
+
});
|
|
301
|
+
const items = rows.slice(0, limit).map(row => toConversation(row));
|
|
302
|
+
const last = items.at(-1);
|
|
303
|
+
return { items, ...(rows.length > limit && last ? {
|
|
304
|
+
nextCursor: encodeCursor({ kind: 'conversations', scope: scope, conversation: null, anchor: [last.updatedAt, last.id] }),
|
|
305
|
+
} : {}) };
|
|
306
|
+
},
|
|
307
|
+
async pageMessages(input, options = {}) {
|
|
308
|
+
const limit = pageLimit({ options: options });
|
|
309
|
+
const anchor = decodeCursor({ cursor: options?.cursor, kind: 'messages', scope: scope, conversation: input.conversationId });
|
|
310
|
+
const id = ordinalId(kernel.dialect, 'm.id');
|
|
311
|
+
// One join, at most limit+1 rows, including the ownership check; no parent pre-read.
|
|
312
|
+
const rows = await run(db => {
|
|
313
|
+
let query = ownedMessages(db, input.conversationId).select('m.position');
|
|
314
|
+
if (anchor)
|
|
315
|
+
query = query.where(eb => eb.or([
|
|
316
|
+
eb('m.position', '>', anchor[0]),
|
|
317
|
+
eb.and([eb('m.position', '=', anchor[0]), eb(id, '>', anchor[1])]),
|
|
318
|
+
]));
|
|
319
|
+
return query.orderBy('m.position', 'asc').orderBy(id, 'asc').limit(limit + 1).execute();
|
|
320
|
+
});
|
|
321
|
+
const page = rows.slice(0, limit), last = page.at(-1);
|
|
322
|
+
return { items: page.map(toMessage), ...(rows.length > limit && last ? {
|
|
323
|
+
nextCursor: encodeCursor({ kind: 'messages', scope: scope, conversation: input.conversationId, anchor: [Number(last.position), last.id] }),
|
|
324
|
+
} : {}) };
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
return {
|
|
328
|
+
list: () => atStoreBoundary({ operation: () => store.list() }),
|
|
329
|
+
get: (required, optional = {}) => atStoreBoundary({ operation: () => store.get(required, optional) }),
|
|
330
|
+
create: (required, optional = {}) => atStoreBoundary({ operation: () => store.create(required, optional) }),
|
|
331
|
+
rename: (required, optional = {}) => atStoreBoundary({ operation: () => store.rename(required, optional) }),
|
|
332
|
+
touch: (required, optional = {}) => atStoreBoundary({ operation: () => store.touch(required, optional) }),
|
|
333
|
+
delete: (required, optional = {}) => atStoreBoundary({ operation: () => store.delete(required, optional) }),
|
|
334
|
+
messages: (required, optional = {}) => atStoreBoundary({ operation: () => store.messages(required, optional) }),
|
|
335
|
+
appendMessage: (required, optional = {}) => atStoreBoundary({ operation: () => store.appendMessage(required, optional) }),
|
|
336
|
+
pageConversations: (required, optional = {}) => atStoreBoundary({ operation: () => store.pageConversations(required, optional) }),
|
|
337
|
+
pageMessages: (required, optional = {}) => atStoreBoundary({ operation: () => store.pageMessages(required, optional) }),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Existing ai_* schema, owned and migrated by the host. No schema creation on construction. */
|
|
2
|
+
import type { Generated } from 'kysely';
|
|
3
|
+
export type AiChatsTable = {
|
|
4
|
+
id: string;
|
|
5
|
+
scope_id: string;
|
|
6
|
+
owner_kind: string;
|
|
7
|
+
owner_id: string;
|
|
8
|
+
title: string | null;
|
|
9
|
+
title_source: Generated<string>;
|
|
10
|
+
created_at: number;
|
|
11
|
+
updated_at: number;
|
|
12
|
+
expires_at: number | null;
|
|
13
|
+
};
|
|
14
|
+
export type AiChatMessagesTable = {
|
|
15
|
+
id: string;
|
|
16
|
+
conversation_id: string;
|
|
17
|
+
role: string;
|
|
18
|
+
content: string;
|
|
19
|
+
agent_id: string | null;
|
|
20
|
+
agent_name: string | null;
|
|
21
|
+
events_json: string | null;
|
|
22
|
+
attachments_json: string | null;
|
|
23
|
+
run_id: string | null;
|
|
24
|
+
run_status: string | null;
|
|
25
|
+
position: number;
|
|
26
|
+
created_at: number;
|
|
27
|
+
started_at: number | null;
|
|
28
|
+
ended_at: number | null;
|
|
29
|
+
};
|
|
30
|
+
export type ChatDatabase = {
|
|
31
|
+
ai_chats: AiChatsTable;
|
|
32
|
+
ai_chat_messages: AiChatMessagesTable;
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=tables.d.ts.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Sqlite transcript adapter. The host owns the opened, migrated kernel and its lifetime. */
|
|
2
|
+
import type { Clock } from '@jini-ai/core/primitives';
|
|
3
|
+
import type { StorageKernel } from '@jini-ai/db/kernel';
|
|
4
|
+
import type { ChatDatabase } from '../sql/tables.js';
|
|
5
|
+
import type { ChatOwnerScope, ChatStore, ChatHistoryMaintenance } from '../ports.js';
|
|
6
|
+
export type { ChatDatabase, AiChatsTable, AiChatMessagesTable } from '../sql/tables.js';
|
|
7
|
+
/** Borrow an existing kernel, with no configuration, migration or connection lifecycle effects.
|
|
8
|
+
* @example createSqliteChatStore({ kernel, scope }, { clock: { nowMs: () => 42 } })
|
|
9
|
+
* @throws ChatStoreError invalid-input for a mismatched dialect/transport or owner scope.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createSqliteChatStore<DB extends ChatDatabase>(required: {
|
|
12
|
+
kernel: StorageKernel<DB>;
|
|
13
|
+
scope: ChatOwnerScope;
|
|
14
|
+
}, optional?: {
|
|
15
|
+
clock?: Clock;
|
|
16
|
+
}): ChatStore;
|
|
17
|
+
/** Privileged bounded expiry maintenance; never hand this collaborator to request handlers. */
|
|
18
|
+
export declare function createSqliteChatMaintenance<DB extends ChatDatabase>(required: {
|
|
19
|
+
kernel: StorageKernel<DB>;
|
|
20
|
+
}): ChatHistoryMaintenance;
|
|
21
|
+
import { type SqliteConnectionSource } from '@jini-ai/db/kernel/sqlite';
|
|
22
|
+
export { CHAT_HISTORY_DDL, ensureChatHistoryTables } from './schema.js';
|
|
23
|
+
/** Compatibility wrapper over db's memoized kernel; joins the exact raw-handle kernel context.
|
|
24
|
+
* @example createChatHistoryStore({ db, scope }, { clock: { nowMs: () => 42 } })
|
|
25
|
+
*/
|
|
26
|
+
export declare function createChatHistoryStore({ db, scope }: {
|
|
27
|
+
db: SqliteConnectionSource;
|
|
28
|
+
scope: ChatOwnerScope;
|
|
29
|
+
}, optional?: {
|
|
30
|
+
clock?: Clock;
|
|
31
|
+
}): ChatStore;
|
|
32
|
+
/** Compatibility retention wrapper; the caller still owns the handle and its configuration. */
|
|
33
|
+
export declare function createChatHistoryMaintenance({ db }: {
|
|
34
|
+
db: SqliteConnectionSource;
|
|
35
|
+
}): ChatHistoryMaintenance;
|
|
36
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createSqlChatStore } from '../sql/store.js';
|
|
2
|
+
import { createSqlChatMaintenance } from '../sql/maintenance.js';
|
|
3
|
+
import { validateKernel } from '../sql/boundary.js';
|
|
4
|
+
/** Borrow an existing kernel, with no configuration, migration or connection lifecycle effects.
|
|
5
|
+
* @example createSqliteChatStore({ kernel, scope }, { clock: { nowMs: () => 42 } })
|
|
6
|
+
* @throws ChatStoreError invalid-input for a mismatched dialect/transport or owner scope.
|
|
7
|
+
*/
|
|
8
|
+
export function createSqliteChatStore(required, optional = {}) {
|
|
9
|
+
validateKernel({ kernel: required.kernel, transports: ['better-sqlite3'], dialect: 'sqlite' });
|
|
10
|
+
return createSqlChatStore(required, optional);
|
|
11
|
+
}
|
|
12
|
+
/** Privileged bounded expiry maintenance; never hand this collaborator to request handlers. */
|
|
13
|
+
export function createSqliteChatMaintenance(required) {
|
|
14
|
+
validateKernel({ kernel: required.kernel, transports: ['better-sqlite3'], dialect: 'sqlite' });
|
|
15
|
+
return createSqlChatMaintenance(required);
|
|
16
|
+
}
|
|
17
|
+
import { sqliteKernel } from '@jini-ai/db/kernel/sqlite';
|
|
18
|
+
export { CHAT_HISTORY_DDL, ensureChatHistoryTables } from './schema.js';
|
|
19
|
+
/** Compatibility wrapper over db's memoized kernel; joins the exact raw-handle kernel context.
|
|
20
|
+
* @example createChatHistoryStore({ db, scope }, { clock: { nowMs: () => 42 } })
|
|
21
|
+
*/
|
|
22
|
+
export function createChatHistoryStore({ db, scope }, optional = {}) {
|
|
23
|
+
return createSqliteChatStore({ kernel: sqliteKernel(db), scope }, optional);
|
|
24
|
+
}
|
|
25
|
+
/** Compatibility retention wrapper; the caller still owns the handle and its configuration. */
|
|
26
|
+
export function createChatHistoryMaintenance({ db }) {
|
|
27
|
+
return createSqliteChatMaintenance({ kernel: sqliteKernel(db) });
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=index.js.map
|