mixdog 1.2.0 → 1.2.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.
- package/README.md +7 -5
- package/package.json +1 -1
- package/src/defaults/shipped-history.json +4 -0
- package/src/defaults/skills/pptx/SKILL.md +6 -6
- package/src/defaults/skills/pptx/references/review.md +6 -4
- package/src/defaults/skills/pptx/scripts/source-extract.mjs +1 -1
- package/src/defaults/skills/setup/references/actions.md +13 -3
- package/src/defaults/skills/visualize/SKILL.md +12 -9
- package/src/rules/shared/60-verification.md +3 -1
- package/src/rules/shared/70-delivery.md +4 -2
- package/src/runtime/agent/orchestrator/cache-mtime.mjs +26 -5
- package/src/runtime/agent/orchestrator/config.mjs +10 -4
- package/src/runtime/agent/orchestrator/context/collect.mjs +2 -1
- package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +11 -5
- package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +74 -21
- package/src/runtime/agent/orchestrator/mcp/client-config.mjs +86 -8
- package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +5 -1
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +65 -7
- package/src/runtime/agent/orchestrator/mcp/client.mjs +128 -44
- package/src/runtime/agent/orchestrator/mcp/lazy-output-validator.mjs +25 -0
- package/src/runtime/agent/orchestrator/mcp/plugin-placeholders.mjs +54 -0
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +108 -17
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +19 -11
- package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/command-code-oauth-login.mjs +337 -0
- package/src/runtime/agent/orchestrator/providers/command-code-oauth-tokens.mjs +159 -0
- package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +444 -0
- package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +7 -4
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +9 -2
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +41 -28
- package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/model-proxy-bridge.mjs +1080 -0
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +15 -0
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +35 -0
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +12 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +6 -5
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +6 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-options.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +9 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +4 -4
- package/src/runtime/agent/orchestrator/providers/registry.mjs +35 -9
- package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +3 -0
- package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +15 -55
- package/src/runtime/agent/orchestrator/runtime-core/goal-control.mjs +15 -2
- package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +267 -84
- package/src/runtime/agent/orchestrator/runtime-core/skill-tool-loading.mjs +4 -1
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-schema.mjs +21 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +54 -3
- package/src/runtime/agent/orchestrator/session/catalog-db/index.mjs +284 -0
- package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +268 -0
- package/src/runtime/agent/orchestrator/session/catalog-db/sqlite-gate.mjs +27 -0
- package/src/runtime/agent/orchestrator/session/catalog-db/worker.mjs +628 -0
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +6 -2
- package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +184 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +136 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +128 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/imported-session.mjs +127 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/explicit-skills.mjs +83 -5
- package/src/runtime/agent/orchestrator/session/imported-message.mjs +11 -0
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +3 -2
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +6 -4
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +4 -3
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +12 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +20 -12
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +9 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +17 -4
- package/src/runtime/agent/orchestrator/session/manager.mjs +0 -1
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +4 -3
- package/src/runtime/agent/orchestrator/session/store/listing.mjs +6 -2
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +817 -0
- package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +19 -12
- package/src/runtime/agent/orchestrator/session/store-summary-reader-catalog-fixture.mjs +158 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +183 -374
- package/src/runtime/agent/orchestrator/session/synthetic-user-envelope.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +21 -2
- package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +38 -17
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +17 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +4 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +6 -6
- package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +173 -22
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-guards.mjs +8 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +5 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +15 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +13 -10
- package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +17 -12
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/root-resolution.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/apply-patch/codex-batch.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/patch/apply-patch/route.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/read-redirects.mjs +34 -30
- package/src/runtime/agent/orchestrator/tools/patch/sequence/units.mjs +7 -7
- package/src/runtime/agent/orchestrator/tools/patch/sequence.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -0
- package/src/runtime/attachments/store.d.mts +3 -0
- package/src/runtime/channels/lib/owned-runtime/owned-start.mjs +1 -1
- package/src/runtime/channels/lib/webhook/session-dispatch.mjs +16 -4
- package/src/runtime/channels/lib/worker-main.mjs +17 -14
- package/src/runtime/computer-bridge/linux-readiness.mjs +0 -2
- package/src/runtime/effort-judge/judge-client.mjs +56 -18
- package/src/runtime/memory/lib/memory.mjs +1 -1
- package/src/runtime/memory/lib/session-ingest.mjs +2 -2
- package/src/runtime/shared/atomic-file.mjs +25 -4
- package/src/runtime/shared/extension-scopes.mjs +5 -3
- package/src/runtime/shared/file-lock.mjs +37 -11
- package/src/runtime/shared/llm/anthropic-effort.mjs +14 -0
- package/src/runtime/shared/llm/model-catalog.mjs +0 -12
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +6 -0
- package/src/runtime/shared/plugin-manifest.mjs +18 -9
- package/src/runtime/shared/pristine-execution-contract.json +9 -2
- package/src/runtime/shared/pristine-execution.mjs +28 -4
- package/src/runtime/shared/provider-accounts.mjs +124 -20
- package/src/runtime/shared/provider-api-key.mjs +12 -0
- package/src/runtime/shared/shell-display.mjs +9 -3
- package/src/runtime/shared/skill-mentions.d.mts +10 -0
- package/src/runtime/shared/skill-mentions.mjs +133 -0
- package/src/runtime/shared/skill-script-runtime.mjs +264 -0
- package/src/runtime/shared/tool-primitives.mjs +35 -2
- package/src/runtime/shared/turn-worktree-snapshot.mjs +25 -5
- package/src/runtime/shared/worker-requests.mjs +15 -2
- package/src/session-runtime/automation-agents.mjs +8 -2
- package/src/session-runtime/boot/apis.mjs +55 -0
- package/src/session-runtime/boot/config.mjs +5 -2
- package/src/session-runtime/boot/facade.mjs +1 -0
- package/src/session-runtime/boot/infrastructure.mjs +29 -18
- package/src/session-runtime/conversation-import/discovery.mjs +698 -0
- package/src/session-runtime/conversation-import/service.mjs +369 -0
- package/src/session-runtime/cwd-plugins/cwd-apply.mjs +10 -5
- package/src/session-runtime/cwd-plugins/plugin-status.mjs +47 -42
- package/src/session-runtime/mcp-glue/connect.mjs +57 -10
- package/src/session-runtime/mcp-glue/plugin-servers.mjs +499 -0
- package/src/session-runtime/mcp-glue/server-catalog.mjs +115 -30
- package/src/session-runtime/mcp-glue.mjs +22 -2
- package/src/session-runtime/new-session-config.mjs +3 -2
- package/src/session-runtime/notification-bus.mjs +9 -2
- package/src/session-runtime/provider-auth/accounts.mjs +33 -15
- package/src/session-runtime/provider-models/web-search-catalog.mjs +5 -1
- package/src/session-runtime/provider-readiness.mjs +16 -10
- package/src/session-runtime/resource-extension-sync.mjs +20 -5
- package/src/session-runtime/resource-mcp-api.mjs +101 -77
- package/src/session-runtime/resource-plugins-api.mjs +29 -41
- package/src/session-runtime/resource-scope-decorate.mjs +6 -8
- package/src/session-runtime/resource-skills-api.mjs +23 -2
- package/src/session-runtime/schedule-session-run.mjs +1 -1
- package/src/session-runtime/services/channel-session-router.mjs +4 -1
- package/src/session-runtime/services/hook-bus/command-handler.mjs +4 -1
- package/src/session-runtime/services/hook-bus/handlers.mjs +7 -3
- package/src/session-runtime/services/provider-admin.mjs +64 -18
- package/src/session-runtime/services/usage-dashboard-model.mjs +3 -0
- package/src/session-runtime/setup-tool/executor.mjs +12 -1
- package/src/session-runtime/setup-tool/extended-actions.mjs +1 -0
- package/src/session-runtime/setup-tool/settings-contract.mjs +1 -0
- package/src/session-runtime/setup-tool/tool-defs.mjs +7 -1
- package/src/session-runtime/skills-api.mjs +7 -3
- package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
- package/src/session-runtime/webhook-session-run.mjs +16 -4
- package/src/skill-script-entry.mjs +36 -0
- package/src/standalone/daemon-stored-session-views.mjs +11 -3
- package/src/standalone/daemon.mjs +41 -4
- package/src/standalone/session-protocol.mjs +5 -0
- package/src/standalone/session-service/session-calls/session-action.mjs +6 -1
- package/src/tui/App.jsx +9 -1
- package/src/tui/app/app-view/floating-panel.jsx +9 -1
- package/src/tui/app/app-view.jsx +6 -0
- package/src/tui/app/extension-pickers/plugins-pickers.mjs +35 -16
- package/src/tui/app/model-options.mjs +2 -0
- package/src/tui/app/skill-mention-palette.mjs +88 -0
- package/src/tui/app/slash-palette.mjs +23 -7
- package/src/tui/app/transcript-row-estimate.mjs +11 -3
- package/src/tui/app/use-prompt-draft-flow.mjs +7 -2
- package/src/tui/app/use-prompt-state.mjs +5 -0
- package/src/tui/components/Message.jsx +22 -1
- package/src/tui/components/PromptInput.jsx +21 -3
- package/src/tui/components/SlashCommandPalette.jsx +12 -3
- package/src/tui/components/TranscriptItem.jsx +9 -1
- package/src/tui/components/prompt-input/key-handler.mjs +21 -8
- package/src/tui/components/user-message-skills.mjs +38 -0
- package/src/tui/dist/index.mjs +507 -171
- package/src/tui/session/queue-helpers.mjs +7 -2
- package/src/tui/session/session-api/extensions.mjs +4 -2
- package/src/tui/session/session-api/settings.mjs +7 -0
- package/src/tui/session/session-flow/drain.mjs +14 -4
- package/src/tui/session/tui-steering-persist.mjs +25 -9
- package/src/tui/session/turn.mjs +17 -0
- package/src/runtime/effort-judge/previous-human-request.mjs +0 -162
- package/src/runtime/effort-judge/verified-context-input.mjs +0 -10
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session catalog DB, phase 2: in the daemon, SQLite is the read source of
|
|
3
|
+
* the ordinary session catalog.
|
|
4
|
+
*
|
|
5
|
+
* Everything runs on one worker thread (./worker.mjs): the catalog engine's
|
|
6
|
+
* change detection (summary index, sessions directory, per-file stamps, `.hb`
|
|
7
|
+
* liveness, background verification) feeds `<dataDir>/session-catalog.sqlite`
|
|
8
|
+
* and an ordinary listing is one request that answers from it. This thread
|
|
9
|
+
* does no filesystem work for it and never loads node:sqlite.
|
|
10
|
+
*
|
|
11
|
+
* JSON stays what every other process reads and writes: the store keeps
|
|
12
|
+
* writing the summary index exactly as before. When one of the daemon's own
|
|
13
|
+
* writes has changed a session file on disk, the store hints the id here
|
|
14
|
+
* (hintSessionCatalogStorage); the worker re-checks it from storage on the
|
|
15
|
+
* next listing. Hints carry no row data. Whenever the DB cannot answer — not
|
|
16
|
+
* the daemon, not Electron, SQLite gate, another process holds the DB,
|
|
17
|
+
* off-switch, backfill or rebuild in progress, any error — `list()` resolves
|
|
18
|
+
* null and the caller uses the JSON path unchanged.
|
|
19
|
+
*/
|
|
20
|
+
import { isMainThread } from 'node:worker_threads';
|
|
21
|
+
import { createWorkerRequestClient } from '../../../../shared/worker-requests.mjs';
|
|
22
|
+
import { dataDir as currentDataDir } from '../store-summary-locations.mjs';
|
|
23
|
+
|
|
24
|
+
export const SESSION_CATALOG_DB_FILE = 'session-catalog.sqlite';
|
|
25
|
+
/** Config: `{ "sessionStore": { "sqliteCatalog": false } }` forces the JSON
|
|
26
|
+
* path (an off-switch); absent or true uses the DB whenever it can. */
|
|
27
|
+
export const SESSION_STORE_CONFIG_SECTION = 'sessionStore';
|
|
28
|
+
export const SESSION_STORE_CONFIG_DEFAULTS = Object.freeze({ sqliteCatalog: true });
|
|
29
|
+
/** Stable prefix of every daemon.log line of the catalog DB. */
|
|
30
|
+
export const SESSION_CATALOG_LOG_PREFIX = '[session-catalog-db]';
|
|
31
|
+
|
|
32
|
+
// A worker that keeps dying is given up on for a while.
|
|
33
|
+
const CRASH_WINDOW_MS = 10 * 60_000;
|
|
34
|
+
const MAX_CRASHES = 3;
|
|
35
|
+
const ERROR_LOG_INTERVAL_MS = 60_000;
|
|
36
|
+
|
|
37
|
+
let testOptions = null;
|
|
38
|
+
let requestWorker = null;
|
|
39
|
+
let controller = null;
|
|
40
|
+
const changeListeners = new Set();
|
|
41
|
+
const noop = () => {};
|
|
42
|
+
|
|
43
|
+
function log(line) {
|
|
44
|
+
const text = `${SESSION_CATALOG_LOG_PREFIX} ${line}`;
|
|
45
|
+
if (typeof testOptions?.log === 'function') {
|
|
46
|
+
testOptions.log(text);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// The daemon's stderr is daemon.log (mirrored by the spawner before ready,
|
|
50
|
+
// redirected into the file after).
|
|
51
|
+
try {
|
|
52
|
+
process.stderr.write(`${text}\n`);
|
|
53
|
+
} catch {
|
|
54
|
+
/* logging is best effort */
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function onWorkerEvent(event) {
|
|
59
|
+
if (event?.type === 'log') log(event.line);
|
|
60
|
+
else if (event?.type === 'changed') {
|
|
61
|
+
for (const listener of changeListeners) {
|
|
62
|
+
try {
|
|
63
|
+
listener();
|
|
64
|
+
} catch {
|
|
65
|
+
/* a listener's failure is its own */
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function request(payload) {
|
|
72
|
+
requestWorker ||= createWorkerRequestClient(new URL('./worker.mjs', import.meta.url), {
|
|
73
|
+
// The worker is the only node:sqlite user; its experimental warning is noise.
|
|
74
|
+
execArgv: ['--disable-warning=ExperimentalWarning'],
|
|
75
|
+
onEvent: onWorkerEvent,
|
|
76
|
+
});
|
|
77
|
+
return requestWorker(payload);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Worker settings a test configured (functions stay on this thread). */
|
|
81
|
+
function workerSettings() {
|
|
82
|
+
if (!testOptions) return undefined;
|
|
83
|
+
const settings = {};
|
|
84
|
+
for (const [key, value] of Object.entries(testOptions)) {
|
|
85
|
+
if (typeof value !== 'function' && key !== 'writer') settings[key] = value;
|
|
86
|
+
}
|
|
87
|
+
return settings;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* True only in the daemon process itself, under the packaged or dev Electron
|
|
92
|
+
* runtime: daemon.mjs sets MIXDOG_DAEMON_HOST and MIXDOG_SERVER_PID (its own
|
|
93
|
+
* pid) at startup; children inherit the environment but have another pid,
|
|
94
|
+
* worker threads are not the main thread, and a daemon started by the CLI
|
|
95
|
+
* under system Node stays on JSON.
|
|
96
|
+
*/
|
|
97
|
+
export function isSessionCatalogDbWriter() {
|
|
98
|
+
if (testOptions && 'writer' in testOptions) return testOptions.writer === true;
|
|
99
|
+
return (
|
|
100
|
+
isMainThread &&
|
|
101
|
+
Boolean(process.versions.electron) &&
|
|
102
|
+
process.env.MIXDOG_DAEMON_HOST === '1' &&
|
|
103
|
+
process.env.MIXDOG_SERVER_PID === String(process.pid)
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The catalog DB of `dataDir`, or null when this process does not keep one. */
|
|
108
|
+
export function sessionCatalogDb(dataDir) {
|
|
109
|
+
if (!isSessionCatalogDbWriter()) return null;
|
|
110
|
+
if (controller?.dataDir === dataDir) return controller;
|
|
111
|
+
void controller?.close();
|
|
112
|
+
controller = new SessionCatalogDb(dataDir);
|
|
113
|
+
return controller;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The existing catalog DB of `dataDir` (never creates one). */
|
|
117
|
+
export function currentSessionCatalogDb(dataDir) {
|
|
118
|
+
return controller?.dataDir === dataDir ? controller : null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** `listener()` runs when the worker's background pass finds a changed
|
|
122
|
+
* session file (an ordinary listing would now differ). */
|
|
123
|
+
export function onSessionCatalogDbChange(listener) {
|
|
124
|
+
changeListeners.add(listener);
|
|
125
|
+
return () => changeListeners.delete(listener);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Storage hint from the store (store/summary-cache.mjs): a write of this
|
|
130
|
+
* process changed the files of `ids` — a save landed, a delete removed the
|
|
131
|
+
* file. Tagged with the current data dir; the worker re-checks those ids from
|
|
132
|
+
* storage on the next listing. A lost hint only delays the change to the
|
|
133
|
+
* engine's own detection.
|
|
134
|
+
*/
|
|
135
|
+
export function hintSessionCatalogStorage(ids) {
|
|
136
|
+
currentSessionCatalogDb(currentDataDir())?.hint(ids);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Closes the catalog DB (releasing its file, WAL and lock) — the teardown
|
|
140
|
+
* boundary before a data dir is removed or switched. */
|
|
141
|
+
export async function closeSessionCatalogDb() {
|
|
142
|
+
const current = controller;
|
|
143
|
+
controller = null;
|
|
144
|
+
if (current) await current.close();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
class SessionCatalogDb {
|
|
148
|
+
constructor(dataDir) {
|
|
149
|
+
this.dataDir = dataDir;
|
|
150
|
+
this.state = 'active'; // active | off (final: gate) | failed (until failedUntil) | closed
|
|
151
|
+
this.failedUntil = 0;
|
|
152
|
+
this.hints = new Set();
|
|
153
|
+
this.flushHandle = null;
|
|
154
|
+
this.started = false;
|
|
155
|
+
this.crashes = [];
|
|
156
|
+
this.errorLoggedAt = 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
send(op, payload = {}) {
|
|
160
|
+
// No running worker: this request starts one. After the first start it
|
|
161
|
+
// is a respawn (the last one died): it reopens and backfills by itself.
|
|
162
|
+
if (!requestWorker?.running()) {
|
|
163
|
+
if (this.started) this.noteRespawn();
|
|
164
|
+
this.started = true;
|
|
165
|
+
}
|
|
166
|
+
return request({ op, dataDir: this.dataDir, settings: workerSettings(), ...payload }).catch((error) => {
|
|
167
|
+
this.noteError(error);
|
|
168
|
+
throw error;
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** A worker died; more than MAX_CRASHES deaths in the window give up. */
|
|
173
|
+
noteRespawn() {
|
|
174
|
+
const now = Date.now();
|
|
175
|
+
this.crashes = this.crashes.filter((at) => now - at < CRASH_WINDOW_MS);
|
|
176
|
+
this.crashes.push(now);
|
|
177
|
+
if (this.crashes.length > MAX_CRASHES && this.state === 'active') {
|
|
178
|
+
this.state = 'failed';
|
|
179
|
+
this.failedUntil = now + CRASH_WINDOW_MS;
|
|
180
|
+
log(`worker failed ${this.crashes.length} times in 10 min; catalog stays JSON`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
noteError(error) {
|
|
185
|
+
const now = Date.now();
|
|
186
|
+
if (now - this.errorLoggedAt >= ERROR_LOG_INTERVAL_MS) {
|
|
187
|
+
this.errorLoggedAt = now;
|
|
188
|
+
log(`request failed: ${error?.message || error}; listing answered from JSON`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
usable() {
|
|
193
|
+
if (this.state === 'failed' && Date.now() >= this.failedUntil) {
|
|
194
|
+
this.state = 'active';
|
|
195
|
+
this.crashes = [];
|
|
196
|
+
}
|
|
197
|
+
return this.state === 'active';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
hint(ids) {
|
|
201
|
+
if (this.state !== 'active') return;
|
|
202
|
+
for (const id of ids) if (id) this.hints.add(String(id));
|
|
203
|
+
if (this.hints.size === 0) return;
|
|
204
|
+
this.flushHandle ||= setImmediate(() => {
|
|
205
|
+
this.flushHandle = null;
|
|
206
|
+
this.flushHints();
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Posts the collected hints ahead of any later request. A worker that is
|
|
211
|
+
* not running gets none: a (re)started worker rescans storage anyway. */
|
|
212
|
+
flushHints() {
|
|
213
|
+
if (this.hints.size === 0) return;
|
|
214
|
+
const ids = [...this.hints];
|
|
215
|
+
this.hints.clear();
|
|
216
|
+
if (!requestWorker?.running()) return;
|
|
217
|
+
this.send('hint', { ids }).catch(noop);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* One ordinary listing (`page` = `{ limit, after }` or null for all):
|
|
222
|
+
* `{ rows, more }`, or null when the DB cannot answer (use JSON). Hints
|
|
223
|
+
* collected before this call are posted first.
|
|
224
|
+
*/
|
|
225
|
+
async list(page = null) {
|
|
226
|
+
if (!this.usable()) return null;
|
|
227
|
+
this.flushHints();
|
|
228
|
+
let answer;
|
|
229
|
+
try {
|
|
230
|
+
answer = await this.send('list', { page });
|
|
231
|
+
} catch {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
if (answer.status === 'ok') return { rows: answer.rows, more: answer.more };
|
|
235
|
+
if (answer.final && this.state === 'active') this.state = 'off';
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
verifyFiles() {
|
|
240
|
+
if (!this.usable()) return Promise.resolve();
|
|
241
|
+
return this.send('verifyFiles').catch(noop);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Test/benchmark seams. */
|
|
245
|
+
status() {
|
|
246
|
+
return this.send('status');
|
|
247
|
+
}
|
|
248
|
+
shadow() {
|
|
249
|
+
return this.send('shadow');
|
|
250
|
+
}
|
|
251
|
+
settled() {
|
|
252
|
+
this.flushHints();
|
|
253
|
+
return this.send('settle');
|
|
254
|
+
}
|
|
255
|
+
testExec(sql, params = []) {
|
|
256
|
+
return this.send('testExec', { sql, params });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async close() {
|
|
260
|
+
if (this.state === 'closed') return;
|
|
261
|
+
this.state = 'closed';
|
|
262
|
+
if (this.flushHandle) clearImmediate(this.flushHandle);
|
|
263
|
+
this.flushHandle = null;
|
|
264
|
+
this.hints.clear();
|
|
265
|
+
// Only a running worker can hold the DB open.
|
|
266
|
+
if (requestWorker?.running()) {
|
|
267
|
+
await request({ op: 'close', dataDir: this.dataDir }).catch(noop);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ── Test seams ──────────────────────────────────────────────────────────────
|
|
273
|
+
/** Test seam: `{ writer, enabled, sqliteVersion, backfillBatch,
|
|
274
|
+
* shadowIntervalMs, retryMs, fenceTimeoutMs, crashAfterBatches, log }`;
|
|
275
|
+
* null restores defaults. */
|
|
276
|
+
export function configureSessionCatalogDbForTest(options) {
|
|
277
|
+
testOptions = options ? { ...options } : null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Test seam: post a storage hint tagged with `dataDir` (another data dir's
|
|
281
|
+
* hint must be ignored by the worker). */
|
|
282
|
+
export function postSessionCatalogHintForTest(dataDir, ids) {
|
|
283
|
+
return request({ op: 'hint', dataDir, ids });
|
|
284
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session catalog DB: table layout, row codec and the listing comparison.
|
|
3
|
+
* Pure (no IO, no node:sqlite) so both the worker and tests can use it.
|
|
4
|
+
*
|
|
5
|
+
* One row per catalog session: every field store-summary-reader.mjs
|
|
6
|
+
* normalizedRow produces, the storage fingerprint (full stamp of the session
|
|
7
|
+
* file the row was judged at), the title normalized at write, and the
|
|
8
|
+
* bookkeeping the mirror needs (lead visibility, sync epoch, row signature).
|
|
9
|
+
*/
|
|
10
|
+
import { createHash } from 'node:crypto';
|
|
11
|
+
|
|
12
|
+
/** Bumped whenever the table layout or a codec changes: the DB is then
|
|
13
|
+
* dropped and backfilled again from the JSON catalog. */
|
|
14
|
+
export const SCHEMA_VERSION = 3;
|
|
15
|
+
/** PRAGMA application_id ('MXSC'): a file without it is not this catalog. */
|
|
16
|
+
export const APPLICATION_ID = 0x4d585343;
|
|
17
|
+
|
|
18
|
+
// [row key, column, codec]
|
|
19
|
+
// real/int numbers; bool 0/1; text strings
|
|
20
|
+
// loose whatever the source carried (string/number/null as is, anything
|
|
21
|
+
// else as a JSON BLOB), so the row round-trips exactly
|
|
22
|
+
// json object or null, as JSON text
|
|
23
|
+
// optjson like json, but the key is absent from the row when NULL
|
|
24
|
+
const FIELDS = [
|
|
25
|
+
['id', 'id', 'text'],
|
|
26
|
+
['updatedAt', 'updated_at', 'real'],
|
|
27
|
+
['lastUsedAt', 'last_used_at', 'real'],
|
|
28
|
+
['createdAt', 'created_at', 'real'],
|
|
29
|
+
['lastHeartbeatAt', 'last_heartbeat_at', 'real'],
|
|
30
|
+
// Always 0 in a stored row: liveness is overlaid from `.hb` sidecars at read.
|
|
31
|
+
['heartbeatAt', 'heartbeat_at', 'real'],
|
|
32
|
+
['closed', 'closed', 'bool'],
|
|
33
|
+
['status', 'status', 'text'],
|
|
34
|
+
['owner', 'owner', 'loose'],
|
|
35
|
+
['agent', 'agent', 'loose'],
|
|
36
|
+
['sourceType', 'source_type', 'loose'],
|
|
37
|
+
['sourceName', 'source_name', 'loose'],
|
|
38
|
+
['scopeKey', 'scope_key', 'loose'],
|
|
39
|
+
['ownerSessionId', 'owner_session_id', 'loose'],
|
|
40
|
+
['visibility', 'visibility', 'loose'],
|
|
41
|
+
['clientHostPid', 'client_host_pid', 'loose'],
|
|
42
|
+
['cwd', 'cwd', 'loose'],
|
|
43
|
+
['desktopSession', 'desktop_session', 'json'],
|
|
44
|
+
['workspace', 'workspace', 'optjson'],
|
|
45
|
+
['provider', 'provider', 'loose'],
|
|
46
|
+
['model', 'model', 'loose'],
|
|
47
|
+
['agentTag', 'agent_tag', 'loose'],
|
|
48
|
+
['task_id', 'task_id', 'loose'],
|
|
49
|
+
['permission', 'permission', 'loose'],
|
|
50
|
+
['toolPermission', 'tool_permission', 'loose'],
|
|
51
|
+
['messageCount', 'message_count', 'int'],
|
|
52
|
+
['title', 'title', 'text'],
|
|
53
|
+
['preview', 'preview', 'text'],
|
|
54
|
+
['generation', 'generation', 'loose'],
|
|
55
|
+
['storageMtimeMs', 'storage_mtime_ms', 'real'],
|
|
56
|
+
['storageSize', 'storage_size', 'real'],
|
|
57
|
+
['detachedReason', 'detached_reason', 'loose'],
|
|
58
|
+
// importOrigin.key of an imported conversation (schema 3).
|
|
59
|
+
['importKey', 'import_key', 'loose'],
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const COLUMN_TYPES = { text: 'TEXT', real: 'REAL', int: 'INTEGER', bool: 'INTEGER', json: 'TEXT', optjson: 'TEXT' };
|
|
63
|
+
|
|
64
|
+
const columnDefinition = ([, column, codec]) => {
|
|
65
|
+
if (column === 'id') return 'id TEXT PRIMARY KEY';
|
|
66
|
+
// No declared type = no affinity: loose values are stored exactly as bound.
|
|
67
|
+
return codec === 'loose' ? column : `${column} ${COLUMN_TYPES[codec]}`;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const SCHEMA_SQL = `
|
|
71
|
+
CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);
|
|
72
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
73
|
+
${FIELDS.map(columnDefinition).join(',\n ')},
|
|
74
|
+
activity REAL NOT NULL,
|
|
75
|
+
title_norm TEXT NOT NULL,
|
|
76
|
+
fingerprint TEXT,
|
|
77
|
+
lead_visible INTEGER NOT NULL,
|
|
78
|
+
row_sig TEXT NOT NULL
|
|
79
|
+
);
|
|
80
|
+
CREATE INDEX IF NOT EXISTS sessions_recent ON sessions(activity DESC, id);
|
|
81
|
+
CREATE INDEX IF NOT EXISTS sessions_lead_recent ON sessions(lead_visible, activity DESC, id);
|
|
82
|
+
CREATE INDEX IF NOT EXISTS sessions_visibility_recent ON sessions(visibility, activity DESC, id);
|
|
83
|
+
CREATE INDEX IF NOT EXISTS sessions_owner_session ON sessions(owner_session_id);
|
|
84
|
+
`;
|
|
85
|
+
|
|
86
|
+
const WRITE_COLUMNS = [
|
|
87
|
+
...FIELDS.map(([, column]) => column),
|
|
88
|
+
'activity',
|
|
89
|
+
'title_norm',
|
|
90
|
+
'fingerprint',
|
|
91
|
+
'lead_visible',
|
|
92
|
+
'row_sig',
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
export const UPSERT_SQL = `INSERT INTO sessions(${WRITE_COLUMNS.join(',')})
|
|
96
|
+
VALUES (${WRITE_COLUMNS.map(() => '?').join(',')})
|
|
97
|
+
ON CONFLICT(id) DO UPDATE SET ${WRITE_COLUMNS.slice(1)
|
|
98
|
+
.map((column) => `${column}=excluded.${column}`)
|
|
99
|
+
.join(',')}`;
|
|
100
|
+
|
|
101
|
+
/** Recent-activity key of the JSON listing (store-summary-reader byRecentActivity). */
|
|
102
|
+
export function activityOf(row) {
|
|
103
|
+
return row.lastUsedAt || row.updatedAt || 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Newest activity first, ties by id: the DB order and the keyset order.
|
|
107
|
+
* Ids are ASCII ([A-Za-z0-9_-]), so code-unit order equals SQLite BINARY. */
|
|
108
|
+
export function byActivityThenId(left, right) {
|
|
109
|
+
const delta = activityOf(right) - activityOf(left);
|
|
110
|
+
if (delta !== 0) return delta;
|
|
111
|
+
if (left.id === right.id) return 0;
|
|
112
|
+
return left.id < right.id ? -1 : 1;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Title normalized once at write: what a title lookup or sort compares. */
|
|
116
|
+
export function normalizedTitle(row) {
|
|
117
|
+
return String(row.title || row.preview || '')
|
|
118
|
+
.normalize('NFKC')
|
|
119
|
+
.toLowerCase()
|
|
120
|
+
.replace(/\s+/g, ' ')
|
|
121
|
+
.trim();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function encodeLoose(value) {
|
|
125
|
+
if (value === null || value === undefined) return null;
|
|
126
|
+
if (typeof value === 'string' || typeof value === 'number') return value;
|
|
127
|
+
return Buffer.from(JSON.stringify(value), 'utf8');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function decodeLoose(value) {
|
|
131
|
+
if (value instanceof Uint8Array) return JSON.parse(Buffer.from(value).toString('utf8'));
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function encodeField(codec, value) {
|
|
136
|
+
switch (codec) {
|
|
137
|
+
case 'bool':
|
|
138
|
+
return value === true ? 1 : 0;
|
|
139
|
+
case 'json':
|
|
140
|
+
case 'optjson':
|
|
141
|
+
return value == null ? null : JSON.stringify(value);
|
|
142
|
+
case 'loose':
|
|
143
|
+
return encodeLoose(value);
|
|
144
|
+
default:
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function decodeField(codec, value) {
|
|
150
|
+
switch (codec) {
|
|
151
|
+
case 'bool':
|
|
152
|
+
return value === 1;
|
|
153
|
+
case 'json':
|
|
154
|
+
case 'optjson':
|
|
155
|
+
return value == null ? null : JSON.parse(value);
|
|
156
|
+
case 'loose':
|
|
157
|
+
return decodeLoose(value);
|
|
158
|
+
default:
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Signature of what one mirror entry writes: equal signature, no write. */
|
|
164
|
+
export function rowSignature({ row, fingerprint, leadVisible }) {
|
|
165
|
+
return createHash('sha1')
|
|
166
|
+
.update(JSON.stringify([fingerprint ?? null, leadVisible === true, row]))
|
|
167
|
+
.digest('base64');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Bind parameters of UPSERT_SQL for one mirror entry. */
|
|
171
|
+
export function upsertParams(entry, signature) {
|
|
172
|
+
const { row } = entry;
|
|
173
|
+
return [
|
|
174
|
+
...FIELDS.map(([key, , codec]) => encodeField(codec, row[key])),
|
|
175
|
+
activityOf(row),
|
|
176
|
+
normalizedTitle(row),
|
|
177
|
+
entry.fingerprint ?? null,
|
|
178
|
+
entry.leadVisible === true ? 1 : 0,
|
|
179
|
+
signature,
|
|
180
|
+
];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** A `sessions` row back in normalizedRow shape. */
|
|
184
|
+
export function rowFromDb(record) {
|
|
185
|
+
const row = {};
|
|
186
|
+
for (const [key, column, codec] of FIELDS) {
|
|
187
|
+
const value = decodeField(codec, record[column]);
|
|
188
|
+
if (codec === 'optjson' && value === null) continue;
|
|
189
|
+
row[key] = value;
|
|
190
|
+
}
|
|
191
|
+
return row;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Overlays a listing adds at read time; never part of the comparison.
|
|
195
|
+
const OVERLAY_KEYS = new Set(['heartbeatAt', 'agentHeartbeatAt']);
|
|
196
|
+
|
|
197
|
+
/** Comparable JSON of a row's stored fields (overlays excluded). */
|
|
198
|
+
function comparable(row) {
|
|
199
|
+
const out = [];
|
|
200
|
+
for (const [key] of FIELDS) {
|
|
201
|
+
if (OVERLAY_KEYS.has(key)) continue;
|
|
202
|
+
out.push(row[key] === undefined ? null : row[key]);
|
|
203
|
+
}
|
|
204
|
+
return JSON.stringify(out);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const FIRST_IDS = 5;
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Shadow comparison of the DB listing against the JSON listing, both in the
|
|
211
|
+
* one catalog order (byActivityThenId) and compared exactly as given: ids
|
|
212
|
+
* present in only one, positions of the common ids, and stored row fields.
|
|
213
|
+
*/
|
|
214
|
+
export function compareListings(json, dbRows) {
|
|
215
|
+
const jsonById = new Map(json.map((row) => [row.id, row]));
|
|
216
|
+
const dbById = new Map(dbRows.map((row) => [row.id, row]));
|
|
217
|
+
const missing = json.filter((row) => !dbById.has(row.id)).map((row) => row.id);
|
|
218
|
+
const extra = dbRows.filter((row) => !jsonById.has(row.id)).map((row) => row.id);
|
|
219
|
+
const commonJson = json.filter((row) => dbById.has(row.id));
|
|
220
|
+
const commonDb = dbRows.filter((row) => jsonById.has(row.id));
|
|
221
|
+
const order = [];
|
|
222
|
+
for (let index = 0; index < commonJson.length; index++) {
|
|
223
|
+
if (commonJson[index].id !== commonDb[index].id) order.push(commonJson[index].id);
|
|
224
|
+
}
|
|
225
|
+
const fields = commonJson.filter((row) => comparable(row) !== comparable(dbById.get(row.id))).map((row) => row.id);
|
|
226
|
+
return {
|
|
227
|
+
json: json.length,
|
|
228
|
+
db: dbRows.length,
|
|
229
|
+
missing: missing.length,
|
|
230
|
+
extra: extra.length,
|
|
231
|
+
order: order.length,
|
|
232
|
+
fields: fields.length,
|
|
233
|
+
ids: {
|
|
234
|
+
missing: missing.slice(0, FIRST_IDS),
|
|
235
|
+
extra: extra.slice(0, FIRST_IDS),
|
|
236
|
+
order: order.slice(0, FIRST_IDS),
|
|
237
|
+
fields: fields.slice(0, FIRST_IDS),
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ── Keyset cursor (activity time, id) ──────────────────────────────────────
|
|
243
|
+
/** Opaque cursor naming the last row of a page. */
|
|
244
|
+
export function encodeCatalogCursor(row) {
|
|
245
|
+
return Buffer.from(JSON.stringify([activityOf(row), row.id]), 'utf8').toString('base64url');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** `{ activity, id }` of a cursor, null for none; throws on a malformed one. */
|
|
249
|
+
export function decodeCatalogCursor(cursor) {
|
|
250
|
+
if (cursor === undefined || cursor === null || cursor === '') return null;
|
|
251
|
+
let parsed;
|
|
252
|
+
try {
|
|
253
|
+
parsed = JSON.parse(Buffer.from(String(cursor), 'base64url').toString('utf8'));
|
|
254
|
+
} catch {
|
|
255
|
+
parsed = null;
|
|
256
|
+
}
|
|
257
|
+
if (!Array.isArray(parsed) || !Number.isFinite(parsed[0]) || typeof parsed[1] !== 'string') {
|
|
258
|
+
throw Object.assign(new Error('invalid session catalog cursor'), { code: 'EINVALIDCURSOR' });
|
|
259
|
+
}
|
|
260
|
+
return { activity: parsed[0], id: parsed[1] };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Whether `row` sorts strictly after the cursor position. */
|
|
264
|
+
export function isAfterCursor(row, after) {
|
|
265
|
+
if (!after) return true;
|
|
266
|
+
const activity = activityOf(row);
|
|
267
|
+
return activity < after.activity || (activity === after.activity && row.id > after.id);
|
|
268
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite runtime gate of the session catalog DB. SQLite 3.7.0–3.51.2 can
|
|
3
|
+
* corrupt a WAL database when the WAL is reset while another connection
|
|
4
|
+
* writes (fixed in 3.51.3, backported to 3.50.7). The catalog opens its file
|
|
5
|
+
* only on a library with that fix; any other runtime keeps the JSON catalog.
|
|
6
|
+
*/
|
|
7
|
+
function parts(version) {
|
|
8
|
+
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(String(version || ''));
|
|
9
|
+
return match ? match.slice(1, 4).map(Number) : null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function atLeast(version, minimum) {
|
|
13
|
+
for (let index = 0; index < 3; index++) {
|
|
14
|
+
if (version[index] !== minimum[index]) return version[index] > minimum[index];
|
|
15
|
+
}
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** True for SQLite ≥ 3.51.3, or 3.50.x ≥ 3.50.7. */
|
|
20
|
+
export function sqliteVersionSupported(version) {
|
|
21
|
+
const value = parts(version);
|
|
22
|
+
if (!value) return false;
|
|
23
|
+
if (atLeast(value, [3, 51, 3])) return true;
|
|
24
|
+
return value[0] === 3 && value[1] === 50 && value[2] >= 7;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const SQLITE_REQUIREMENT = '>= 3.51.3 or 3.50.7+';
|