@jini-ai/server 0.0.0-stage → 0.5.1

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.
@@ -0,0 +1,125 @@
1
+ /**
2
+ * @module kernel-base
3
+ *
4
+ * The infrastructure every composition owns regardless of which features it activates: the durable
5
+ * (or in-memory) event log, the run lifecycle, the byte journal, the agent executor, the one shared
6
+ * `ToolRegistry`/`ToolExecutor` pair, and — for sqlite storage — the raw database connection.
7
+ *
8
+ * **The database connection belongs here, not to a feature.** It is borrowed by the readiness probe
9
+ * (`health`), by the raw db-operations tools (`daemonDb`), and by the durable tool catalog
10
+ * (`toolCatalog`) — three independently-selectable features. Had any one of them owned it, disabling
11
+ * that feature would have silently broken the others: turning off `daemonDb` would leave `health`'s
12
+ * readiness probe with nothing to check, which is a much worse failure than the one it was meant to
13
+ * simplify, because a readiness probe that stops probing still answers `200`. Ownership by the base,
14
+ * borrowing by features, is what makes the feature set genuinely independent.
15
+ */
16
+ import { openSqliteConnection } from '@jini-ai/db/sqlite';
17
+ import { join } from 'node:path';
18
+ import { createToolRegistry } from '@jini-ai/core';
19
+ import { createAgentExecutor, createInMemoryEventLog, createRunByteJournal, createRunLifecycle, createToolExecutor, resumableFromProcessExit, } from '@jini-ai/daemon';
20
+ import { openSqliteEventLog } from '@jini-ai/daemon/store/event-log/sqlite';
21
+ /**
22
+ * The zero-config retry classifier every composition gets. Delegates entirely to `@jini-ai/daemon`'s
23
+ * `resumableFromProcessExit` — see that function's own doc for the classification policy.
24
+ */
25
+ export function classifyRunFailureForRetry(context) {
26
+ return resumableFromProcessExit({ code: context.code, signal: context.signal }, context.sideEffects === undefined ? {} : { sideEffects: context.sideEffects });
27
+ }
28
+ /**
29
+ * Opens every kernel-owned resource and rehydrates durable run history.
30
+ *
31
+ * @throws If rehydration fails against a corrupt or unreadable durable history. Every handle this
32
+ * call had already opened is closed before it rethrows, so a failed boot never leaks a sqlite file
33
+ * handle — the invariant the standalone daemon has always maintained.
34
+ */
35
+ export async function createJiniKernelBase(options) {
36
+ const sqliteStorage = options.storage.kind === 'sqlite' ? options.storage : null;
37
+ const dataDir = sqliteStorage?.dataDir ?? null;
38
+ const eventsDbPath = dataDir === null ? null : join(dataDir, 'events.db');
39
+ // One acquisition block, one cleanup path — and **every** acquisition is inside it. Opening even
40
+ // the first event log outside the block would reintroduce exactly the leak this shape exists to
41
+ // prevent: `events.db` opens, `journal.db` fails (corrupt file, or a directory sitting where a
42
+ // database was expected), and the first handle stays open for the lifetime of a process that has
43
+ // no kernel. The second sqlite connection and the rehydration are grouped here for the same
44
+ // reason: all four can fail after handles are already open, and two try/catch blocks with
45
+ // near-identical cleanup is how one of them eventually drifts and starts leaking.
46
+ const opened = [];
47
+ const openEventLog = (path) => {
48
+ let log;
49
+ if (path === null)
50
+ log = createInMemoryEventLog({});
51
+ else {
52
+ // A non-null path is derived only from sqliteStorage above.
53
+ log = openSqliteEventLog({ file: path, open: sqliteStorage.open });
54
+ }
55
+ opened.push(log);
56
+ return log;
57
+ };
58
+ let sqlite = null;
59
+ let featureConnection;
60
+ let eventLog;
61
+ let journalEventLog;
62
+ let lifecycle;
63
+ let journal;
64
+ try {
65
+ eventLog = openEventLog(eventsDbPath);
66
+ // Gap 1's byte journal gets its own store, deliberately separate from `eventLog`: that log
67
+ // replays every entry it holds to SSE subscribers as a `RunProtocolEvent`, and a journal entry
68
+ // has no corresponding protocol-event kind.
69
+ journalEventLog = openEventLog(dataDir === null ? null : join(dataDir, 'journal.db'));
70
+ if (sqliteStorage && eventsDbPath !== null && dataDir !== null) {
71
+ // A *second* connection to the same `events.db` the log above owns — safe: both run in WAL
72
+ // mode, which permits multiple concurrently open handles on one file within a single process.
73
+ featureConnection = openSqliteConnection({ filePath: eventsDbPath, open({ filePath }, settings) {
74
+ featureConnection = sqliteStorage.open(filePath, settings);
75
+ return featureConnection;
76
+ } });
77
+ sqlite = { connection: featureConnection, eventsDbPath, dataDir };
78
+ }
79
+ journal = createRunByteJournal({ eventLog: journalEventLog });
80
+ lifecycle = createRunLifecycle({ eventLog });
81
+ await lifecycle.rehydrate({});
82
+ }
83
+ catch (error) {
84
+ // Every acquired handle is attempted even if another closer fails. Preserve the boot error.
85
+ try {
86
+ featureConnection?.close();
87
+ }
88
+ catch { /* Keep the boot failure. */ }
89
+ await Promise.allSettled(opened.map((log) => log.close?.({})));
90
+ throw error;
91
+ }
92
+ const agentExecutor = createAgentExecutor({ lifecycle }, {
93
+ ...options.agentExecutor,
94
+ journal,
95
+ classifyFailure: classifyRunFailureForRetry,
96
+ });
97
+ const registry = createToolRegistry({});
98
+ // Created eagerly against the still-empty registry, and that is correct: `ToolExecutor` resolves
99
+ // a registration at execute time, not at construction, so features composed later can contribute
100
+ // tools this executor will find. It is what lets a feature's routes take the executor as a
101
+ // dependency while its own tools register in the same pass.
102
+ const toolExecutor = createToolExecutor({ registry });
103
+ let closed = null;
104
+ return {
105
+ eventLog,
106
+ journalEventLog,
107
+ lifecycle,
108
+ agentExecutor,
109
+ registry,
110
+ toolExecutor,
111
+ sqlite,
112
+ close() {
113
+ closed ??= (async () => {
114
+ try {
115
+ sqlite?.connection.close();
116
+ }
117
+ finally {
118
+ await Promise.all([eventLog.close?.({}), journalEventLog.close?.({})]);
119
+ }
120
+ })();
121
+ return closed;
122
+ },
123
+ };
124
+ }
125
+ //# sourceMappingURL=kernel-base.js.map
@@ -0,0 +1,61 @@
1
+ /** Two-stage boot orchestration. Modules clean up their own partially failed stage. */
2
+ export type ModuleLifecycleStatus = {
3
+ status: 'ready';
4
+ } | {
5
+ status: 'disabled';
6
+ reasonCode: string;
7
+ remediationHint: string;
8
+ } | {
9
+ status: 'failed';
10
+ reasonCode: string;
11
+ remediationHint: string;
12
+ };
13
+ export type BootCriticality = 'critical' | 'optional';
14
+ export interface BootModule {
15
+ name: string;
16
+ owner: string;
17
+ criticality: BootCriticality;
18
+ /** Acquire reversible resources; do not start external work yet. */
19
+ prepare(): Promise<void>;
20
+ start(): Promise<void>;
21
+ /** Idempotent; never called on the module whose current stage failed. */
22
+ stop(): Promise<void>;
23
+ }
24
+ export interface BootModuleResult {
25
+ name: string;
26
+ owner: string;
27
+ criticality: BootCriticality;
28
+ lifecycle: ModuleLifecycleStatus;
29
+ }
30
+ export interface BootResult {
31
+ ok: boolean;
32
+ modules: BootModuleResult[];
33
+ }
34
+ export interface ReadinessStore {
35
+ set(required: {
36
+ snapshot: BootResult;
37
+ }): void;
38
+ get(): BootResult;
39
+ }
40
+ export interface LifecycleReporter {
41
+ report(required: {
42
+ result: BootResult;
43
+ }): void;
44
+ }
45
+ /**
46
+ * Preserve source rollback semantics: prepare failure unwinds completed preparations;
47
+ * start failure unwinds completed starts only. Failing modules own their partial cleanup.
48
+ * Prepared modules not yet started on a start-stage abort remain host-owned.
49
+
50
+ * Contract rationale (including readiness ownership):
51
+ * Ports: `BootModule`, `ReadinessStore`, optional `LifecycleReporter`. `runBootLifecycle({ modules }, { readiness, reporter })` runs all preparations before starts. Optional failures do not set `ok=false`; critical failures stop the stage and unwind completed modules in reverse order. The failing module self-cleans. The existing start-stage rollback stops only modules that completed start, leaving other prepared resources host-owned; this extraction deliberately preserves that source behavior. A ready record for an unwound module retains the original source status.
52
+ *
53
+ * `createReadinessStore({}, { initial })` owns defensive snapshot copies per instance. No module-global state or synthetic daemon name. Keep daemon health latching in the host and compose it with this store. Duplicate module names now reject before acquisition; reporter failure is best effort. Lifecycle calls can be concurrent only if callers provide separate modules/stores or coordinate their own boot.
54
+ */
55
+ export declare function runBootLifecycle(required: {
56
+ modules: readonly BootModule[];
57
+ }, optional?: {
58
+ readiness?: ReadinessStore;
59
+ reporter?: LifecycleReporter;
60
+ }): Promise<BootResult>;
61
+ //# sourceMappingURL=boot-lifecycle.d.ts.map
@@ -0,0 +1,74 @@
1
+ async function stopInReverse(modules) {
2
+ for (const module of [...modules].reverse()) {
3
+ try {
4
+ await module.stop();
5
+ }
6
+ catch { /* Preserve the original failure and continue cleanup. */ }
7
+ }
8
+ }
9
+ /**
10
+ * Preserve source rollback semantics: prepare failure unwinds completed preparations;
11
+ * start failure unwinds completed starts only. Failing modules own their partial cleanup.
12
+ * Prepared modules not yet started on a start-stage abort remain host-owned.
13
+
14
+ * Contract rationale (including readiness ownership):
15
+ * Ports: `BootModule`, `ReadinessStore`, optional `LifecycleReporter`. `runBootLifecycle({ modules }, { readiness, reporter })` runs all preparations before starts. Optional failures do not set `ok=false`; critical failures stop the stage and unwind completed modules in reverse order. The failing module self-cleans. The existing start-stage rollback stops only modules that completed start, leaving other prepared resources host-owned; this extraction deliberately preserves that source behavior. A ready record for an unwound module retains the original source status.
16
+ *
17
+ * `createReadinessStore({}, { initial })` owns defensive snapshot copies per instance. No module-global state or synthetic daemon name. Keep daemon health latching in the host and compose it with this store. Duplicate module names now reject before acquisition; reporter failure is best effort. Lifecycle calls can be concurrent only if callers provide separate modules/stores or coordinate their own boot.
18
+ */
19
+ export async function runBootLifecycle(required, optional = {}) {
20
+ const modules = [...required.modules];
21
+ if (new Set(modules.map(module => module.name)).size !== modules.length) {
22
+ throw new Error('boot module names must be unique');
23
+ }
24
+ const results = new Map();
25
+ const prepared = [];
26
+ const ready = [];
27
+ let aborted = false;
28
+ for (const stage of ['prepare', 'start']) {
29
+ const completed = stage === 'prepare' ? prepared : ready;
30
+ for (const module of stage === 'prepare' ? modules : prepared) {
31
+ try {
32
+ await module[stage]();
33
+ completed.push(module);
34
+ if (stage === 'start')
35
+ results.set(module.name, {
36
+ name: module.name, owner: module.owner, criticality: module.criticality,
37
+ lifecycle: { status: 'ready' },
38
+ });
39
+ }
40
+ catch (error) {
41
+ results.set(module.name, {
42
+ name: module.name, owner: module.owner, criticality: module.criticality,
43
+ lifecycle: {
44
+ status: 'failed', reasonCode: error instanceof Error ? error.message : String(error),
45
+ remediationHint: 'check the boot log for the underlying error and retry after fixing it',
46
+ },
47
+ });
48
+ if (module.criticality === 'critical') {
49
+ await stopInReverse(completed);
50
+ aborted = true;
51
+ break;
52
+ }
53
+ }
54
+ }
55
+ if (aborted)
56
+ break;
57
+ }
58
+ const ordered = modules.flatMap(module => {
59
+ const result = results.get(module.name);
60
+ return result === undefined ? [] : [result];
61
+ });
62
+ const result = {
63
+ ok: ordered.every(module => module.criticality !== 'critical' || module.lifecycle.status === 'ready'),
64
+ modules: ordered,
65
+ };
66
+ optional.readiness?.set({ snapshot: result });
67
+ // A reporter must never turn a completed boot into an apparent failure.
68
+ try {
69
+ optional.reporter?.report({ result });
70
+ }
71
+ catch { /* Best-effort reporting. */ }
72
+ return result;
73
+ }
74
+ //# sourceMappingURL=boot-lifecycle.js.map
@@ -0,0 +1,3 @@
1
+ export * from './boot-lifecycle.js';
2
+ export * from './readiness-state.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,3 @@
1
+ export * from './boot-lifecycle.js';
2
+ export * from './readiness-state.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,6 @@
1
+ import type { BootResult, ReadinessStore } from './boot-lifecycle.js';
2
+ /** Instance-owned snapshots. Host-specific health records are supplied by the caller. */
3
+ export declare function createReadinessStore(required: Record<string, never>, optional?: {
4
+ initial?: BootResult;
5
+ }): ReadinessStore;
6
+ //# sourceMappingURL=readiness-state.d.ts.map
@@ -0,0 +1,15 @@
1
+ function copy(snapshot) {
2
+ return { ok: snapshot.ok, modules: snapshot.modules.map(module => ({
3
+ ...module, lifecycle: { ...module.lifecycle },
4
+ })) };
5
+ }
6
+ /** Instance-owned snapshots. Host-specific health records are supplied by the caller. */
7
+ export function createReadinessStore(required, optional = {}) {
8
+ void required;
9
+ let snapshot = copy(optional.initial ?? { ok: true, modules: [] });
10
+ return {
11
+ set({ snapshot: next }) { snapshot = copy(next); },
12
+ get() { return copy(snapshot); },
13
+ };
14
+ }
15
+ //# sourceMappingURL=readiness-state.js.map
@@ -0,0 +1,48 @@
1
+ /**
2
+ * @module backend-config
3
+ *
4
+ * Resolves which SQL backend a host daemon should use from environment
5
+ * variables. v1 ships local SQLite via an injected `better-sqlite3` opener (the event-log adapter
6
+ * now belongs to the daemon's storage subpath);
7
+ * a `postgres` selection is recorded so a future adapter can read it, but
8
+ * `resolveSqliteBackendConfig` never opens a connection itself — it only
9
+ * pins the parameter surface (host/port/database/user/sslMode) so a
10
+ * follow-up package can land the adapter without re-litigating the env-var
11
+ * contract.
12
+ */
13
+ export type SqliteBackendKind = 'sqlite' | 'postgres';
14
+ export interface SqliteBackendConfig {
15
+ kind: SqliteBackendKind;
16
+ /** Resolution metadata a future Postgres adapter will read. */
17
+ postgres?: {
18
+ host: string;
19
+ port: number;
20
+ database: string;
21
+ user: string;
22
+ /**
23
+ * Password / connection string are looked up at runtime from the
24
+ * caller's own secret manager; this layer never reads them through env.
25
+ */
26
+ sslMode?: 'disable' | 'require' | 'verify-full';
27
+ };
28
+ }
29
+ export declare class SqliteBackendConfigError extends Error {
30
+ constructor({ message }: {
31
+ message: string;
32
+ });
33
+ }
34
+ /**
35
+ * Reads `JINI_SQLITE_BACKEND` (`'sqlite'` default, or `'postgres'`) and, for
36
+ * `postgres`, `JINI_PG_HOST`/`JINI_PG_PORT`/`JINI_PG_DATABASE`/`JINI_PG_USER`/
37
+ * `JINI_PG_SSL_MODE` from the given env map (defaults to `process.env`).
38
+ *
39
+ * @param required - No required configuration values; pass an empty argument object.
40
+ * @param optional.env - Environment map to read from. Defaults to `process.env`.
41
+ * @returns The resolved backend config.
42
+ * @throws {SqliteBackendConfigError} When `kind` is unrecognized, or `postgres`
43
+ * is selected without the required host/database/user.
44
+ */
45
+ export declare function resolveSqliteBackendConfig(required: Record<string, never>, optional?: {
46
+ env?: Record<string, string | undefined>;
47
+ }): SqliteBackendConfig;
48
+ //# sourceMappingURL=backend-config.d.ts.map
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @module backend-config
3
+ *
4
+ * Resolves which SQL backend a host daemon should use from environment
5
+ * variables. v1 ships local SQLite via an injected `better-sqlite3` opener (the event-log adapter
6
+ * now belongs to the daemon's storage subpath);
7
+ * a `postgres` selection is recorded so a future adapter can read it, but
8
+ * `resolveSqliteBackendConfig` never opens a connection itself — it only
9
+ * pins the parameter surface (host/port/database/user/sslMode) so a
10
+ * follow-up package can land the adapter without re-litigating the env-var
11
+ * contract.
12
+ */
13
+ export class SqliteBackendConfigError extends Error {
14
+ constructor({ message }) {
15
+ super(message);
16
+ this.name = 'SqliteBackendConfigError';
17
+ }
18
+ }
19
+ /**
20
+ * Reads `JINI_SQLITE_BACKEND` (`'sqlite'` default, or `'postgres'`) and, for
21
+ * `postgres`, `JINI_PG_HOST`/`JINI_PG_PORT`/`JINI_PG_DATABASE`/`JINI_PG_USER`/
22
+ * `JINI_PG_SSL_MODE` from the given env map (defaults to `process.env`).
23
+ *
24
+ * @param required - No required configuration values; pass an empty argument object.
25
+ * @param optional.env - Environment map to read from. Defaults to `process.env`.
26
+ * @returns The resolved backend config.
27
+ * @throws {SqliteBackendConfigError} When `kind` is unrecognized, or `postgres`
28
+ * is selected without the required host/database/user.
29
+ */
30
+ export function resolveSqliteBackendConfig(required, optional = {}) {
31
+ void required;
32
+ const e = optional.env ?? process.env;
33
+ const kind = (e.JINI_SQLITE_BACKEND ?? 'sqlite').trim().toLowerCase();
34
+ if (kind === 'postgres') {
35
+ const host = e.JINI_PG_HOST ?? '';
36
+ const portStr = e.JINI_PG_PORT ?? '5432';
37
+ const database = e.JINI_PG_DATABASE ?? '';
38
+ const user = e.JINI_PG_USER ?? '';
39
+ const sslMode = e.JINI_PG_SSL_MODE === 'disable' || e.JINI_PG_SSL_MODE === 'verify-full' ? e.JINI_PG_SSL_MODE : 'require';
40
+ if (!host || !database || !user) {
41
+ throw new SqliteBackendConfigError({ message: 'JINI_SQLITE_BACKEND=postgres requires JINI_PG_HOST, JINI_PG_DATABASE, JINI_PG_USER. ' +
42
+ 'JINI_PG_PORT defaults to 5432; JINI_PG_SSL_MODE defaults to "require".',
43
+ });
44
+ }
45
+ return {
46
+ kind: 'postgres',
47
+ postgres: {
48
+ host,
49
+ port: Number.parseInt(portStr, 10) || 5432,
50
+ database,
51
+ user,
52
+ sslMode,
53
+ },
54
+ };
55
+ }
56
+ if (kind !== 'sqlite' && kind !== '') {
57
+ throw new SqliteBackendConfigError({ message: `unknown JINI_SQLITE_BACKEND value '${kind}'. Accepted: 'sqlite' (default), 'postgres'.` });
58
+ }
59
+ return { kind: 'sqlite' };
60
+ }
61
+ //# sourceMappingURL=backend-config.js.map
@@ -0,0 +1,3 @@
1
+ /** Host backend selection only; never opens a database or imports a driver. */
2
+ export * from './backend-config.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,3 @@
1
+ /** Host backend selection only; never opens a database or imports a driver. */
2
+ export * from './backend-config.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,26 @@
1
+ import fs from 'node:fs';
2
+ import { type SqliteDb, type SqliteSyncOpener } from '@jini-ai/db/sqlite';
3
+ /**
4
+ * Explicit idempotent bootstrap of unchanged legacy tables; no versioned migration ledger.
5
+ * @param db Borrowed host handle; this operation neither opens nor closes it.
6
+ * @complexity O(1) schema statements.
7
+ */
8
+ export declare function migrate({ db }: {
9
+ db: SqliteDb;
10
+ }): void;
11
+ /**
12
+ * Opens/caches <dataDir>/app.sqlite using the injected opener, WAL and foreign keys.
13
+ * A new path closes the previous singleton; setup failure closes only the newly acquired handle.
14
+ * @throws The original host acquisition/schema failure, or a missing opener.
15
+ * @complexity O(1) acquisition/bootstrap calls, independent of stored row count.
16
+ */
17
+ export declare function openDatabase({ projectRoot, open }: {
18
+ projectRoot: string;
19
+ open: SqliteSyncOpener;
20
+ }, options?: {
21
+ dataDir?: string;
22
+ filesystem?: Pick<typeof fs, 'mkdirSync'>;
23
+ }): SqliteDb;
24
+ /** Close the active owned singleton once; clearing state first permits a clean retry after failure. */
25
+ export declare function closeDatabase(): void;
26
+ //# sourceMappingURL=sqlite.d.ts.map
@@ -0,0 +1,63 @@
1
+ /** Local app.sqlite singleton policy; the host explicitly supplies its SQLite driver. */
2
+ import path from 'node:path';
3
+ import fs from 'node:fs';
4
+ import { openSqliteConnection } from '@jini-ai/db/sqlite';
5
+ import { LEGACY_CHAT_DDL } from '@jini-ai/chat/store/legacy/sqlite';
6
+ import { LEGACY_AGENT_SESSIONS_DDL } from '@jini-ai/daemon/store/agent-sessions/sqlite';
7
+ import { LEGACY_PROJECTS_DDL } from '../../store/projects/sqlite.js';
8
+ let dbInstance = null;
9
+ let dbFile = null;
10
+ /**
11
+ * Explicit idempotent bootstrap of unchanged legacy tables; no versioned migration ledger.
12
+ * @param db Borrowed host handle; this operation neither opens nor closes it.
13
+ * @complexity O(1) schema statements.
14
+ */
15
+ // Only generic engine tables belong in this bootstrap; consumer feature tables and migrations
16
+ // stay in host adapters. Every legacy column is declared in the base DDL, so this is not an
17
+ // ALTER TABLE migration path for a consumer's pre-existing product schema.
18
+ // Catalog tables are created only by hosts that actually seed and query them. A parallel execution
19
+ // table would also weaken the existing ToolExecutionAuditRecord (principal, run and phase data);
20
+ // durable audit storage should preserve that record rather than introduce a second audit noun.
21
+ export function migrate({ db }) {
22
+ db.exec(LEGACY_PROJECTS_DDL + LEGACY_CHAT_DDL + LEGACY_AGENT_SESSIONS_DDL);
23
+ }
24
+ /**
25
+ * Opens/caches <dataDir>/app.sqlite using the injected opener, WAL and foreign keys.
26
+ * A new path closes the previous singleton; setup failure closes only the newly acquired handle.
27
+ * @throws The original host acquisition/schema failure, or a missing opener.
28
+ * @complexity O(1) acquisition/bootstrap calls, independent of stored row count.
29
+ */
30
+ export function openDatabase({ projectRoot, open }, options = {}) {
31
+ // Keep the historical refusal text for JavaScript callers that omit the required port.
32
+ if (!open)
33
+ throw new Error('openDatabase: inject options.open');
34
+ const dir = options.dataDir ? path.resolve(options.dataDir) : path.join(projectRoot, '.jini');
35
+ const file = path.join(dir, 'app.sqlite');
36
+ if (dbInstance && dbFile === file)
37
+ return dbInstance;
38
+ closeDatabase();
39
+ (options.filesystem ?? fs).mkdirSync(dir, { recursive: true });
40
+ let db;
41
+ try {
42
+ db = openSqliteConnection({ filePath: file, open({ filePath }, settings) { db = open(filePath, settings); return db; } }, { pragmas: ['journal_mode = WAL', 'foreign_keys = ON'] });
43
+ migrate({ db });
44
+ dbInstance = db;
45
+ dbFile = file;
46
+ return db;
47
+ }
48
+ catch (error) {
49
+ try {
50
+ db?.close();
51
+ }
52
+ catch { /* Keep the original bootstrap failure. */ }
53
+ throw error;
54
+ }
55
+ }
56
+ /** Close the active owned singleton once; clearing state first permits a clean retry after failure. */
57
+ export function closeDatabase() {
58
+ const db = dbInstance;
59
+ dbInstance = null;
60
+ dbFile = null;
61
+ db?.close();
62
+ }
63
+ //# sourceMappingURL=sqlite.js.map
@@ -0,0 +1,161 @@
1
+ /** @module db/projects
2
+ * CRUD, run-status queries, and awaiting-input detection for the `projects` SQLite table — a
3
+ * generic workspace/container row-store, not an OD-specific concept. Also queries `messages` and
4
+ * `conversations` for run-status and input-gate state. Imports only the core/ kernel (SqliteDb,
5
+ * DbRow); reaches no sibling concern.
6
+ *
7
+ * NOTE (neutral port): drops three OD-product columns with no generic engine equivalent —
8
+ * `skill_id` (OD's agent-skill marketplace selection), `design_system_id` (OD's Design System
9
+ * feature), and `applied_plugin_snapshot_id` (OD's plugin host). The already-ported
10
+ * `schema/migrate.ts` projects table was pre-trimmed to match; see that file and `source-map.md`'s
11
+ * Design Decisions for the same drop list. The kernel-noun exclusion in `extraction-plan.md` §2.1
12
+ * ("projects... conversations" are not kernel nouns) does not block this module for the same reason
13
+ * it didn't block `conversations`: `concern-owned storage` is a generic *optional* storage adapter any
14
+ * consumer may use, not the kernel itself (`@jini-ai/core`/`@jini-ai/daemon` know nothing about this
15
+ * table) — so "project" here is a de-branded generic workspace/container concept, not OD's model.
16
+ */
17
+ import type { SqliteDb } from '@jini-ai/db/sqlite';
18
+ import type { DbRow } from '@jini-ai/db/core';
19
+ /**
20
+ * Returns all projects ordered by most-recently updated first.
21
+ * @param db - Open SQLite connection.
22
+ */
23
+ export declare function listProjects({ db }: {
24
+ db: SqliteDb;
25
+ }): {
26
+ id: any;
27
+ name: any;
28
+ pendingPrompt: any;
29
+ metadata: any;
30
+ customInstructions: any;
31
+ createdAt: number;
32
+ updatedAt: number;
33
+ }[];
34
+ /**
35
+ * Returns the most recent run status for each project, keyed by projectId.
36
+ * Scans messages with a run_status column and collapses to one entry per project,
37
+ * so callers can render project-level status badges without iterating conversations.
38
+ * @param db - Open SQLite connection.
39
+ */
40
+ export declare function listLatestProjectRunStatuses({ db }: {
41
+ db: SqliteDb;
42
+ }): Map<string, DbRow>;
43
+ /**
44
+ * Returns the most recent run status for each conversation, keyed by conversationId.
45
+ * Used to display per-conversation activity state in a project sidebar.
46
+ * @param db - Open SQLite connection.
47
+ */
48
+ export declare function listLatestConversationRunStatuses({ db }: {
49
+ db: SqliteDb;
50
+ }): Map<string, DbRow>;
51
+ /**
52
+ * Returns the earliest run status for each conversation, keyed by conversationId.
53
+ * Useful for determining when a conversation's first agent turn began.
54
+ * @param db - Open SQLite connection.
55
+ */
56
+ export declare function listFirstConversationRunStatuses({ db }: {
57
+ db: SqliteDb;
58
+ }): Map<string, DbRow>;
59
+ /**
60
+ * Returns the most recent run status for each individual run, keyed by runId.
61
+ * Powers per-run status indicators in a conversation thread.
62
+ * @param db - Open SQLite connection.
63
+ */
64
+ export declare function listLatestRunStatuses({ db }: {
65
+ db: SqliteDb;
66
+ }): Map<string, DbRow>;
67
+ /**
68
+ * Returns the set of project IDs whose latest form-bearing assistant message contains a
69
+ * `<question-form>` or `<ask-question>` artifact and has not yet received a user reply.
70
+ * Form filtering deliberately precedes ranking: a newer plain assistant turn does not consume
71
+ * an outstanding question. Keep timestamp/position ordering for replies sharing a timestamp.
72
+ * This is the data source for an "awaiting input" badge on a project list.
73
+ * @param db - Open SQLite connection.
74
+ */
75
+ export declare function listProjectsAwaitingInput({ db }: {
76
+ db: SqliteDb;
77
+ }): Set<any>;
78
+ /**
79
+ * Returns the set of conversation IDs whose latest form-bearing assistant message contains a
80
+ * `<question-form>` or `<ask-question>` artifact and has not yet received a user reply.
81
+ * As in the project query, a newer plain assistant turn leaves an unanswered form outstanding.
82
+ * @param db - Open SQLite connection.
83
+ */
84
+ export declare function listConversationsAwaitingInput({ db }: {
85
+ db: SqliteDb;
86
+ }): Set<any>;
87
+ /**
88
+ * Fetches a single project by id, or returns null if it does not exist.
89
+ * @param db - Open SQLite connection.
90
+ * @param id - Project id.
91
+ */
92
+ export declare function getProject({ db, id }: {
93
+ db: SqliteDb;
94
+ id: string;
95
+ }): {
96
+ id: any;
97
+ name: any;
98
+ pendingPrompt: any;
99
+ metadata: any;
100
+ customInstructions: any;
101
+ createdAt: number;
102
+ updatedAt: number;
103
+ } | null;
104
+ /**
105
+ * Inserts a new project row and returns the freshly read, normalized record.
106
+ * Metadata is serialized to JSON; all nullable fields default to null on omission.
107
+ * @param db - Open SQLite connection.
108
+ * @param project - Project fields to insert.
109
+ */
110
+ export declare function insertProject({ db, project: p }: {
111
+ db: SqliteDb;
112
+ project: DbRow;
113
+ }): {
114
+ id: any;
115
+ name: any;
116
+ pendingPrompt: any;
117
+ metadata: any;
118
+ customInstructions: any;
119
+ createdAt: number;
120
+ updatedAt: number;
121
+ } | null;
122
+ /**
123
+ * Applies a partial patch to an existing project, preserving all unpatched fields.
124
+ * Returns null when the project does not exist; otherwise returns the updated record.
125
+ * updatedAt defaults to Date.now() when the patch omits it.
126
+ * @param db - Open SQLite connection.
127
+ * @param id - Project id to update.
128
+ * @param patch - Fields to overwrite.
129
+ * @param optional.clock - Host clock port; defaults to Date.now() as before. An explicit updatedAt
130
+ * wins without reading the clock, so replayed/imported timestamps remain stable.
131
+ */
132
+ export declare function updateProject({ db, id, patch }: {
133
+ db: SqliteDb;
134
+ id: string;
135
+ patch: DbRow;
136
+ }, optional?: {
137
+ clock?: {
138
+ now(): number;
139
+ };
140
+ }): {
141
+ id: any;
142
+ name: any;
143
+ pendingPrompt: any;
144
+ metadata: any;
145
+ customInstructions: any;
146
+ createdAt: number;
147
+ updatedAt: number;
148
+ } | null;
149
+ /**
150
+ * Permanently removes a project row. Cascading deletes for related rows are
151
+ * handled at the SQLite schema level, not here.
152
+ * @param db - Open SQLite connection.
153
+ * @param id - Project id to delete.
154
+ */
155
+ export declare function deleteProject({ db, id }: {
156
+ db: SqliteDb;
157
+ id: string;
158
+ }): void;
159
+ /** Unchanged local host-project schema; no automatic migration at import. */
160
+ export declare const LEGACY_PROJECTS_DDL = " CREATE TABLE IF NOT EXISTS projects (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n pending_prompt TEXT,\n metadata_json TEXT,\n custom_instructions TEXT,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n );\n\n";
161
+ //# sourceMappingURL=sqlite.d.ts.map