Add MindSpace service runtime release chain

This commit is contained in:
john
2026-07-03 09:29:00 +08:00
parent ec5b1a6e3f
commit 7005c2a834
13 changed files with 1261 additions and 1 deletions
+20
View File
@@ -0,0 +1,20 @@
# MindSpace Service
Standalone MindSpace service source tracked inside the Memind repo.
This service is deployed to production as a separate runtime artifact under
`/Users/john/MindSpace` and exposes the shared adapter RPC contract that Memind
can call through `MINDSPACE_SERVER_ADAPTER=remote`.
Production defaults:
- MindSpace service listens on `127.0.0.1:8082`
- Memind remains on `127.0.0.1:8081`
- Runtime code lives at `/Users/john/MindSpace`
- Runtime data continues to use the existing persisted Memind directories:
`MindSpace/`, `data/mindspace/`, and `users/`
Key scripts in this repo:
- `node scripts/build-mindspace-service-runtime.mjs`
- `bash scripts/release-mindspace-service-prod.sh --yes`
+160
View File
@@ -0,0 +1,160 @@
import http from 'node:http';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
async function loadContract(env = process.env) {
const memindRoot = path.resolve(__dirname, env.MINDSPACE_MEMIND_ROOT ?? '../memind-source');
return import(
pathToFileURL(path.join(memindRoot, 'mindspace-server-adapter-contract.mjs')).href
);
}
function parseBearerToken(headers) {
const auth = String(headers.authorization ?? '');
if (!auth.startsWith('Bearer ')) return '';
return auth.slice('Bearer '.length).trim();
}
async function readJsonBody(req) {
const chunks = [];
for await (const chunk of req) {
chunks.push(chunk);
}
const text = Buffer.concat(chunks).toString('utf8').trim();
return text ? JSON.parse(text) : {};
}
function reviveRpcValue(value) {
if (
value &&
typeof value === 'object' &&
value.type === 'Buffer' &&
Array.isArray(value.data)
) {
return Buffer.from(value.data);
}
if (Array.isArray(value)) return value.map((item) => reviveRpcValue(item));
if (value && typeof value === 'object') {
return Object.fromEntries(
Object.entries(value).map(([key, item]) => [key, reviveRpcValue(item)]),
);
}
return value;
}
function json(res, statusCode, payload) {
const body = JSON.stringify(payload);
res.writeHead(statusCode, {
'Content-Type': 'application/json; charset=utf-8',
'Content-Length': Buffer.byteLength(body),
});
res.end(body);
}
export async function createMindSpaceRpcRequestHandler({
adapter,
env = process.env,
logger = console,
serviceMeta = {},
} = {}) {
const {
MINDSPACE_SERVER_ADAPTER_BINDINGS,
MINDSPACE_SERVER_ADAPTER_BINDING_KEYS,
} = await loadContract(env);
const operationBasePath = String(
serviceMeta.operationBasePath ?? env.MINDSPACE_REMOTE_OPERATION_BASE_PATH ?? '/mindspace/v1/adapter',
)
.trim()
.replace(/\/+$/, '') || '/mindspace/v1/adapter';
const authToken = String(env.MINDSPACE_REMOTE_AUTH_TOKEN ?? '').trim();
return async function handleMindSpaceRpc(req, res) {
try {
const url = new URL(req.url, 'http://127.0.0.1');
if (req.method === 'GET' && url.pathname === '/health') {
return json(res, 200, {
ok: true,
service: 'mindspace-service',
adapterKind: adapter.kind,
implementationStatus: adapter.implementationStatus,
operationBasePath,
backgroundJobsManagedLocally: adapter.kind === 'local',
});
}
if (req.method === 'GET' && url.pathname === '/mindspace/v1/contract') {
return json(res, 200, {
bindings: MINDSPACE_SERVER_ADAPTER_BINDINGS,
bindingKeys: MINDSPACE_SERVER_ADAPTER_BINDING_KEYS,
});
}
if (req.method !== 'POST' || !url.pathname.startsWith(`${operationBasePath}/`)) {
return json(res, 404, { message: 'Not found' });
}
if (authToken && parseBearerToken(req.headers) !== authToken) {
return json(res, 401, { message: 'Unauthorized' });
}
const segments = url.pathname.slice(operationBasePath.length + 1).split('/').filter(Boolean);
if (segments.length !== 2) {
return json(res, 400, { message: 'Expected /:binding/:method' });
}
const [bindingKey, method] = segments.map(decodeURIComponent);
const service = adapter?.[bindingKey];
const allowedMethods = MINDSPACE_SERVER_ADAPTER_BINDINGS[bindingKey];
if (!service || !allowedMethods) {
return json(res, 404, { message: `Unknown binding: ${bindingKey}` });
}
if (!allowedMethods.includes(method)) {
return json(res, 404, { message: `Unknown method: ${bindingKey}.${method}` });
}
const body = await readJsonBody(req);
const args = Array.isArray(body?.args) ? reviveRpcValue(body.args) : [];
const result = await service[method](...args);
return json(res, 200, result);
} catch (error) {
logger.error?.('[MindSpace RPC Error]', error);
return json(res, 500, {
message: error instanceof Error ? error.message : String(error),
});
}
};
}
export async function startMindSpaceRpcServer({
adapter,
env = process.env,
logger = console,
serviceMeta = {},
} = {}) {
const handler = await createMindSpaceRpcRequestHandler({
adapter,
env,
logger,
serviceMeta,
});
const host = env.MINDSPACE_SERVICE_HOST ?? '127.0.0.1';
const port = Number(env.MINDSPACE_SERVICE_PORT ?? 8082);
const server = http.createServer((req, res) => {
void handler(req, res);
});
await new Promise((resolve, reject) => {
server.once('error', reject);
server.listen(port, host, () => resolve());
});
return {
server,
host,
port,
close: () =>
new Promise((resolve, reject) => {
server.close((error) => {
if (error) reject(error);
else resolve();
});
}),
};
}
@@ -0,0 +1,198 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { createMindSpaceRpcRequestHandler } from './mindspace-rpc-server.mjs';
function createStubAdapter() {
const calls = {
writeUploadContent: [],
};
return {
calls,
kind: 'local',
implementationStatus: 'ready',
serviceFacade: {
async prepareConversationPackageRead() {
return [];
},
},
conversationPackageRegistry: {
async ensurePackage() { return { ok: true }; },
async putObjectForSession() { return { ok: true }; },
async recordArtifact() { return { ok: true }; },
async readArtifactObject() { return { ok: true }; },
async readManifestForSession() { return { ok: true }; },
async writeManifestForSession() { return { ok: true }; },
},
assetService: {
async cancelUpload() { return { ok: true }; },
async claimUploadArtifactsForConversation() { return []; },
async completeUpload() { return { ok: true }; },
async createChatAsset() { return { ok: true }; },
async createUpload() { return { ok: true }; },
async deleteAsset() { return { ok: true }; },
async listAssets() { return []; },
async readAsset() { return { ok: true }; },
async readPublicAsset() { return { ok: true }; },
async renderAssetPreview() { return { preview: true }; },
async renderAssetThumbnail() { return { thumbnail: true }; },
async syncWorkspaceAssets() { return { synced: true }; },
async writeUploadContent(...args) {
calls.writeUploadContent.push(args);
return { isBuffer: Buffer.isBuffer(args[2]), size: args[2]?.length ?? 0 };
},
async expireStaleUploads() { return { expired: 0 }; },
},
pageService: {
async createFromChat() { return { ok: true }; },
async createPage() { return { ok: true }; },
async deletePage() { return { ok: true }; },
async findPageByRelativePath() { return null; },
async findPageBySourceAsset() { return null; },
async getDeletePreview() { return { ok: true }; },
async getPage() { return { ok: true }; },
async listPages() { return []; },
async listVersions() { return []; },
async localizePrivateResources() { return { ok: true }; },
async redactPage() { return { ok: true }; },
async regenerateThumbnail() { return { ok: true }; },
async renderDraftPreview() { return { html: '' }; },
async renderPreview() { return { html: '' }; },
async renderThumbnail() { return { ok: true }; },
async rewriteHtmlDownloadLinksForPage() { return { ok: true }; },
async updatePage() { return { ok: true }; },
async uploadThumbnail() { return { ok: true }; },
},
pageLiveEditService: {
async applyAgentPatch() { return { ok: true }; },
async bindSession() { return { ok: true }; },
async getRevisionSnapshot() { return { ok: true }; },
},
assetAgentService: {
async applyAgentDelete() { return { ok: true }; },
},
publicationService: {
async check() { return { ok: true }; },
async cleanupExpiredUnconfirmedPublications() { return { cleaned: 0 }; },
async getCurrent() { return null; },
async getPublicHomepage() { return null; },
async getStats() { return { ok: true }; },
async offline() { return { ok: true }; },
async publish() { return { ok: true }; },
async resolvePrivateLink() { return { ok: true }; },
async resolvePublic() { return { ok: true }; },
async updatePublicationStatus() { return { ok: true }; },
},
cleanupService: {
async listCandidates() { return []; },
async runCleanup() { return { ok: true }; },
},
agentJobService: {
async cancelJob() { return { ok: true }; },
async claimJob() { return { ok: true }; },
async claimNextJob() { return null; },
async completeJob() { return { ok: true }; },
async createJob() { return { ok: true }; },
async getAssetForJob() { return { ok: true }; },
async getJob() { return { ok: true }; },
async heartbeat() { return { ok: true }; },
async listJobs() { return []; },
async reapStaleJobs() { return 0; },
async retryJob() { return { ok: true }; },
},
};
}
async function runRequest(handler, { method = 'GET', path = '/', headers = {}, body = '' } = {}) {
const chunks = [];
const req = {
method,
url: path,
headers,
async *[Symbol.asyncIterator]() {
if (body) yield Buffer.from(body);
},
};
const res = {
statusCode: 0,
headers: {},
writeHead(statusCode, responseHeaders) {
this.statusCode = statusCode;
this.headers = responseHeaders;
},
end(chunk) {
if (chunk) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
},
};
await handler(req, res);
return {
statusCode: res.statusCode,
headers: res.headers,
body: chunks.length ? JSON.parse(Buffer.concat(chunks).toString('utf8')) : null,
};
}
test('health and contract endpoints are exposed without auth', async () => {
const handler = await createMindSpaceRpcRequestHandler({
adapter: createStubAdapter(),
env: {
MINDSPACE_MEMIND_ROOT: '..',
},
});
const health = await runRequest(handler, { path: '/health' });
const contract = await runRequest(handler, { path: '/mindspace/v1/contract' });
assert.equal(health.statusCode, 200);
assert.equal(health.body.ok, true);
assert.equal(contract.statusCode, 200);
assert.ok(contract.body.bindings.assetService.includes('renderAssetPreview'));
});
test('rpc invocation enforces auth token and dispatches args to adapter method', async () => {
const handler = await createMindSpaceRpcRequestHandler({
adapter: createStubAdapter(),
env: {
MINDSPACE_MEMIND_ROOT: '..',
MINDSPACE_REMOTE_AUTH_TOKEN: 'secret-token',
},
});
const unauthorized = await runRequest(handler, {
method: 'POST',
path: '/mindspace/v1/adapter/assetService/renderAssetPreview',
body: JSON.stringify({ args: [{ assetId: 'a1' }] }),
});
const authorized = await runRequest(handler, {
method: 'POST',
path: '/mindspace/v1/adapter/assetService/renderAssetPreview',
headers: {
authorization: 'Bearer secret-token',
},
body: JSON.stringify({ args: [{ assetId: 'a1' }] }),
});
assert.equal(unauthorized.statusCode, 401);
assert.equal(authorized.statusCode, 200);
assert.deepEqual(authorized.body, { preview: true });
});
test('rpc invocation revives JSON-serialized buffers before dispatch', async () => {
const adapter = createStubAdapter();
const handler = await createMindSpaceRpcRequestHandler({
adapter,
env: {
MINDSPACE_MEMIND_ROOT: '..',
},
});
const payload = Buffer.from('asset parity payload');
const response = await runRequest(handler, {
method: 'POST',
path: '/mindspace/v1/adapter/assetService/writeUploadContent',
body: JSON.stringify({ args: ['user-1', 'upload-1', payload] }),
});
assert.equal(response.statusCode, 200);
assert.deepEqual(response.body, { isBuffer: true, size: payload.length });
assert.equal(Buffer.isBuffer(adapter.calls.writeUploadContent[0][2]), true);
});
@@ -0,0 +1,237 @@
import path from 'node:path';
import fs from 'node:fs';
import { fileURLToPath, pathToFileURL } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
function resolveMemindRoot(env = process.env) {
return path.resolve(__dirname, env.MINDSPACE_MEMIND_ROOT ?? '../memind-source');
}
function resolveStandaloneDataRoot(env = process.env) {
return path.resolve(env.MINDSPACE_SERVICE_H5_ROOT ?? path.join(__dirname, 'runtime'));
}
function ensureStandaloneRuntimeLayout(standaloneDataRoot) {
fs.mkdirSync(standaloneDataRoot, { recursive: true });
fs.mkdirSync(path.join(standaloneDataRoot, 'MindSpace'), { recursive: true });
fs.mkdirSync(path.join(standaloneDataRoot, 'data'), { recursive: true });
fs.mkdirSync(path.join(standaloneDataRoot, 'users'), { recursive: true });
}
function loadDotenvFile(targetEnv, filepath) {
if (!fs.existsSync(filepath) || !fs.statSync(filepath).isFile()) return;
const content = fs.readFileSync(filepath, 'utf8');
for (const rawLine of content.split(/\r?\n/)) {
const line = rawLine.trim();
if (!line || line.startsWith('#') || !line.includes('=')) continue;
const [rawKey, ...rawValueParts] = line.split('=');
const key = rawKey.trim();
if (!key || key in targetEnv) continue;
const rawValue = rawValueParts.join('=').trim();
const unwrapped =
(rawValue.startsWith('"') && rawValue.endsWith('"')) ||
(rawValue.startsWith("'") && rawValue.endsWith("'"))
? rawValue.slice(1, -1)
: rawValue;
targetEnv[key] = unwrapped;
}
}
function applyMemindEnvDefaults(memindRoot, env = process.env) {
loadDotenvFile(env, path.join(memindRoot, '.env'));
loadDotenvFile(env, path.join(memindRoot, '.env.local'));
return env;
}
async function importFromMemindRoot(memindRoot, relativePath) {
return import(pathToFileURL(path.join(memindRoot, relativePath)).href);
}
async function loadMemindModules(memindRoot) {
const [
db,
config,
conversationMemory,
localServerAdapter,
publicHtmlHelpers,
runtimeConfig,
sessionSnapshot,
userAuth,
agentRunner,
experienceService,
workspaceThumbnails,
workspaceSync,
] = await Promise.all([
importFromMemindRoot(memindRoot, 'db.mjs'),
importFromMemindRoot(memindRoot, 'mindspace-config.mjs'),
importFromMemindRoot(memindRoot, 'conversation-memory.mjs'),
importFromMemindRoot(memindRoot, 'mindspace-local-server-adapter.mjs'),
importFromMemindRoot(memindRoot, 'mindspace-conversation-package-public-html.mjs'),
importFromMemindRoot(memindRoot, 'mindspace-runtime-config.mjs'),
importFromMemindRoot(memindRoot, 'session-snapshot.mjs'),
importFromMemindRoot(memindRoot, 'user-auth.mjs'),
importFromMemindRoot(memindRoot, 'mindspace-agent-runner.mjs'),
importFromMemindRoot(memindRoot, 'experience-service.mjs'),
importFromMemindRoot(memindRoot, 'mindspace-workspace-thumbnails.mjs'),
importFromMemindRoot(memindRoot, 'mindspace-workspace-sync.mjs'),
]);
return {
...db,
...config,
...conversationMemory,
...localServerAdapter,
...publicHtmlHelpers,
...runtimeConfig,
...sessionSnapshot,
...userAuth,
...agentRunner,
...experienceService,
...workspaceThumbnails,
...workspaceSync,
};
}
function parseApiTargets(env = process.env) {
const csvTargets = String(env.TKMIND_API_TARGETS ?? '')
.split(',')
.map((value) => value.trim())
.filter(Boolean);
const legacyTargets = [env.TKMIND_API_TARGET ?? 'https://127.0.0.1:18006', env.TKMIND_API_TARGET_1].filter(
Boolean,
);
return [...new Set([...csvTargets, ...legacyTargets])];
}
async function resolveUserIdForAgentSession(pool, sessionId) {
const [rows] = await pool.query(
`SELECT user_id FROM h5_user_sessions WHERE agent_session_id = ? LIMIT 1`,
[sessionId],
);
return rows[0]?.user_id ?? null;
}
async function resolveUserIdByDirKey(pool, dirKey) {
const key = String(dirKey ?? '').trim();
if (!key) return null;
const [rows] = await pool.query(`SELECT id FROM h5_users WHERE username = ? LIMIT 1`, [key]);
return rows[0]?.id ?? key;
}
export async function bootstrapMindSpaceService({
env = process.env,
logger = console,
} = {}) {
const memindRoot = resolveMemindRoot(env);
applyMemindEnvDefaults(memindRoot, env);
const standaloneDataRoot = resolveStandaloneDataRoot(env);
ensureStandaloneRuntimeLayout(standaloneDataRoot);
const {
createDbPool,
initSchema,
isDatabaseConfigured,
ensureMindSpaceConfig,
createConversationMemoryService,
createMindSpaceLocalServerAdapter,
createMindSpaceAgentRunner,
createSessionSnapshotService,
createUserAuth,
createExperienceService,
registerPublicHtmlArtifactsForConversationPackage,
resolveMindSpaceServerRuntimeOptions,
startWorkspaceAssetSyncWatcher,
startWorkspaceThumbnailWatcher,
} = await loadMemindModules(memindRoot);
if (!isDatabaseConfigured()) {
throw new Error('MindSpace service requires DATABASE_URL or MYSQL_* configuration');
}
const runtime = resolveMindSpaceServerRuntimeOptions(standaloneDataRoot, env);
const pool = createDbPool();
await initSchema(pool);
await ensureMindSpaceConfig(pool, { env });
const conversationMemoryService = createConversationMemoryService(pool);
const sessionSnapshotService = createSessionSnapshotService(pool, {
conversationMemoryService,
});
const usersRoot = path.resolve(env.H5_USERS_ROOT ?? path.join(standaloneDataRoot, 'users'));
const userAuth = createUserAuth(pool, {
usersRoot,
h5Root: standaloneDataRoot,
env,
defaultSignupBalanceCents: Number(env.H5_SIGNUP_BALANCE_CENTS ?? 500),
});
const adapter = createMindSpaceLocalServerAdapter({
pool,
h5Root: standaloneDataRoot,
env,
maxFileBytes: runtime.maxFileBytes,
publicPageLimit: runtime.publicPageLimit,
resolveUserIdForAgentSession: (sessionId) => resolveUserIdForAgentSession(pool, sessionId),
resolveSessionSnapshot: (sessionId) => sessionSnapshotService.get(sessionId),
registerPublicHtmlArtifactsForConversation: (payload) =>
registerPublicHtmlArtifactsForConversationPackage({
conversationPackageRegistry: adapter.conversationPackageRegistry,
h5Root: standaloneDataRoot,
env,
...payload,
}),
logger,
});
const apiTargets = parseApiTargets(env);
const apiTarget = apiTargets[0] ?? 'https://127.0.0.1:18006';
const apiSecret = env.TKMIND_SERVER__SECRET_KEY ?? 'local-dev-secret';
let experienceService = null;
if (runtime.experienceEnabled) {
experienceService = createExperienceService(pool);
}
const agentRunner = createMindSpaceAgentRunner({
apiTarget,
apiSecret,
userAuth,
agentJobService: adapter.agentJobService,
experienceService,
});
const standaloneAgentWorkerEnabled =
env.MINDSPACE_SERVICE_AGENT_WORKER_ENABLED === '1' ||
env.MINDSPACE_SERVICE_AGENT_WORKER_ENABLED === 'true' ||
(env.MINDSPACE_SERVICE_AGENT_WORKER_ENABLED == null && runtime.agentWorker.enabled);
const backgroundJobs = adapter.startBackgroundJobs({
publicationCleanupIntervalMs: 60 * 1000,
agentWorker: {
...runtime.agentWorker,
enabled: standaloneAgentWorkerEnabled,
},
agentRunner,
workspaceMaintenanceEnabled: env.MINDSPACE_SERVICE_WORKSPACE_MAINTENANCE === 'true',
publishRoot: runtime.publishRoot,
startWorkspaceThumbnailWatcher,
startWorkspaceAssetSyncWatcher,
syncUserWorkspaceByDirKey: async (dirKey, options) => {
const userId = await resolveUserIdByDirKey(pool, dirKey);
if (!userId) return;
await adapter.assetService.syncWorkspaceAssets(userId, options);
},
expireStaleUploadsIntervalMs: 5 * 60 * 1000,
});
return {
memindRoot,
standaloneDataRoot,
runtime,
pool,
conversationMemoryService,
sessionSnapshotService,
userAuth,
agentRunner,
adapter,
backgroundJobs,
async close() {
await pool.end();
},
};
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "mindspace-service",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"start": "node server.mjs",
"test": "node --test mindspace-rpc-server.test.mjs"
}
}
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
MEMIND_ROOT="${MEMIND_SHARED_ROOT:-/Users/john/Project/Memind}"
if [[ -f "${MEMIND_ROOT}/.env" ]]; then
set -a
# shellcheck disable=SC1090
source "${MEMIND_ROOT}/.env"
set +a
fi
export NODE_ENV=production
export MINDSPACE_MEMIND_ROOT="${MINDSPACE_MEMIND_ROOT:-${ROOT}/memind-source}"
export MINDSPACE_SERVICE_H5_ROOT="${MINDSPACE_SERVICE_H5_ROOT:-${MEMIND_ROOT}}"
export MINDSPACE_STORAGE_ROOT="${MINDSPACE_STORAGE_ROOT:-${MEMIND_ROOT}/data/mindspace}"
export H5_USERS_ROOT="${H5_USERS_ROOT:-${MEMIND_ROOT}/users}"
export MINDSPACE_SERVICE_PORT="${MINDSPACE_SERVICE_PORT:-8082}"
export MINDSPACE_SERVICE_HOST="${MINDSPACE_SERVICE_HOST:-127.0.0.1}"
export MINDSPACE_REMOTE_AUTH_TOKEN="${MINDSPACE_REMOTE_AUTH_TOKEN:-${TKMIND_SERVER__SECRET_KEY:-}}"
export MINDSPACE_SERVICE_AGENT_WORKER_ENABLED="${MINDSPACE_SERVICE_AGENT_WORKER_ENABLED:-true}"
export MINDSPACE_SERVICE_WORKSPACE_MAINTENANCE="${MINDSPACE_SERVICE_WORKSPACE_MAINTENANCE:-true}"
NODE_BIN="${NODE_BIN:-/opt/homebrew/opt/node@24/bin/node}"
if [[ ! -x "${NODE_BIN}" ]]; then
NODE_BIN="$(command -v node)"
fi
export NODE_PATH="${ROOT}/node_modules"
exec "${NODE_BIN}" "${ROOT}/server.mjs"
+42
View File
@@ -0,0 +1,42 @@
import { bootstrapMindSpaceService } from './mindspace-service-bootstrap.mjs';
import { startMindSpaceRpcServer } from './mindspace-rpc-server.mjs';
const logger = console;
async function main() {
const service = await bootstrapMindSpaceService({ env: process.env, logger });
const rpc = await startMindSpaceRpcServer({
adapter: service.adapter,
env: process.env,
logger,
serviceMeta: {
operationBasePath: service.runtime.remote.operationBasePath,
},
});
logger.log(
`MindSpace service listening on http://${rpc.host}:${rpc.port}${service.runtime.remote.operationBasePath}`,
);
logger.log(`Memind root: ${service.memindRoot}`);
logger.log(`Standalone data root: ${service.standaloneDataRoot}`);
logger.log(`Standalone agent worker: ${service.backgroundJobs.agentWorker ? 'enabled' : 'disabled'}`);
const shutdown = async (signal) => {
logger.log(`Received ${signal}, shutting down MindSpace service...`);
await rpc.close().catch(() => {});
await service.close().catch(() => {});
process.exit(0);
};
process.on('SIGINT', () => {
void shutdown('SIGINT');
});
process.on('SIGTERM', () => {
void shutdown('SIGTERM');
});
}
main().catch((error) => {
console.error('MindSpace service failed to start:', error);
process.exit(1);
});