Bug #1 — quick-start wizard let users skip the LLM provider/API key yet reported "Mosaic is ready". provider-setup now requires an interactive provider select + a validated key; quick-start guards the headless path; finalize won't print "Mosaic is ready" without a configured provider; removed references to the non-existent `mosaic configure` command. Bug #2 — the "local" storage tier still opened ioredis/BullMQ connections at bootstrap (ECONNREFUSED, gateway never healthy). Every Redis consumer (queue.service, gc.module, session-gc.service, commands.module, command-executor.service, system-override.service, cron.service, admin-health.controller) is now tier-aware and degrades gracefully on local tier. Standalone/Federated unaffected. Also fixed a pre-existing SystemOverrideService ioredis handle leak (added shutdown hook). Refs #675 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RMoEx7hfdFGjUiCHuN1RRi
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Inject, Injectable, Logger, Optional, type OnApplicationShutdown } from '@nestjs/common';
|
||||
import { createQueue, type QueueHandle } from '@mosaicstack/queue';
|
||||
import type { MosaicConfig } from '@mosaicstack/config';
|
||||
import { MOSAIC_CONFIG } from '../config/config.module.js';
|
||||
|
||||
const SESSION_SYSTEM_KEY = (sessionId: string) => `mosaic:session:${sessionId}:system`;
|
||||
const SESSION_SYSTEM_FRAGMENTS_KEY = (sessionId: string) =>
|
||||
@@ -11,16 +13,54 @@ interface OverrideFragment {
|
||||
addedAt: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class SystemOverrideService {
|
||||
private readonly logger = new Logger(SystemOverrideService.name);
|
||||
private readonly handle: QueueHandle;
|
||||
interface LocalOverrideEntry {
|
||||
condensed: string;
|
||||
fragments: OverrideFragment[];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.handle = createQueue();
|
||||
@Injectable()
|
||||
export class SystemOverrideService implements OnApplicationShutdown {
|
||||
private readonly logger = new Logger(SystemOverrideService.name);
|
||||
private readonly handle: QueueHandle | null;
|
||||
/**
|
||||
* In-memory fallback used on Local tier (no Redis).
|
||||
* NOTE: state is ephemeral — lost on restart. For Local single-user installs
|
||||
* this is acceptable; system overrides are re-applied at the next session.
|
||||
* This is a deliberate behavior change from the Redis-backed 7-day TTL.
|
||||
*/
|
||||
private readonly localStore = new Map<string, LocalOverrideEntry>();
|
||||
|
||||
constructor(
|
||||
@Optional()
|
||||
@Inject(MOSAIC_CONFIG)
|
||||
private readonly mosaicConfig: MosaicConfig | null,
|
||||
) {
|
||||
if (this.mosaicConfig?.queue?.type === 'local') {
|
||||
this.handle = null;
|
||||
} else {
|
||||
this.handle = createQueue();
|
||||
}
|
||||
}
|
||||
|
||||
async onApplicationShutdown(): Promise<void> {
|
||||
// On non-local tiers the constructor opens an ioredis connection; close it
|
||||
// on graceful shutdown to avoid leaking the handle (local tier is null).
|
||||
await this.handle?.close().catch(() => {});
|
||||
}
|
||||
|
||||
async set(sessionId: string, override: string): Promise<void> {
|
||||
if (!this.handle) {
|
||||
// Local tier: in-memory path
|
||||
const entry = this.localStore.get(sessionId) ?? { condensed: '', fragments: [] };
|
||||
entry.fragments.push({ text: override, addedAt: Date.now() });
|
||||
entry.condensed = await this.condenseOverrides(entry.fragments.map((f) => f.text));
|
||||
this.localStore.set(sessionId, entry);
|
||||
this.logger.debug(
|
||||
`Set system override for session ${sessionId} (local, ${entry.fragments.length} fragment(s))`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Load existing fragments
|
||||
const existing = await this.handle.redis.get(SESSION_SYSTEM_FRAGMENTS_KEY(sessionId));
|
||||
const fragments: OverrideFragment[] = existing
|
||||
@@ -50,10 +90,17 @@ export class SystemOverrideService {
|
||||
}
|
||||
|
||||
async get(sessionId: string): Promise<string | null> {
|
||||
if (!this.handle) {
|
||||
return this.localStore.get(sessionId)?.condensed ?? null;
|
||||
}
|
||||
return this.handle.redis.get(SESSION_SYSTEM_KEY(sessionId));
|
||||
}
|
||||
|
||||
async renew(sessionId: string): Promise<void> {
|
||||
if (!this.handle) {
|
||||
// Local tier: no TTL to renew; entry persists until restart
|
||||
return;
|
||||
}
|
||||
const pipeline = this.handle.redis.pipeline();
|
||||
pipeline.expire(SESSION_SYSTEM_KEY(sessionId), SYSTEM_OVERRIDE_TTL_SECONDS);
|
||||
pipeline.expire(SESSION_SYSTEM_FRAGMENTS_KEY(sessionId), SYSTEM_OVERRIDE_TTL_SECONDS);
|
||||
@@ -61,6 +108,11 @@ export class SystemOverrideService {
|
||||
}
|
||||
|
||||
async clear(sessionId: string): Promise<void> {
|
||||
if (!this.handle) {
|
||||
this.localStore.delete(sessionId);
|
||||
this.logger.debug(`Cleared system override for session ${sessionId} (local)`);
|
||||
return;
|
||||
}
|
||||
await this.handle.redis.del(
|
||||
SESSION_SYSTEM_KEY(sessionId),
|
||||
SESSION_SYSTEM_FRAGMENTS_KEY(sessionId),
|
||||
|
||||
Reference in New Issue
Block a user