@mercury-fw/core 0.25.0 → 0.25.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @mercury-fw/core
2
2
 
3
+ ## 0.25.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 5613e5e: - Mercury starts even when Qdrant isn't answering yet: the memory collections are set up in the background and retried until Qdrant is reachable, instead of crashing the process at startup.
8
+ - A new session's context primer goes on without the last-session recap when Qdrant doesn't answer, instead of failing the turn.
9
+ - The scaffolded app's `docker-compose.yml` restarts the app service unless it was stopped.
10
+ - @mercury-fw/plugin-types@0.25.1
11
+ - @mercury-fw/channel-types@0.25.1
12
+ - @mercury-fw/cli-engine@0.25.1
13
+ - @mercury-fw/confirm-engine@0.25.1
14
+
3
15
  ## 0.25.0
4
16
 
5
17
  ### Minor Changes
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Layer 3's startup setup (creating the Qdrant collections and their indexes),
3
+ * run in the background so an unreachable Qdrant never stops Mercury from
4
+ * starting: the episodic store is enrichment and fails soft (principle 3).
5
+ */
6
+ /**
7
+ * Runs `setup` now and, while it fails, again every `retryMs`, on a timer that
8
+ * doesn't keep the process alive. Logs once when Qdrant goes missing and once
9
+ * when it's back, never per retry. `done` resolves after the first success.
10
+ */
11
+ export declare function setUpWhenReachable(setup: () => Promise<void>, { log, retryMs }: {
12
+ log: (msg: string) => void;
13
+ retryMs?: number;
14
+ }): {
15
+ done: Promise<void>;
16
+ };
@@ -7,6 +7,8 @@ export type ContextPrimerDeps = {
7
7
  listWikiFilesInRootsFn: typeof listWikiFilesInRoots;
8
8
  readWikiFileInRootsFn: typeof readWikiFileInRoots;
9
9
  readIndexFileFn: typeof readIndexFile;
10
+ /** Where a skipped section says why (the recap, when Qdrant doesn't answer). */
11
+ log: (msg: string) => void;
10
12
  };
11
13
  /**
12
14
  * Text of the primer for `userId`, built from injected deps only — never
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mercury-fw/core",
3
- "version": "0.25.0",
3
+ "version": "0.25.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -31,10 +31,10 @@
31
31
  "test": "bun test"
32
32
  },
33
33
  "dependencies": {
34
- "@mercury-fw/channel-types": "0.25.0",
35
- "@mercury-fw/cli-engine": "0.25.0",
36
- "@mercury-fw/confirm-engine": "0.25.0",
37
- "@mercury-fw/plugin-types": "0.25.0",
34
+ "@mercury-fw/channel-types": "0.25.1",
35
+ "@mercury-fw/cli-engine": "0.25.1",
36
+ "@mercury-fw/confirm-engine": "0.25.1",
37
+ "@mercury-fw/plugin-types": "0.25.1",
38
38
  "@qdrant/js-client-rest": "^1.19.0",
39
39
  "ai": "^7.0.77",
40
40
  "ai-sdk-ollama": "^4.2.0",
package/src/compose.ts CHANGED
@@ -56,6 +56,7 @@ import { ensureVerbatimCollection, listVerbatimBySession, listVerbatimSessions }
56
56
  import { createVerbatimArchiveProvider } from "./memory/memory-provider.ts";
57
57
  import { ensureSemanticFactsCollection, storeSemanticFact, searchSemanticFactsByTopic } from "./memory/semantic-facts-store.ts";
58
58
  import { ensureToolCorrectionsCollection, storeToolCorrection, searchToolCorrectionsByTopic } from "./memory/tool-corrections-store.ts";
59
+ import { setUpWhenReachable } from "./memory/collection-setup.ts";
59
60
  import { consolidateSemanticFact, consolidateToolCorrection, type ToolCorrectionConsolidationDeps } from "./cron/semantic-consolidation.ts";
60
61
  import { createToolCorrectionExtractor } from "./session/tool-correction-extractor.ts";
61
62
  import { createEmbedder } from "./memory/embedder.ts";
@@ -227,14 +228,12 @@ export async function composeMercury(config: MercuryConfig): Promise<ComposedApp
227
228
  const qdrant = new QdrantClient({ url: process.env.QDRANT_URL ?? "http://qdrant:6333" });
228
229
  const episodicCollection = process.env.QDRANT_EPISODIC_COLLECTION ?? "episodic_memory";
229
230
  const episodicVectorSize = Number(process.env.QDRANT_EPISODIC_VECTOR_SIZE ?? "768");
230
- await ensureEpisodicCollection(qdrant, episodicCollection, episodicVectorSize);
231
231
 
232
232
  // Verbatim conversation archive (#4): a distinct collection holding the raw
233
233
  // user<->model exchange, lossless and durable — separate from the lossy
234
234
  // Layer-1 window and the derived episodic summaries above.
235
235
  const verbatimCollection = process.env.QDRANT_VERBATIM_COLLECTION ?? "verbatim_archive";
236
236
  const verbatimVectorSize = Number(process.env.QDRANT_VERBATIM_VECTOR_SIZE ?? "768");
237
- await ensureVerbatimCollection(qdrant, verbatimCollection, verbatimVectorSize);
238
237
  const verbatimProvider = createVerbatimArchiveProvider({ client: qdrant, collectionName: verbatimCollection, embed });
239
238
 
240
239
  // Semantic consolidation (D-22/D-34): a separate Qdrant collection from
@@ -242,7 +241,6 @@ export async function composeMercury(config: MercuryConfig): Promise<ComposedApp
242
241
  // vector embedded on the topic alone (see semantic-facts-store.ts for why).
243
242
  const semanticFactsCollection = process.env.QDRANT_SEMANTIC_FACTS_COLLECTION ?? "semantic_facts";
244
243
  const semanticFactsVectorSize = Number(process.env.QDRANT_SEMANTIC_FACTS_VECTOR_SIZE ?? "768");
245
- await ensureSemanticFactsCollection(qdrant, semanticFactsCollection, semanticFactsVectorSize);
246
244
  const extractFacts = createSemanticFactExtractor(model);
247
245
 
248
246
  // Idempotent self-heal: the vault lives on a named Docker volume, empty on
@@ -322,7 +320,17 @@ export async function composeMercury(config: MercuryConfig): Promise<ComposedApp
322
320
  // the tool-call trace only exists in memory for the duration of its turn.
323
321
  const toolCorrectionsCollection = process.env.QDRANT_TOOL_CORRECTIONS_COLLECTION ?? "tool_corrections";
324
322
  const toolCorrectionsVectorSize = Number(process.env.QDRANT_TOOL_CORRECTIONS_VECTOR_SIZE ?? "768");
325
- await ensureToolCorrectionsCollection(qdrant, toolCorrectionsCollection, toolCorrectionsVectorSize);
323
+ // Every Layer-3 collection, set up in the background: Mercury starts even
324
+ // while Qdrant isn't answering yet, and memory switches on once it does.
325
+ setUpWhenReachable(
326
+ async () => {
327
+ await ensureEpisodicCollection(qdrant, episodicCollection, episodicVectorSize);
328
+ await ensureVerbatimCollection(qdrant, verbatimCollection, verbatimVectorSize);
329
+ await ensureSemanticFactsCollection(qdrant, semanticFactsCollection, semanticFactsVectorSize);
330
+ await ensureToolCorrectionsCollection(qdrant, toolCorrectionsCollection, toolCorrectionsVectorSize);
331
+ },
332
+ { log: (msg) => console.error(`[memory] ${msg}`) },
333
+ );
326
334
  const extractToolCorrections = createToolCorrectionExtractor(model, undefined, {
327
335
  log: (msg) => console.error(`[cron] ${msg}`),
328
336
  });
@@ -464,6 +472,7 @@ export async function composeMercury(config: MercuryConfig): Promise<ComposedApp
464
472
  listWikiFilesInRootsFn: listWikiFilesInRoots,
465
473
  readWikiFileInRootsFn: readWikiFileInRoots,
466
474
  readIndexFileFn: readIndexFile,
475
+ log: (msg) => console.error(`[memory] ${msg}`),
467
476
  });
468
477
  return getOrCreateHistory(key, trackForCapture, primer);
469
478
  }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Layer 3's startup setup (creating the Qdrant collections and their indexes),
3
+ * run in the background so an unreachable Qdrant never stops Mercury from
4
+ * starting: the episodic store is enrichment and fails soft (principle 3).
5
+ */
6
+
7
+ /**
8
+ * Runs `setup` now and, while it fails, again every `retryMs`, on a timer that
9
+ * doesn't keep the process alive. Logs once when Qdrant goes missing and once
10
+ * when it's back, never per retry. `done` resolves after the first success.
11
+ */
12
+ export function setUpWhenReachable(
13
+ setup: () => Promise<void>,
14
+ { log, retryMs = 5000 }: { log: (msg: string) => void; retryMs?: number },
15
+ ): { done: Promise<void> } {
16
+ let failing = false;
17
+ const done = new Promise<void>((resolve) => {
18
+ const attempt = async () => {
19
+ try {
20
+ await setup();
21
+ if (failing) log("Qdrant reachable, memory collections ready");
22
+ resolve();
23
+ } catch (err) {
24
+ if (!failing) {
25
+ log(`Qdrant unreachable, Layer-3 memory is off until it answers (retrying every ${retryMs / 1000}s): ${String(err)}`);
26
+ }
27
+ failing = true;
28
+ setTimeout(attempt, retryMs).unref();
29
+ }
30
+ };
31
+ void attempt();
32
+ });
33
+ return { done };
34
+ }
@@ -21,6 +21,8 @@ export type ContextPrimerDeps = {
21
21
  listWikiFilesInRootsFn: typeof listWikiFilesInRoots;
22
22
  readWikiFileInRootsFn: typeof readWikiFileInRoots;
23
23
  readIndexFileFn: typeof readIndexFile;
24
+ /** Where a skipped section says why (the recap, when Qdrant doesn't answer). */
25
+ log: (msg: string) => void;
24
26
  };
25
27
 
26
28
  const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n/;
@@ -78,7 +80,12 @@ async function pendingConfirmationTokens(userId: string, deps: ContextPrimerDeps
78
80
  * `index.ts` supplies the real Qdrant-backed episodic query and wiki reads.
79
81
  */
80
82
  export async function buildContextPrimer(userId: string, deps: ContextPrimerDeps): Promise<string> {
81
- const entries = await deps.getLastSessionEntries(userId);
83
+ // Episodic memory is enrichment: without Qdrant the primer goes on without
84
+ // the recap instead of failing the turn.
85
+ const entries = await deps.getLastSessionEntries(userId).catch((err: unknown) => {
86
+ deps.log(`last session for ${userId} unavailable, primer built without it: ${String(err)}`);
87
+ return [];
88
+ });
82
89
  // Checked regardless of `entries` — a pending confirmation isn't tied to
83
90
  // "was there a prior episodic session", it's simply still open right now.
84
91
  const pendingTokens = await pendingConfirmationTokens(userId, deps);