@mercury-fw/core 0.25.0 → 0.26.0
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 +33 -0
- package/dist/src/memory/collection-setup.d.ts +16 -0
- package/dist/src/memory/memory-cli.d.ts +38 -0
- package/dist/src/session/context-primer.d.ts +2 -0
- package/package.json +5 -5
- package/src/compose.ts +13 -4
- package/src/memory/collection-setup.ts +34 -0
- package/src/memory/memory-cli.ts +140 -0
- package/src/session/context-primer.ts +8 -1
- package/src/wiki/vault-cli.ts +9 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# @mercury-fw/core
|
|
2
2
|
|
|
3
|
+
## 0.26.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 8b92abb: - `mfw` operates an app from inside its folder: `start`, `stop` and `restart` (with `--no-cache`), `logs`, `repl`, `shell`, all as `docker compose` calls.
|
|
8
|
+
- `mfw vault` maintains the wiki vault (`list`, `read`, `grep`, `write-curated`, `write-raw`) in a one-off container.
|
|
9
|
+
- `mfw memory list` and `mfw memory read <collection>` read the memory on Qdrant, newest first where the collection has a timestamp index.
|
|
10
|
+
- `mfw reset memory` and `mfw reset wiki` delete a memory volume after you type the app's name, then bring the service back up empty; after a memory reset a running app is restarted, so it sets up its collections again.
|
|
11
|
+
- The command line is declared with commander: `--help` at every level, a suggestion for a mistyped command, and every argument checked before anything runs.
|
|
12
|
+
- A scaffolded app lists `@mercury-fw/cli` among its devDependencies, at the framework's version, and its README runs it through `bunx mfw`.
|
|
13
|
+
- A scaffolded app's `@types/bun` and `typescript` use caret ranges instead of exact versions.
|
|
14
|
+
- The core ships a read-only memory CLI (`src/memory/memory-cli.ts`) next to the vault one, which is what `mfw memory` runs.
|
|
15
|
+
- The vault CLI says so when asked to read a note that doesn't exist, instead of printing a stack trace.
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- @mercury-fw/plugin-types@0.26.0
|
|
20
|
+
- @mercury-fw/channel-types@0.26.0
|
|
21
|
+
- @mercury-fw/cli-engine@0.26.0
|
|
22
|
+
- @mercury-fw/confirm-engine@0.26.0
|
|
23
|
+
|
|
24
|
+
## 0.25.1
|
|
25
|
+
|
|
26
|
+
### Patch Changes
|
|
27
|
+
|
|
28
|
+
- 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.
|
|
29
|
+
- A new session's context primer goes on without the last-session recap when Qdrant doesn't answer, instead of failing the turn.
|
|
30
|
+
- The scaffolded app's `docker-compose.yml` restarts the app service unless it was stopped.
|
|
31
|
+
- @mercury-fw/plugin-types@0.25.1
|
|
32
|
+
- @mercury-fw/channel-types@0.25.1
|
|
33
|
+
- @mercury-fw/cli-engine@0.25.1
|
|
34
|
+
- @mercury-fw/confirm-engine@0.25.1
|
|
35
|
+
|
|
3
36
|
## 0.25.0
|
|
4
37
|
|
|
5
38
|
### 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
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
type ScrollOffset = string | number | Record<string, unknown> | null;
|
|
3
|
+
/** The slice of the Qdrant client this CLI uses. */
|
|
4
|
+
export type MemoryCliClient = {
|
|
5
|
+
getCollections(): Promise<{
|
|
6
|
+
collections: Array<{
|
|
7
|
+
name: string;
|
|
8
|
+
}>;
|
|
9
|
+
}>;
|
|
10
|
+
count(collection: string, params: {
|
|
11
|
+
exact: boolean;
|
|
12
|
+
}): Promise<{
|
|
13
|
+
count: number;
|
|
14
|
+
}>;
|
|
15
|
+
scroll(collection: string, params: {
|
|
16
|
+
limit: number;
|
|
17
|
+
offset?: ScrollOffset;
|
|
18
|
+
with_payload: boolean;
|
|
19
|
+
order_by?: {
|
|
20
|
+
key: string;
|
|
21
|
+
direction: "asc" | "desc";
|
|
22
|
+
};
|
|
23
|
+
}): Promise<{
|
|
24
|
+
points: Array<{
|
|
25
|
+
id: string | number;
|
|
26
|
+
payload?: Record<string, unknown> | null;
|
|
27
|
+
}>;
|
|
28
|
+
next_page_offset?: ScrollOffset;
|
|
29
|
+
}>;
|
|
30
|
+
};
|
|
31
|
+
type Io = {
|
|
32
|
+
client: MemoryCliClient;
|
|
33
|
+
out: (line: string) => void;
|
|
34
|
+
err: (line: string) => void;
|
|
35
|
+
};
|
|
36
|
+
/** Runs the CLI on `argv` against `io.client`; returns the exit code. */
|
|
37
|
+
export declare function runMemoryCli(argv: string[], { client, out, err }: Io): Promise<number>;
|
|
38
|
+
export {};
|
|
@@ -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.
|
|
3
|
+
"version": "0.26.0",
|
|
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.
|
|
35
|
-
"@mercury-fw/cli-engine": "0.
|
|
36
|
-
"@mercury-fw/confirm-engine": "0.
|
|
37
|
-
"@mercury-fw/plugin-types": "0.
|
|
34
|
+
"@mercury-fw/channel-types": "0.26.0",
|
|
35
|
+
"@mercury-fw/cli-engine": "0.26.0",
|
|
36
|
+
"@mercury-fw/confirm-engine": "0.26.0",
|
|
37
|
+
"@mercury-fw/plugin-types": "0.26.0",
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Read-only CLI for Layer-3 memory on Qdrant — runs INSIDE the Mercury
|
|
4
|
+
* container (where `QDRANT_URL` reaches Qdrant): `mfw memory` runs it in a
|
|
5
|
+
* one-off container, by this path.
|
|
6
|
+
* `list` shows the collections and their sizes, `read` a collection's points:
|
|
7
|
+
* newest first where the collection has a `timestamp` payload index (episodic
|
|
8
|
+
* memory, the verbatim archive), in Qdrant's own order otherwise, and it says
|
|
9
|
+
* which. Nothing here writes.
|
|
10
|
+
*/
|
|
11
|
+
import { parseArgs } from "node:util";
|
|
12
|
+
import { QdrantClient } from "@qdrant/js-client-rest";
|
|
13
|
+
import { scrollCollection } from "../admin/qdrant-scroll.ts";
|
|
14
|
+
|
|
15
|
+
type ScrollOffset = string | number | Record<string, unknown> | null;
|
|
16
|
+
|
|
17
|
+
/** The slice of the Qdrant client this CLI uses. */
|
|
18
|
+
export type MemoryCliClient = {
|
|
19
|
+
getCollections(): Promise<{ collections: Array<{ name: string }> }>;
|
|
20
|
+
count(collection: string, params: { exact: boolean }): Promise<{ count: number }>;
|
|
21
|
+
scroll(
|
|
22
|
+
collection: string,
|
|
23
|
+
params: {
|
|
24
|
+
limit: number;
|
|
25
|
+
offset?: ScrollOffset;
|
|
26
|
+
with_payload: boolean;
|
|
27
|
+
order_by?: { key: string; direction: "asc" | "desc" };
|
|
28
|
+
},
|
|
29
|
+
): Promise<{
|
|
30
|
+
points: Array<{ id: string | number; payload?: Record<string, unknown> | null }>;
|
|
31
|
+
next_page_offset?: ScrollOffset;
|
|
32
|
+
}>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type Io = { client: MemoryCliClient; out: (line: string) => void; err: (line: string) => void };
|
|
36
|
+
|
|
37
|
+
const USAGE = [
|
|
38
|
+
"Usage: mfw memory <command> [args]",
|
|
39
|
+
"",
|
|
40
|
+
"Commands:",
|
|
41
|
+
" list the collections and their points",
|
|
42
|
+
" read <collection> [--limit N] a collection's points, newest first where it can (default 20)",
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/** A point as the lines `read` prints: its id, then one indented line per payload field. */
|
|
46
|
+
function pointLines(point: { id: string | number; payload?: Record<string, unknown> | null }): string[] {
|
|
47
|
+
const fields = Object.entries(point.payload ?? {}).map(
|
|
48
|
+
([key, value]) => ` ${key}: ${typeof value === "string" ? value : JSON.stringify(value)}`,
|
|
49
|
+
);
|
|
50
|
+
return [String(point.id), ...fields, ""];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Whether `err` is Qdrant refusing to order by a field it has no index on
|
|
54
|
+
* (HTTP 400, "No range index for `order_by` key"). */
|
|
55
|
+
function isMissingOrderIndex(err: unknown): boolean {
|
|
56
|
+
const e = err as { status?: number; message?: string; data?: unknown };
|
|
57
|
+
const text = `${e?.message ?? ""} ${JSON.stringify(e?.data ?? "")}`;
|
|
58
|
+
return (e?.status === 400 || /\b400\b/.test(text)) && /index/i.test(text);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Runs the CLI on `argv` against `io.client`; returns the exit code. */
|
|
62
|
+
export async function runMemoryCli(argv: string[], { client, out, err }: Io): Promise<number> {
|
|
63
|
+
const usage = () => {
|
|
64
|
+
USAGE.forEach((line) => err(line));
|
|
65
|
+
return 1;
|
|
66
|
+
};
|
|
67
|
+
let parsed;
|
|
68
|
+
try {
|
|
69
|
+
parsed = parseArgs({ args: argv, options: { limit: { type: "string" } }, allowPositionals: true });
|
|
70
|
+
} catch {
|
|
71
|
+
return usage();
|
|
72
|
+
}
|
|
73
|
+
const [command, collection, ...extra] = parsed.positionals;
|
|
74
|
+
if (extra.length > 0 || (command === "list" && (collection !== undefined || parsed.values.limit !== undefined))) {
|
|
75
|
+
return usage();
|
|
76
|
+
}
|
|
77
|
+
if (command !== "list" && !(command === "read" && collection !== undefined)) return usage();
|
|
78
|
+
|
|
79
|
+
const limitText = parsed.values.limit ?? "20";
|
|
80
|
+
const limit = Number(limitText);
|
|
81
|
+
if (!/^\d+$/.test(limitText) || limit < 1) {
|
|
82
|
+
err(`--limit takes a positive whole number (got "${limitText}").`);
|
|
83
|
+
return 1;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let names: string[];
|
|
87
|
+
try {
|
|
88
|
+
names = (await client.getCollections()).collections.map((c) => c.name).sort();
|
|
89
|
+
} catch (e) {
|
|
90
|
+
err(`Can't reach Qdrant: ${String(e)}`);
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
if (command === "list") {
|
|
96
|
+
if (names.length === 0) {
|
|
97
|
+
out("No collections yet.");
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
for (const name of names) {
|
|
101
|
+
out(`${name} ${(await client.count(name, { exact: true })).count} points`);
|
|
102
|
+
}
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!names.includes(collection as string)) {
|
|
107
|
+
err(`No collection "${collection}". There are: ${names.join(", ") || "none"}.`);
|
|
108
|
+
return 1;
|
|
109
|
+
}
|
|
110
|
+
let points: Array<{ id: string | number; payload?: Record<string, unknown> | null }>;
|
|
111
|
+
try {
|
|
112
|
+
// Qdrant orders by a payload field only when it has an index on it.
|
|
113
|
+
({ points } = await client.scroll(collection as string, {
|
|
114
|
+
limit,
|
|
115
|
+
with_payload: true,
|
|
116
|
+
order_by: { key: "timestamp", direction: "desc" },
|
|
117
|
+
}));
|
|
118
|
+
} catch (e) {
|
|
119
|
+
if (!isMissingOrderIndex(e)) throw e;
|
|
120
|
+
err(`${collection} has no timestamp index: points in Qdrant's own order.`);
|
|
121
|
+
({ points } = await scrollCollection(client, collection as string, { limit }));
|
|
122
|
+
}
|
|
123
|
+
points.flatMap(pointLines).forEach((line) => out(line));
|
|
124
|
+
return 0;
|
|
125
|
+
} catch (e) {
|
|
126
|
+
err(`Qdrant error: ${String(e)}`);
|
|
127
|
+
return 1;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (import.meta.main) {
|
|
132
|
+
const client = new QdrantClient({ url: process.env.QDRANT_URL ?? "http://qdrant:6333" });
|
|
133
|
+
process.exit(
|
|
134
|
+
await runMemoryCli(process.argv.slice(2), {
|
|
135
|
+
client,
|
|
136
|
+
out: (line) => console.log(line),
|
|
137
|
+
err: (line) => console.error(line),
|
|
138
|
+
}),
|
|
139
|
+
);
|
|
140
|
+
}
|
|
@@ -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
|
-
|
|
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);
|
package/src/wiki/vault-cli.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
3
|
* Maintenance CLI for the wiki vault — runs INSIDE the Mercury container
|
|
4
|
-
* (the vault is a Docker named volume, not a host path
|
|
5
|
-
*
|
|
4
|
+
* (the vault is a Docker named volume, not a host path): `mfw vault` runs it
|
|
5
|
+
* in a one-off container, by this path. Thin argv wrapper around
|
|
6
6
|
* functions that already exist and are already tested (`wiki-note.ts`,
|
|
7
7
|
* `vault-init.ts`) — no new write/read logic here, only routing.
|
|
8
8
|
*
|
|
@@ -29,7 +29,7 @@ import { writeCuratedNote, writeRawEntry } from "./wiki-note.ts";
|
|
|
29
29
|
function usage(): never {
|
|
30
30
|
console.error(
|
|
31
31
|
[
|
|
32
|
-
"Usage: vault
|
|
32
|
+
"Usage: mfw vault <command> [args]",
|
|
33
33
|
"",
|
|
34
34
|
"Commands:",
|
|
35
35
|
" write-curated <curated/...path.md> [--author NAME] body read from stdin",
|
|
@@ -107,7 +107,12 @@ async function main(): Promise<void> {
|
|
|
107
107
|
case "read": {
|
|
108
108
|
const relativePath = args[0];
|
|
109
109
|
if (!relativePath) usage();
|
|
110
|
-
|
|
110
|
+
const note = Bun.file(`${vaultPath}/${relativePath}`);
|
|
111
|
+
if (!(await note.exists())) {
|
|
112
|
+
console.error(`no note at ${relativePath} (mfw vault list shows them)`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
console.log(await note.text());
|
|
111
116
|
break;
|
|
112
117
|
}
|
|
113
118
|
|