@jini-ai/sqlite 0.1.2 → 0.3.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/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # `@jini-ai/sqlite`
2
+
3
+ The durable SQLite adapter layer for a Jini host. Its headline export is `createSqliteEventLog`, a
4
+ real on-disk implementation of `@jini-ai/protocol`'s `EventLog` port (the dedupe-aware,
5
+ gap-detecting run-event log that `@jini-ai/daemon` also implements in memory) — swap one for the
6
+ other and runs survive a process restart. Alongside it sits the application schema and typed query
7
+ helpers a chat/agent host needs: projects, conversations, messages, agent sessions, a searchable
8
+ tool catalog, plus migration and integrity-inspection utilities. This package depends only on
9
+ `@jini-ai/protocol` and `better-sqlite3`; it does **not** depend on `@jini-ai/daemon`.
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ npm install @jini-ai/sqlite
15
+ ```
16
+
17
+ `better-sqlite3` is a regular dependency (not an optional peer) because opening the database is this
18
+ package's entire job. It is a **native compiled addon**, so it needs a prebuild matching the exact
19
+ Node ABI it runs under — inside an Electron/Tauri shell that means an `electron-rebuild` step
20
+ whenever the shell's bundled Node/Electron version changes, not just a plain install.
21
+
22
+ ## What you get
23
+
24
+ **Durable event log** — `createSqliteEventLog(dbPath, { maxEntriesPerRun })` returning a
25
+ `SqliteEventLog` (an `EventLog` plus its own close/lifecycle surface), and
26
+ `DEFAULT_MAX_ENTRIES_PER_RUN` (2000). Unlike the in-memory reference log, a durable store defaults
27
+ to a *bounded* retention window per run, because unbounded on-disk growth is a real unattended
28
+ operational risk; pass an explicit cap (larger, smaller, or `0`) to own that tradeoff yourself.
29
+
30
+ **Connection and migration** — `openDatabase(projectRoot, { dataDir })` (opens/creates
31
+ `<dataDir ?? projectRoot/.jini>/app.sqlite` and memoizes the handle), `closeDatabase()`, and
32
+ `migrate(db)`. Core types: `SqliteDb`, `DbRow`, `JsonObject`, `ChatSessionMode`.
33
+
34
+ **Backend selection** — `resolveSqliteBackendConfig(env)` → `SqliteBackendConfig`
35
+ (`kind: 'sqlite' | 'postgres'` plus Postgres resolution metadata a future adapter will read, with
36
+ secrets deliberately never read through env at this layer), and `SqliteBackendConfigError`.
37
+
38
+ **Application queries** — plain functions over a `SqliteDb`, grouped by table:
39
+ - projects: `listProjects`, `getProject`, `insertProject`, `updateProject`, `deleteProject`, and the
40
+ run-status rollups (`listLatestRunStatuses`, `listLatestProjectRunStatuses`,
41
+ `listLatestConversationRunStatuses`, `listFirstConversationRunStatuses`,
42
+ `listProjectsAwaitingInput`, `listConversationsAwaitingInput`)
43
+ - conversations: `listConversations`, `getConversation`, `insertConversation`, `updateConversation`,
44
+ `deleteConversation`, `normalizeConversationSessionMode`
45
+ - messages: `listMessages`, `upsertMessage`, `deleteMessage`, `appendMessageStatusEvent`,
46
+ `appendMessageAgentEvent`, `getMessageTelemetryFinalizationState`, and
47
+ `MessageConversationMismatchError`
48
+ - agent sessions: `getAgentSession`, `getAgentSessionRecord`, `upsertAgentSession`,
49
+ `clearAgentSession`, `updateAgentSessionStableHash`, `latestCompletedAssistantMessageId`
50
+
51
+ **Tool catalog** — `ensureToolCatalogTables`, `reseedToolCatalog`, `getToolCatalogEntry`,
52
+ `searchToolCatalog` (FTS-backed), with `ToolCatalogEntry` / `ToolCatalogSearchHit`.
53
+
54
+ **Inspection** — `inspectSqliteDatabase({ db, file })` → `DaemonDbStatusReport` (table/row
55
+ inventory) and `verifySqliteIntegrity(opts)` → `DbIntegrityReport` (`PRAGMA integrity_check` plus
56
+ foreign-key violations).
57
+
58
+ **Row utilities** — `row`, `rows`, `parseJsonOrUndef`.
59
+
60
+ ## Usage
61
+
62
+ ```ts
63
+ import {
64
+ createSqliteEventLog,
65
+ openDatabase,
66
+ migrate,
67
+ listProjects,
68
+ searchToolCatalog,
69
+ verifySqliteIntegrity,
70
+ } from '@jini-ai/sqlite';
71
+ import type { EventLog } from '@jini-ai/protocol';
72
+
73
+ // The durable EventLog port — drop-in for @jini-ai/daemon's in-memory log.
74
+ const eventLog: EventLog = createSqliteEventLog('/var/lib/example/events.sqlite', {
75
+ maxEntriesPerRun: 5000,
76
+ });
77
+
78
+ await eventLog.append({ runId: 'run_123', event: 'stdout', data: { text: 'hello' } });
79
+ const replay = await eventLog.replay('run_123', null);
80
+ if (replay.kind === 'replay-gap') {
81
+ // The requested cursor was already evicted — resync rather than trusting a hole.
82
+ }
83
+
84
+ // The application database.
85
+ const db = openDatabase('/srv/workspace');
86
+ migrate(db);
87
+
88
+ const projects = listProjects(db);
89
+ const hits = searchToolCatalog(db, 'deploy', 5);
90
+ const integrity = verifySqliteIntegrity({ db });
91
+ ```
92
+
93
+ ## What's swappable
94
+
95
+ This package *is* an adapter — it implements `@jini-ai/protocol`'s `EventLog` so a host can bind
96
+ either it or an in-memory log behind the same token. Going the other way, very little inside is
97
+ injectable: `createSqliteEventLog` takes only a path plus the retention cap, and the query helpers
98
+ take an already-open `SqliteDb` handle (so you control connection lifetime, and can pass a
99
+ `:memory:` database in tests) but the SQL and schema themselves are fixed.
100
+ `resolveSqliteBackendConfig` reads a caller-supplied `env` record rather than `process.env` directly.
101
+
102
+ ## Runtime
103
+
104
+ `jini.runtime: "node"` — `better-sqlite3` plus `node:fs`/`node:path`.
105
+ ESM only — ships `"type": "module"` with no CommonJS `require` build.
106
+
107
+ ## Provenance
108
+
109
+ See [source-map.md](./source-map.md) for per-file provenance and scope decisions. Apache-2.0,
110
+ inherited from Open Design — see the repo `NOTICE`.
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @module db/chat-history
3
+ *
4
+ * Durable, owner-scoped chat history — the SQLite side of `@jini-ai/chat/core`'s
5
+ * `ChatHistoryStore`. Takes an injected handle and never opens a database of its own, so a host
6
+ * with its own file (and its own migration tooling) can use it without adopting this package's
7
+ * `app.sqlite`.
8
+ *
9
+ * Distinct from the older `db/conversations` module, which is `project_id`-scoped with no owner
10
+ * predicate at all — correct for a single-user local app, unsafe for a shared one. See
11
+ * `store.ts`'s header.
12
+ */
13
+ export * from './schema.js';
14
+ export * from './store.js';
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/db/chat-history/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @module db/chat-history
3
+ *
4
+ * Durable, owner-scoped chat history — the SQLite side of `@jini-ai/chat/core`'s
5
+ * `ChatHistoryStore`. Takes an injected handle and never opens a database of its own, so a host
6
+ * with its own file (and its own migration tooling) can use it without adopting this package's
7
+ * `app.sqlite`.
8
+ *
9
+ * Distinct from the older `db/conversations` module, which is `project_id`-scoped with no owner
10
+ * predicate at all — correct for a single-user local app, unsafe for a shared one. See
11
+ * `store.ts`'s header.
12
+ */
13
+ export * from './schema.js';
14
+ export * from './store.js';
15
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/db/chat-history/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @module db/chat-history/schema
3
+ *
4
+ * DDL for durable chat history (`ai_chats` + `ai_chat_messages`), exported as a constant so a
5
+ * host that owns its own migration tooling applies the *same* statements this package's
6
+ * {@link ensureChatHistoryTables} would — the schema is defined once and cannot drift between
7
+ * the two paths.
8
+ *
9
+ * Deliberately NOT wired into `schema/migrate.ts`, following `tool-catalog.ts`'s precedent and
10
+ * for its stated reason: `migrate()` opens and owns `<dataDir>/app.sqlite`, so anything it
11
+ * creates lands in *this package's* database. A host with its own database (the reference
12
+ * implementation's `content.db`)
13
+ * must not have a second migrator writing DDL into it behind its backup and snapshot tooling.
14
+ * Such a host copies {@link CHAT_HISTORY_DDL} into its own numbered migration and never calls
15
+ * `ensureChatHistoryTables`; a host with no migration system of its own calls the function.
16
+ * Both end up at the same schema.
17
+ *
18
+ * Two tables, not one. `session_key_hash` is a bearer credential and must not repeat on every
19
+ * message row; retention becomes one cascading `DELETE`; an empty conversation has to be
20
+ * representable so "New chat" can exist before the first message; and a list view needs a row
21
+ * to list without touching message storage at all.
22
+ */
23
+ import type { SqliteDb } from '../core/types.js';
24
+ /**
25
+ * The canonical chat-history schema.
26
+ *
27
+ * Notes on the parts that are easy to get wrong:
28
+ *
29
+ * - **`scope_id`** is the host's partition key (workspace/tenant/project). It is `NOT NULL` from
30
+ * the first migration on purpose: adding it later is trivial, but *backfilling* it is not —
31
+ * pre-existing rows carry no evidence of which partition they belonged to, so a late addition
32
+ * forces a choice between deleting history and guessing.
33
+ * - **`owner_kind` + the CHECK pair** make "a guest row with a `user_id`" unrepresentable rather
34
+ * than merely discouraged.
35
+ * - **`owner_id`** holds a user id for `user`, and a *hash* of the session key for `guest`. The
36
+ * raw token never reaches this table.
37
+ * - **`title_source`** exists so an agent-generated title cannot clobber a manual rename. That
38
+ * rule is enforced in the store, but it needs a column to be enforceable at all.
39
+ * - **`position`** orders messages, not `created_at`: two inserts inside the same millisecond
40
+ * are ordinary under concurrent writers, and a tie there corrupts replay order permanently.
41
+ * `UNIQUE (conversation_id, position)` turns a position-assignment bug into a loud constraint
42
+ * violation instead of a silently scrambled transcript.
43
+ * - **`expires_at NULL`** means "never expires". The retention sweep filters on
44
+ * `expires_at IS NOT NULL`, so authenticated history is out of its reach by construction, not
45
+ * by a predicate that could drift.
46
+ */
47
+ export declare const CHAT_HISTORY_DDL = "\nCREATE TABLE IF NOT EXISTS ai_chats (\n id TEXT PRIMARY KEY,\n scope_id TEXT NOT NULL,\n owner_kind TEXT NOT NULL CHECK (owner_kind IN ('user','guest')),\n owner_id TEXT NOT NULL,\n title TEXT,\n title_source TEXT NOT NULL DEFAULT 'fallback'\n CHECK (title_source IN ('fallback','generated','manual')),\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL,\n expires_at INTEGER\n);\n\nCREATE INDEX IF NOT EXISTS idx_ai_chats_owner\n ON ai_chats(scope_id, owner_kind, owner_id, updated_at DESC);\n\nCREATE INDEX IF NOT EXISTS idx_ai_chats_expiry\n ON ai_chats(expires_at) WHERE expires_at IS NOT NULL;\n\nCREATE TABLE IF NOT EXISTS ai_chat_messages (\n id TEXT PRIMARY KEY,\n conversation_id TEXT NOT NULL REFERENCES ai_chats(id) ON DELETE CASCADE,\n role TEXT NOT NULL CHECK (role IN ('user','assistant')),\n content TEXT NOT NULL,\n agent_id TEXT,\n agent_name TEXT,\n events_json TEXT,\n attachments_json TEXT,\n run_id TEXT,\n run_status TEXT,\n position INTEGER NOT NULL,\n created_at INTEGER NOT NULL,\n started_at INTEGER,\n ended_at INTEGER,\n UNIQUE (conversation_id, position)\n);\n\nCREATE INDEX IF NOT EXISTS idx_ai_chat_messages_order\n ON ai_chat_messages(conversation_id, position);\n";
48
+ /**
49
+ * Creates the chat-history tables in the given database if they are absent.
50
+ *
51
+ * Idempotent, matching `ensureToolCatalogTables`'s convention. For a host whose database is
52
+ * managed by its own migration system, prefer copying {@link CHAT_HISTORY_DDL} into a numbered
53
+ * migration instead — see this module's header for why two migrators over one file is a hazard.
54
+ *
55
+ * The caller is responsible for `PRAGMA foreign_keys = ON`; without it the `ON DELETE CASCADE`
56
+ * above is silently inert and deleting a conversation orphans its messages. This function does
57
+ * not set it, because pragmas are connection-wide and a library has no business reconfiguring a
58
+ * handle it was merely lent.
59
+ */
60
+ export declare function ensureChatHistoryTables(db: SqliteDb): void;
61
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../src/db/chat-history/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEjD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,gBAAgB,u2CAwC5B,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,CAE1D"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The canonical chat-history schema.
3
+ *
4
+ * Notes on the parts that are easy to get wrong:
5
+ *
6
+ * - **`scope_id`** is the host's partition key (workspace/tenant/project). It is `NOT NULL` from
7
+ * the first migration on purpose: adding it later is trivial, but *backfilling* it is not —
8
+ * pre-existing rows carry no evidence of which partition they belonged to, so a late addition
9
+ * forces a choice between deleting history and guessing.
10
+ * - **`owner_kind` + the CHECK pair** make "a guest row with a `user_id`" unrepresentable rather
11
+ * than merely discouraged.
12
+ * - **`owner_id`** holds a user id for `user`, and a *hash* of the session key for `guest`. The
13
+ * raw token never reaches this table.
14
+ * - **`title_source`** exists so an agent-generated title cannot clobber a manual rename. That
15
+ * rule is enforced in the store, but it needs a column to be enforceable at all.
16
+ * - **`position`** orders messages, not `created_at`: two inserts inside the same millisecond
17
+ * are ordinary under concurrent writers, and a tie there corrupts replay order permanently.
18
+ * `UNIQUE (conversation_id, position)` turns a position-assignment bug into a loud constraint
19
+ * violation instead of a silently scrambled transcript.
20
+ * - **`expires_at NULL`** means "never expires". The retention sweep filters on
21
+ * `expires_at IS NOT NULL`, so authenticated history is out of its reach by construction, not
22
+ * by a predicate that could drift.
23
+ */
24
+ export const CHAT_HISTORY_DDL = `
25
+ CREATE TABLE IF NOT EXISTS ai_chats (
26
+ id TEXT PRIMARY KEY,
27
+ scope_id TEXT NOT NULL,
28
+ owner_kind TEXT NOT NULL CHECK (owner_kind IN ('user','guest')),
29
+ owner_id TEXT NOT NULL,
30
+ title TEXT,
31
+ title_source TEXT NOT NULL DEFAULT 'fallback'
32
+ CHECK (title_source IN ('fallback','generated','manual')),
33
+ created_at INTEGER NOT NULL,
34
+ updated_at INTEGER NOT NULL,
35
+ expires_at INTEGER
36
+ );
37
+
38
+ CREATE INDEX IF NOT EXISTS idx_ai_chats_owner
39
+ ON ai_chats(scope_id, owner_kind, owner_id, updated_at DESC);
40
+
41
+ CREATE INDEX IF NOT EXISTS idx_ai_chats_expiry
42
+ ON ai_chats(expires_at) WHERE expires_at IS NOT NULL;
43
+
44
+ CREATE TABLE IF NOT EXISTS ai_chat_messages (
45
+ id TEXT PRIMARY KEY,
46
+ conversation_id TEXT NOT NULL REFERENCES ai_chats(id) ON DELETE CASCADE,
47
+ role TEXT NOT NULL CHECK (role IN ('user','assistant')),
48
+ content TEXT NOT NULL,
49
+ agent_id TEXT,
50
+ agent_name TEXT,
51
+ events_json TEXT,
52
+ attachments_json TEXT,
53
+ run_id TEXT,
54
+ run_status TEXT,
55
+ position INTEGER NOT NULL,
56
+ created_at INTEGER NOT NULL,
57
+ started_at INTEGER,
58
+ ended_at INTEGER,
59
+ UNIQUE (conversation_id, position)
60
+ );
61
+
62
+ CREATE INDEX IF NOT EXISTS idx_ai_chat_messages_order
63
+ ON ai_chat_messages(conversation_id, position);
64
+ `;
65
+ /**
66
+ * Creates the chat-history tables in the given database if they are absent.
67
+ *
68
+ * Idempotent, matching `ensureToolCatalogTables`'s convention. For a host whose database is
69
+ * managed by its own migration system, prefer copying {@link CHAT_HISTORY_DDL} into a numbered
70
+ * migration instead — see this module's header for why two migrators over one file is a hazard.
71
+ *
72
+ * The caller is responsible for `PRAGMA foreign_keys = ON`; without it the `ON DELETE CASCADE`
73
+ * above is silently inert and deleting a conversation orphans its messages. This function does
74
+ * not set it, because pragmas are connection-wide and a library has no business reconfiguring a
75
+ * handle it was merely lent.
76
+ */
77
+ export function ensureChatHistoryTables(db) {
78
+ db.exec(CHAT_HISTORY_DDL);
79
+ }
80
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../src/db/chat-history/schema.ts"],"names":[],"mappings":"AAwBA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwC/B,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,uBAAuB,CAAC,EAAY;IAClD,EAAE,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;AAC5B,CAAC"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @module db/chat-history/store
3
+ *
4
+ * The SQLite implementation of `@jini-ai/chat/core`'s `ChatHistoryStore`, bound to one owner.
5
+ *
6
+ * The load-bearing property of this file is that **there is no way to obtain an unscoped store**.
7
+ * {@link createChatHistoryStore} takes the scope up front and closes over it; every statement
8
+ * below carries `scope_id = ? AND owner_kind = ? AND owner_id = ?` in its `WHERE` clause, and no
9
+ * exported function accepts a bare conversation id without one. A route handler that forgets the
10
+ * predicate cannot compile, because it never holds the pieces to write the query itself.
11
+ *
12
+ * This is a deliberate departure from this package's older `db/conversations` module, whose
13
+ * `getConversation(db, id)` / `deleteConversation(db, id)` / `listMessages(db, conversationId)`
14
+ * take no owner at all. That module's trust boundary is "you can open this local SQLite file",
15
+ * which is correct for a single-user desktop app and unusable for a shared multi-tenant one.
16
+ * Both exist; pick by trust model, not by convenience.
17
+ *
18
+ * The handle is **injected, never opened here**. `connection.ts`'s `openDatabase` owns
19
+ * `<dataDir>/app.sqlite`; a host with its own database passes its own handle and this module
20
+ * writes into it without ever knowing its path. That injection is what lets the same adapter
21
+ * serve this package's daemon and a host CMS without either one adopting the other's file.
22
+ */
23
+ import type { ChatHistoryMaintenance, ChatHistoryStore, ChatOwnerScope } from '@jini-ai/chat';
24
+ import type { SqliteDb } from '../core/types.js';
25
+ /**
26
+ * Returns a {@link ChatHistoryStore} that can only ever see `scope`'s own conversations.
27
+ *
28
+ * @param db an open `better-sqlite3` handle owned by the caller. This function neither opens nor
29
+ * closes it, and does not set pragmas on it — see `schema.ts` on `foreign_keys`.
30
+ * @param scope the isolation predicate, resolved from the host's authentication. For a `guest`,
31
+ * `ownerId` must already be a hash of the session key; this module never hashes and never
32
+ * verifies that it was hashed.
33
+ * @param now injectable clock, for tests that need deterministic timestamps.
34
+ */
35
+ export declare function createChatHistoryStore(db: SqliteDb, scope: ChatOwnerScope, now?: () => number): ChatHistoryStore;
36
+ /**
37
+ * Retention, across every owner — which is why it is a separate factory rather than a method on
38
+ * a scoped store. Handing this to a store would mean an unscoped store had to exist.
39
+ */
40
+ export declare function createChatHistoryMaintenance(db: SqliteDb): ChatHistoryMaintenance;
41
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../../src/db/chat-history/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAK,EAEV,sBAAsB,EACtB,gBAAgB,EAEhB,cAAc,EAGf,MAAM,eAAe,CAAC;AAEvB,OAAO,KAAK,EAAS,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AA0FxD;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,QAAQ,EACZ,KAAK,EAAE,cAAc,EACrB,GAAG,GAAE,MAAM,MAAiB,GAC3B,gBAAgB,CAgLlB;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,CAAC,EAAE,EAAE,QAAQ,GAAG,sBAAsB,CAmBjF"}
@@ -0,0 +1,254 @@
1
+ /** Columns that make up a `ChatConversation`, aliased once so every read projects identically. */
2
+ const CONVERSATION_COLUMNS = `
3
+ c.id AS id,
4
+ c.title AS title,
5
+ c.title_source AS titleSource,
6
+ c.created_at AS createdAt,
7
+ c.updated_at AS updatedAt,
8
+ c.expires_at AS expiresAt,
9
+ (SELECT COUNT(*) FROM ai_chat_messages m WHERE m.conversation_id = c.id) AS messageCount
10
+ `;
11
+ /** The isolation predicate. Every statement in this file uses this exact clause. */
12
+ const OWNER_PREDICATE = `c.scope_id = ? AND c.owner_kind = ? AND c.owner_id = ?`;
13
+ function toConversation(row) {
14
+ return {
15
+ id: row.id,
16
+ title: row.title ?? null,
17
+ titleSource: (row.titleSource ?? 'fallback'),
18
+ messageCount: Number(row.messageCount ?? 0),
19
+ createdAt: Number(row.createdAt),
20
+ updatedAt: Number(row.updatedAt),
21
+ ...(row.expiresAt === null || row.expiresAt === undefined ? {} : { expiresAt: Number(row.expiresAt) }),
22
+ };
23
+ }
24
+ /**
25
+ * KNOWN GAP — `resumable` and `lastRunEventId` are dropped, deliberately, and adding columns is
26
+ * NOT the fix.
27
+ *
28
+ * `ChatMessage` declares both (`@jini-ai/chat/core`'s `messages.ts`): `resumable` marks a failed run
29
+ * recoverable by resuming the agent's existing session rather than restarting it, and
30
+ * `lastRunEventId` is the cursor to resume from. Neither the projection below, nor `appendMessage`'s
31
+ * insert, nor `CHAT_HISTORY_DDL` carries them, so a message round-tripped through this store comes
32
+ * back with both `undefined` and a failed run cannot be resumed at its cursor. It fails silently —
33
+ * `toMessage` omits absent keys rather than throwing, so the caller gets a well-formed `ChatMessage`.
34
+ *
35
+ * Why the obvious fix is wrong. Both fields describe a RUN, and the daemon owns run state
36
+ * (`@jini-ai/daemon`'s `run-lifecycle.ts` is what decides `resumable` from the terminal event). The
37
+ * component writing through this store today is the host's BROWSER, which knows only what its chat
38
+ * pane handed it. Adding the columns would ask the component that does not own the data to persist
39
+ * it — the values would be as trustworthy as whatever the client happened to send. The ordering that
40
+ * actually works is: associate a run with a conversation id first, then let the daemon persist
41
+ * run-linked state as it streams, and only then add storage for it, at which point there is a
42
+ * consumer to validate the shape against.
43
+ *
44
+ * Two further facts worth having before touching this. First, this is the multi-tenant store; its
45
+ * sibling `db/messages/messages.ts` DOES carry `last_run_event_id`, so the two message stores in
46
+ * this package genuinely disagree and neither is evidence about the other. Second,
47
+ * `CHAT_HISTORY_DDL` is copied verbatim into the reference implementation's
48
+ * `0023_ai_chat_history.sql` and guarded by a
49
+ * structural parity test there; that migration is already applied, so any column added here also
50
+ * needs a follow-up `ALTER TABLE` on the host side and a parity test taught to compare the pair
51
+ * against this constant.
52
+ */
53
+ function toMessage(row) {
54
+ return {
55
+ id: row.id,
56
+ role: row.role,
57
+ content: row.content,
58
+ ...(row.agentId ? { agentId: row.agentId } : {}),
59
+ ...(row.agentName ? { agentName: row.agentName } : {}),
60
+ ...(row.eventsJson ? { events: safeParse(row.eventsJson) } : {}),
61
+ ...(row.attachmentsJson ? { attachments: safeParse(row.attachmentsJson) } : {}),
62
+ ...(row.runId ? { runId: row.runId } : {}),
63
+ ...(row.runStatus ? { runStatus: row.runStatus } : {}),
64
+ ...(row.createdAt === null || row.createdAt === undefined ? {} : { createdAt: Number(row.createdAt) }),
65
+ ...(row.startedAt === null || row.startedAt === undefined ? {} : { startedAt: Number(row.startedAt) }),
66
+ ...(row.endedAt === null || row.endedAt === undefined ? {} : { endedAt: Number(row.endedAt) }),
67
+ };
68
+ }
69
+ /**
70
+ * A malformed JSON column yields `undefined` rather than throwing.
71
+ *
72
+ * A single corrupt `events_json` — a half-written row from a crash mid-stream, say — must not
73
+ * make an entire conversation unreadable. The message's text lives in its own column and is
74
+ * still recoverable; losing the event timeline for one turn is a far better outcome than a list
75
+ * view that throws.
76
+ */
77
+ function safeParse(value) {
78
+ try {
79
+ return JSON.parse(value);
80
+ }
81
+ catch {
82
+ return undefined;
83
+ }
84
+ }
85
+ /**
86
+ * Returns a {@link ChatHistoryStore} that can only ever see `scope`'s own conversations.
87
+ *
88
+ * @param db an open `better-sqlite3` handle owned by the caller. This function neither opens nor
89
+ * closes it, and does not set pragmas on it — see `schema.ts` on `foreign_keys`.
90
+ * @param scope the isolation predicate, resolved from the host's authentication. For a `guest`,
91
+ * `ownerId` must already be a hash of the session key; this module never hashes and never
92
+ * verifies that it was hashed.
93
+ * @param now injectable clock, for tests that need deterministic timestamps.
94
+ */
95
+ export function createChatHistoryStore(db, scope, now = Date.now) {
96
+ const owner = [scope.scopeId, scope.ownerKind, scope.ownerId];
97
+ /** `true` only when this scope owns the conversation — the gate every message write passes. */
98
+ function owns(conversationId) {
99
+ const row = db
100
+ .prepare(`SELECT 1 FROM ai_chats c WHERE c.id = ? AND ${OWNER_PREDICATE}`)
101
+ .get(conversationId, ...owner);
102
+ return row !== undefined;
103
+ }
104
+ return {
105
+ async list() {
106
+ const rows = db
107
+ .prepare(`SELECT ${CONVERSATION_COLUMNS} FROM ai_chats c
108
+ WHERE ${OWNER_PREDICATE}
109
+ ORDER BY c.updated_at DESC`)
110
+ .all(...owner);
111
+ return rows.map(toConversation);
112
+ },
113
+ async get(id) {
114
+ const row = db
115
+ .prepare(`SELECT ${CONVERSATION_COLUMNS} FROM ai_chats c
116
+ WHERE c.id = ? AND ${OWNER_PREDICATE}`)
117
+ .get(id, ...owner);
118
+ return row ? toConversation(row) : null;
119
+ },
120
+ async create(input) {
121
+ const ts = now();
122
+ db.prepare(`INSERT INTO ai_chats
123
+ (id, scope_id, owner_kind, owner_id, title, title_source, created_at, updated_at, expires_at)
124
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(input.id, scope.scopeId, scope.ownerKind, scope.ownerId, input.title ?? null, input.titleSource ?? 'fallback', ts, ts, input.expiresAt ?? null);
125
+ const created = await this.get(input.id);
126
+ // Unreachable in practice: the INSERT above used this exact scope, so the scoped read
127
+ // that follows cannot miss it. Asserted rather than `!`-ed so a future change to either
128
+ // statement fails loudly here instead of returning a malformed conversation.
129
+ if (!created)
130
+ throw new Error(`ai_chats: created conversation ${input.id} was not readable in its own scope`);
131
+ return created;
132
+ },
133
+ async rename(id, title, source = 'manual') {
134
+ // A generated title never overwrites one the user typed. Expressed in the WHERE clause
135
+ // rather than a read-then-write so a concurrent rename cannot slip between the two.
136
+ const guard = source === 'generated' ? ` AND c.title_source <> 'manual'` : '';
137
+ db.prepare(`UPDATE ai_chats AS c
138
+ SET title = ?, title_source = ?, updated_at = ?
139
+ WHERE c.id = ? AND ${OWNER_PREDICATE}${guard}`).run(title, source, now(), id, ...owner);
140
+ return this.get(id);
141
+ },
142
+ async touch(id, options) {
143
+ const ts = now();
144
+ if (options?.expiresAt === undefined) {
145
+ db.prepare(`UPDATE ai_chats AS c SET updated_at = ? WHERE c.id = ? AND ${OWNER_PREDICATE}`)
146
+ .run(ts, id, ...owner);
147
+ return;
148
+ }
149
+ db.prepare(`UPDATE ai_chats AS c SET updated_at = ?, expires_at = ? WHERE c.id = ? AND ${OWNER_PREDICATE}`).run(ts, options.expiresAt, id, ...owner);
150
+ },
151
+ async delete(id) {
152
+ // Messages go via `ON DELETE CASCADE`, which requires `PRAGMA foreign_keys = ON` on this
153
+ // connection. See `schema.ts` — a library setting connection-wide pragmas on a borrowed
154
+ // handle would be worse than documenting the requirement.
155
+ db.prepare(`DELETE FROM ai_chats AS c WHERE c.id = ? AND ${OWNER_PREDICATE}`).run(id, ...owner);
156
+ },
157
+ async messages(conversationId) {
158
+ const rows = db
159
+ .prepare(`SELECT m.id, m.role, m.content, m.agent_id AS agentId, m.agent_name AS agentName,
160
+ m.events_json AS eventsJson, m.attachments_json AS attachmentsJson,
161
+ m.run_id AS runId, m.run_status AS runStatus, m.created_at AS createdAt,
162
+ m.started_at AS startedAt, m.ended_at AS endedAt
163
+ FROM ai_chat_messages m
164
+ JOIN ai_chats c ON c.id = m.conversation_id
165
+ WHERE m.conversation_id = ? AND ${OWNER_PREDICATE}
166
+ ORDER BY m.position`)
167
+ .all(conversationId, ...owner);
168
+ return rows.map(toMessage);
169
+ },
170
+ async appendMessage(conversationId, message) {
171
+ if (!owns(conversationId))
172
+ return null;
173
+ // One `BEGIN IMMEDIATE` around read-then-write: `MAX(position) + 1` computed outside a
174
+ // write transaction is the classic way two concurrent turns claim the same slot. The
175
+ // `UNIQUE (conversation_id, position)` constraint is the backstop if this is ever bypassed.
176
+ //
177
+ // `owns()` above is NOT sufficient on its own, which is why the upsert below carries its own
178
+ // conversation predicate. `owns()` authorizes the *conversation* being written to; the upsert's
179
+ // conflict target is `id`, the GLOBAL primary key on messages. Without the `WHERE`, appending a
180
+ // message id that already exists in someone else's conversation updated THAT row — the caller
181
+ // legitimately owned the conversation it named, so nothing here refused it, and it then got
182
+ // `null` back (a 404 at the route) after the write had landed. See the
183
+ // "colliding id to its OWN chat" case in `__tests__/isolation.test.ts`.
184
+ const write = db.transaction((m) => {
185
+ const existing = db
186
+ .prepare(`SELECT position FROM ai_chat_messages WHERE id = ? AND conversation_id = ?`)
187
+ .get(m.id, conversationId);
188
+ const position = existing
189
+ ? Number(existing.position)
190
+ : Number(db
191
+ .prepare(`SELECT COALESCE(MAX(position), -1) + 1 AS next FROM ai_chat_messages WHERE conversation_id = ?`)
192
+ .get(conversationId).next);
193
+ db.prepare(`INSERT INTO ai_chat_messages
194
+ (id, conversation_id, role, content, agent_id, agent_name, events_json,
195
+ attachments_json, run_id, run_status, position, created_at, started_at, ended_at)
196
+ VALUES (@id, @conversationId, @role, @content, @agentId, @agentName, @eventsJson,
197
+ @attachmentsJson, @runId, @runStatus, @position, @createdAt, @startedAt, @endedAt)
198
+ ON CONFLICT(id) DO UPDATE SET
199
+ content = excluded.content,
200
+ events_json = excluded.events_json,
201
+ attachments_json = excluded.attachments_json,
202
+ run_id = excluded.run_id,
203
+ run_status = excluded.run_status,
204
+ started_at = excluded.started_at,
205
+ ended_at = excluded.ended_at
206
+ WHERE ai_chat_messages.conversation_id = excluded.conversation_id`).run({
207
+ id: m.id,
208
+ conversationId,
209
+ role: m.role,
210
+ content: m.content,
211
+ agentId: m.agentId ?? null,
212
+ agentName: m.agentName ?? null,
213
+ eventsJson: m.events ? JSON.stringify(m.events) : null,
214
+ attachmentsJson: m.attachments ? JSON.stringify(m.attachments) : null,
215
+ runId: m.runId ?? null,
216
+ runStatus: m.runStatus ?? null,
217
+ position,
218
+ createdAt: m.createdAt ?? now(),
219
+ startedAt: m.startedAt ?? null,
220
+ endedAt: m.endedAt ?? null,
221
+ });
222
+ // A new message is activity on the conversation; the list orders by `updated_at`, so
223
+ // skipping this would leave an actively-used chat sinking down the list.
224
+ db.prepare(`UPDATE ai_chats SET updated_at = ? WHERE id = ?`).run(now(), conversationId);
225
+ });
226
+ write(message);
227
+ const saved = await this.messages(conversationId);
228
+ return saved.find((m) => m.id === message.id) ?? null;
229
+ },
230
+ };
231
+ }
232
+ /**
233
+ * Retention, across every owner — which is why it is a separate factory rather than a method on
234
+ * a scoped store. Handing this to a store would mean an unscoped store had to exist.
235
+ */
236
+ export function createChatHistoryMaintenance(db) {
237
+ return {
238
+ async sweepExpired(now, limit = 500) {
239
+ // Bounded by `limit` so a large backlog drains in chunks. An unbounded `DELETE` holds the
240
+ // write lock long enough that concurrent inserts exhaust `busy_timeout` and fail outright.
241
+ // `expires_at IS NOT NULL` keeps never-expiring history structurally out of reach.
242
+ const result = db
243
+ .prepare(`DELETE FROM ai_chats
244
+ WHERE id IN (
245
+ SELECT id FROM ai_chats
246
+ WHERE expires_at IS NOT NULL AND expires_at <= ?
247
+ LIMIT ?
248
+ )`)
249
+ .run(now, limit);
250
+ return result.changes;
251
+ },
252
+ };
253
+ }
254
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../../src/db/chat-history/store.ts"],"names":[],"mappings":"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"}
@@ -15,4 +15,10 @@ export { parseJsonOrUndef, row, rows } from './core/index.js';
15
15
  export { migrate } from './schema/index.js';
16
16
  export type { ToolCatalogEntry, ToolCatalogSearchHit } from './tool-catalog/index.js';
17
17
  export { ensureToolCatalogTables, reseedToolCatalog, getToolCatalogEntry, searchToolCatalog } from './tool-catalog/index.js';
18
+ /**
19
+ * Owner-scoped chat history. Not to be confused with `./conversations/index.js` above: that one
20
+ * is `project_id`-scoped with no owner predicate (single-user local trust model), this one binds
21
+ * an owner into the store so no query can read across tenants. Pick by trust model.
22
+ */
23
+ export { CHAT_HISTORY_DDL, ensureChatHistoryTables, createChatHistoryStore, createChatHistoryMaintenance } from './chat-history/index.js';
18
24
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/db/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,gCAAgC,EAChC,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,YAAY,EACZ,aAAa,EACb,oCAAoC,EACpC,wBAAwB,EACxB,uBAAuB,EACvB,aAAa,GACd,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,YAAY,EACZ,4BAA4B,EAC5B,iCAAiC,EACjC,gCAAgC,EAChC,qBAAqB,EACrB,yBAAyB,EACzB,8BAA8B,EAC9B,UAAU,EACV,aAAa,EACb,aAAa,EACb,aAAa,GACd,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,qBAAqB,EACrB,iCAAiC,EACjC,4BAA4B,EAC5B,iBAAiB,GAClB,MAAM,2BAA2B,CAAC;AACnC,YAAY,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACpF,OAAO,EAAE,gBAAgB,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAC9D,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,YAAY,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AACtF,OAAO,EAAE,uBAAuB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/db/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,gCAAgC,EAChC,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,YAAY,EACZ,aAAa,EACb,oCAAoC,EACpC,wBAAwB,EACxB,uBAAuB,EACvB,aAAa,GACd,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,YAAY,EACZ,4BAA4B,EAC5B,iCAAiC,EACjC,gCAAgC,EAChC,qBAAqB,EACrB,yBAAyB,EACzB,8BAA8B,EAC9B,UAAU,EACV,aAAa,EACb,aAAa,EACb,aAAa,GACd,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,qBAAqB,EACrB,iCAAiC,EACjC,4BAA4B,EAC5B,iBAAiB,GAClB,MAAM,2BAA2B,CAAC;AACnC,YAAY,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACpF,OAAO,EAAE,gBAAgB,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAC9D,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,YAAY,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AACtF,OAAO,EAAE,uBAAuB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC7H;;;;GAIG;AACH,OAAO,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,4BAA4B,EAAE,MAAM,yBAAyB,CAAC"}
package/dist/db/index.js CHANGED
@@ -13,4 +13,10 @@ export { getAgentSession, upsertAgentSession, getAgentSessionRecord, latestCompl
13
13
  export { parseJsonOrUndef, row, rows } from './core/index.js';
14
14
  export { migrate } from './schema/index.js';
15
15
  export { ensureToolCatalogTables, reseedToolCatalog, getToolCatalogEntry, searchToolCatalog } from './tool-catalog/index.js';
16
+ /**
17
+ * Owner-scoped chat history. Not to be confused with `./conversations/index.js` above: that one
18
+ * is `project_id`-scoped with no owner predicate (single-user local trust model), this one binds
19
+ * an owner into the store so no query can read across tenants. Pick by trust model.
20
+ */
21
+ export { CHAT_HISTORY_DDL, ensureChatHistoryTables, createChatHistoryStore, createChatHistoryMaintenance } from './chat-history/index.js';
16
22
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/db/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,gCAAgC,EAChC,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,YAAY,EACZ,aAAa,EACb,oCAAoC,EACpC,wBAAwB,EACxB,uBAAuB,EACvB,aAAa,GACd,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,YAAY,EACZ,4BAA4B,EAC5B,iCAAiC,EACjC,gCAAgC,EAChC,qBAAqB,EACrB,yBAAyB,EACzB,8BAA8B,EAC9B,UAAU,EACV,aAAa,EACb,aAAa,EACb,aAAa,GACd,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,qBAAqB,EACrB,iCAAiC,EACjC,4BAA4B,EAC5B,iBAAiB,GAClB,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EAAE,gBAAgB,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAC9D,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAE5C,OAAO,EAAE,uBAAuB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/db/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,gCAAgC,EAChC,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,YAAY,EACZ,aAAa,EACb,oCAAoC,EACpC,wBAAwB,EACxB,uBAAuB,EACvB,aAAa,GACd,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,YAAY,EACZ,4BAA4B,EAC5B,iCAAiC,EACjC,gCAAgC,EAChC,qBAAqB,EACrB,yBAAyB,EACzB,8BAA8B,EAC9B,UAAU,EACV,aAAa,EACb,aAAa,EACb,aAAa,GACd,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,qBAAqB,EACrB,iCAAiC,EACjC,4BAA4B,EAC5B,iBAAiB,GAClB,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EAAE,gBAAgB,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAC9D,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAE5C,OAAO,EAAE,uBAAuB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC7H;;;;GAIG;AACH,OAAO,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,4BAA4B,EAAE,MAAM,yBAAyB,CAAC"}
@@ -1,4 +1,4 @@
1
- import type { EventLog } from '@jini-ai/daemon';
1
+ import type { EventLog } from '@jini-ai/protocol';
2
2
  /**
3
3
  * Default per-run retention cap applied when `SqliteEventLogOptions.maxEntriesPerRun` is
4
4
  * omitted — see that option's doc for why a durable adapter defaults to a bound instead of
@@ -1 +1 @@
1
- {"version":3,"file":"event-log.d.ts","sourceRoot":"","sources":["../src/event-log.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EACV,QAAQ,EAIT,MAAM,iBAAiB,CAAC;AAEzB;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,OAAO,CAAC;AAEhD,MAAM,WAAW,qBAAqB;IACpC;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CACpC;AAwBD,yHAAyH;AACzH,MAAM,WAAW,cAAe,SAAQ,QAAQ;IAC9C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAyBD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,qBAA0B,GAClC,cAAc,CAuKhB"}
1
+ {"version":3,"file":"event-log.d.ts","sourceRoot":"","sources":["../src/event-log.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EACV,QAAQ,EAIT,MAAM,mBAAmB,CAAC;AAE3B;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,OAAO,CAAC;AAEhD,MAAM,WAAW,qBAAqB;IACpC;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CACpC;AAwBD,yHAAyH;AACzH,MAAM,WAAW,cAAe,SAAQ,QAAQ;IAC9C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAyBD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,qBAA0B,GAClC,cAAc,CAuKhB"}
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@jini-ai/sqlite",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
+ "description": "better-sqlite3-backed persistence adapters for @jini-ai/daemon’s ports: durable event log, backend selection, schema migration, and database inspection.",
4
5
  "license": "Apache-2.0",
5
6
  "repository": {
6
7
  "type": "git",
@@ -10,10 +11,10 @@
10
11
  "jini": {
11
12
  "domain": "server",
12
13
  "kind": "persistence-adapter",
13
- "runtime": "node",
14
- "admission": "locked"
14
+ "runtime": "node"
15
15
  },
16
16
  "type": "module",
17
+ "sideEffects": false,
17
18
  "main": "./dist/index.js",
18
19
  "types": "./dist/index.d.ts",
19
20
  "exports": {
@@ -36,8 +37,9 @@
36
37
  "registry": "https://registry.npmjs.org"
37
38
  },
38
39
  "dependencies": {
39
- "better-sqlite3": "^11.10.0",
40
- "@jini-ai/daemon": "0.2.1"
40
+ "better-sqlite3": "^13.0.0",
41
+ "@jini-ai/chat": "0.3.0",
42
+ "@jini-ai/protocol": "0.3.0"
41
43
  },
42
44
  "devDependencies": {
43
45
  "@types/better-sqlite3": "^7.6.11",