@mercury-fw/core 0.25.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.
Files changed (117) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +38 -0
  3. package/dist/index.d.ts +23 -0
  4. package/dist/src/admin/cli-routes.d.ts +22 -0
  5. package/dist/src/admin/env-file.d.ts +1 -0
  6. package/dist/src/admin/model-routes.d.ts +26 -0
  7. package/dist/src/admin/qdrant-scroll.d.ts +34 -0
  8. package/dist/src/admin/server.d.ts +40 -0
  9. package/dist/src/admin/wiki-routes.d.ts +31 -0
  10. package/dist/src/compose.d.ts +42 -0
  11. package/dist/src/config/define-config.d.ts +31 -0
  12. package/dist/src/cron/idle-session-cron.d.ts +80 -0
  13. package/dist/src/cron/idle-session-scanner.d.ts +16 -0
  14. package/dist/src/cron/self-review-cron.d.ts +55 -0
  15. package/dist/src/cron/semantic-consolidation.d.ts +71 -0
  16. package/dist/src/memory/embedder.d.ts +9 -0
  17. package/dist/src/memory/episodic-store.d.ts +121 -0
  18. package/dist/src/memory/memory-provider.d.ts +51 -0
  19. package/dist/src/memory/semantic-facts-store.d.ts +37 -0
  20. package/dist/src/memory/tool-corrections-store.d.ts +26 -0
  21. package/dist/src/memory/verbatim-archive-store.d.ts +86 -0
  22. package/dist/src/model/client.d.ts +24 -0
  23. package/dist/src/model/context-size.d.ts +30 -0
  24. package/dist/src/plugins/manifest.d.ts +29 -0
  25. package/dist/src/plugins/plugin-loader.d.ts +85 -0
  26. package/dist/src/router/channel-loader.d.ts +30 -0
  27. package/dist/src/router/provider.d.ts +7 -0
  28. package/dist/src/router/terminal-provider.d.ts +37 -0
  29. package/dist/src/router/terminal.d.ts +41 -0
  30. package/dist/src/router/tool-log.d.ts +65 -0
  31. package/dist/src/router/turn-runner.d.ts +86 -0
  32. package/dist/src/session/agent-turn.d.ts +266 -0
  33. package/dist/src/session/context-primer.d.ts +16 -0
  34. package/dist/src/session/episodic-summarizer.d.ts +25 -0
  35. package/dist/src/session/history.d.ts +95 -0
  36. package/dist/src/session/pending-confirmation.d.ts +8 -0
  37. package/dist/src/session/read-skill-tool.d.ts +4 -0
  38. package/dist/src/session/semantic-fact-extractor.d.ts +45 -0
  39. package/dist/src/session/step-info.d.ts +24 -0
  40. package/dist/src/session/summarizer.d.ts +23 -0
  41. package/dist/src/session/system-prompt.d.ts +38 -0
  42. package/dist/src/session/tool-correction-extractor.d.ts +43 -0
  43. package/dist/src/session/tool-log-buffer.d.ts +24 -0
  44. package/dist/src/session/tool-log-recall-tool.d.ts +18 -0
  45. package/dist/src/session/tool-start-hook.d.ts +57 -0
  46. package/dist/src/tools/display-store.d.ts +36 -0
  47. package/dist/src/tools/present-tool.d.ts +23 -0
  48. package/dist/src/wiki/frontmatter-schema.d.ts +53 -0
  49. package/dist/src/wiki/index-entry.d.ts +15 -0
  50. package/dist/src/wiki/orphan-detector.d.ts +1 -0
  51. package/dist/src/wiki/self-review-runner.d.ts +48 -0
  52. package/dist/src/wiki/self-review-tools.d.ts +22 -0
  53. package/dist/src/wiki/vault-cli.d.ts +2 -0
  54. package/dist/src/wiki/vault-init.d.ts +7 -0
  55. package/dist/src/wiki/wiki-note.d.ts +62 -0
  56. package/dist/src/wiki/wiki-read.d.ts +27 -0
  57. package/dist/src/wiki/wiki-tools.d.ts +7 -0
  58. package/index.ts +23 -0
  59. package/package.json +49 -0
  60. package/src/admin/cli-routes.ts +48 -0
  61. package/src/admin/env-file.ts +29 -0
  62. package/src/admin/model-routes.ts +71 -0
  63. package/src/admin/public/index.html +416 -0
  64. package/src/admin/qdrant-scroll.ts +45 -0
  65. package/src/admin/server.ts +188 -0
  66. package/src/admin/wiki-routes.ts +93 -0
  67. package/src/compose.ts +599 -0
  68. package/src/config/define-config.ts +35 -0
  69. package/src/cron/.gitkeep +0 -0
  70. package/src/cron/idle-session-cron.ts +144 -0
  71. package/src/cron/idle-session-scanner.ts +37 -0
  72. package/src/cron/self-review-cron.ts +103 -0
  73. package/src/cron/semantic-consolidation.ts +228 -0
  74. package/src/memory/.gitkeep +0 -0
  75. package/src/memory/embedder.ts +15 -0
  76. package/src/memory/episodic-store.ts +183 -0
  77. package/src/memory/memory-provider.ts +98 -0
  78. package/src/memory/semantic-facts-store.ts +89 -0
  79. package/src/memory/tool-corrections-store.ts +72 -0
  80. package/src/memory/verbatim-archive-store.ts +202 -0
  81. package/src/model/client.ts +33 -0
  82. package/src/model/context-size.ts +42 -0
  83. package/src/plugins/manifest.ts +47 -0
  84. package/src/plugins/plugin-loader.ts +205 -0
  85. package/src/router/channel-loader.ts +56 -0
  86. package/src/router/provider.ts +7 -0
  87. package/src/router/terminal-provider.ts +155 -0
  88. package/src/router/terminal.ts +151 -0
  89. package/src/router/tool-log.ts +116 -0
  90. package/src/router/turn-runner.ts +205 -0
  91. package/src/session/agent-turn.ts +391 -0
  92. package/src/session/context-primer.ts +134 -0
  93. package/src/session/episodic-summarizer.ts +38 -0
  94. package/src/session/history.ts +168 -0
  95. package/src/session/pending-confirmation.ts +8 -0
  96. package/src/session/read-skill-tool.ts +38 -0
  97. package/src/session/semantic-fact-extractor.ts +69 -0
  98. package/src/session/step-info.ts +27 -0
  99. package/src/session/summarizer.ts +36 -0
  100. package/src/session/system-prompt.ts +142 -0
  101. package/src/session/tool-correction-extractor.ts +133 -0
  102. package/src/session/tool-log-buffer.ts +73 -0
  103. package/src/session/tool-log-recall-tool.ts +38 -0
  104. package/src/session/tool-start-hook.ts +164 -0
  105. package/src/tools/display-store.ts +89 -0
  106. package/src/tools/present-tool.ts +41 -0
  107. package/src/wiki/.gitkeep +0 -0
  108. package/src/wiki/frontmatter-schema.ts +49 -0
  109. package/src/wiki/index-entry.ts +59 -0
  110. package/src/wiki/orphan-detector.ts +61 -0
  111. package/src/wiki/self-review-runner.ts +133 -0
  112. package/src/wiki/self-review-tools.ts +162 -0
  113. package/src/wiki/vault-cli.ts +143 -0
  114. package/src/wiki/vault-init.ts +43 -0
  115. package/src/wiki/wiki-note.ts +326 -0
  116. package/src/wiki/wiki-read.ts +122 -0
  117. package/src/wiki/wiki-tools.ts +112 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ # @mercury-fw/core
2
+
3
+ ## 0.25.0
4
+
5
+ ### Minor Changes
6
+
7
+ - c114e34: - Mercury is published on npm as `@mercury-fw/*`: the framework packages move together under one version, plugins and channels have versions of their own.
8
+ - `bun create mercury-agent my-agent` scaffolds an app, and the CLI command is now `mfw` (`@mercury-fw/cli`).
9
+ - Packages ship type declarations, so a new app type-checks against the framework without re-checking its source.
10
+ - External dependencies use version ranges instead of exact pins, so an app shares them with the framework.
11
+ - Every package has its own README, and the repo README describes the framework.
12
+ - MIT license.
13
+
14
+ ### Patch Changes
15
+
16
+ - @mercury-fw/plugin-types@0.25.0
17
+ - @mercury-fw/channel-types@0.25.0
18
+ - @mercury-fw/cli-engine@0.25.0
19
+ - @mercury-fw/confirm-engine@0.25.0
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # @mercury-fw/core
2
+
3
+ The runtime of [Mercury](https://github.com/lucabro81/mercury-fw): it builds an agent from the config an app gives it and runs it, from the conversation loop to the channels. An app depends on it; a plugin doesn't (plugins build on [`@mercury-fw/kit`](https://www.npmjs.com/package/@mercury-fw/kit)).
4
+
5
+ The usual way to get it is scaffolding an app, which wires it up for you:
6
+
7
+ ```bash
8
+ bun create mercury-agent my-agent
9
+ ```
10
+
11
+ ## What it exports
12
+
13
+ - `defineMercuryConfig(config)` types the app's `mercury.config.ts`: `plugins`, `channels`, `persona`.
14
+ - `composeMercury(config)` builds the agent from that config and the environment, and returns what the entrypoints start: `handleTurn`, the declared channels with their runtime, and the background jobs (`startCrons`, `startAdmin`).
15
+ - `loadChannels(channels, { runtime })` turns the declared channels into started providers, and `createTerminalProvider(...)` opens the dev REPL on `handleTurn`.
16
+ - `DEFAULT_PERSONA_IDENTITY`, `DEFAULT_PERSONA_TONE` are the persona an app gets when its config sets none.
17
+
18
+ A scaffolded app's `src/index.ts` (the service) and `src/repl.ts` (the REPL) are the reference for using them.
19
+
20
+ ## Environment
21
+
22
+ | Variable | |
23
+ |---|---|
24
+ | `OLLAMA_HOST` | The Ollama-compatible endpoint. Required. |
25
+ | `OLLAMA_MODEL` | The chat model. Required. |
26
+ | `OLLAMA_EMBEDDING_MODEL` | The embedding model for the episodic memory (default `nomic-embed-text`). |
27
+ | `OLLAMA_THINK` | `false` for a model that doesn't support thinking. |
28
+ | `QDRANT_URL` | Qdrant, for the episodic memory (default `http://qdrant:6333`). |
29
+ | `WIKI_VAULT_PATH` | Where the wiki lives. Required. |
30
+ | `MERCURY_CLIS` | The tool plugins this deployment turns on, by name, comma-separated. |
31
+
32
+ Qdrant being unreachable degrades memory, it doesn't stop the agent.
33
+
34
+ ## Requirements
35
+
36
+ Bun: the package ships its TypeScript source, which Bun runs as is, plus type declarations for your editor and `tsc`.
37
+
38
+ MIT
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The public surface of `@mercury-fw/core` — the framework runtime a Mercury
3
+ * instance consumes. Everything else under `src/` is internal: an app depends on
4
+ * this barrel, never on a deep path.
5
+ *
6
+ * What's exported, and who uses it:
7
+ * - `composeMercury` builds the instance from a config it is *given* (the
8
+ * config is never imported by the core — that's what keeps it app-agnostic).
9
+ * `ComposedApp`/`ConfirmDeps` are its result and confirm-binding types.
10
+ * - `loadChannels`/`LoadedChannel` — the service entrypoint starts the declared
11
+ * channels with these.
12
+ * - `createTerminalProvider` — the dev REPL entrypoint opens the terminal with it.
13
+ * - `defineMercuryConfig`/`MercuryConfig` — the app's `mercury.config.ts` declares
14
+ * its composition through these; `Persona` types its `persona` field.
15
+ * - `DEFAULT_PERSONA_IDENTITY`/`DEFAULT_PERSONA_TONE` — the persona an instance
16
+ * gets when its config sets none; the scaffolder writes them as a new app's
17
+ * starting persona.
18
+ */
19
+ export { composeMercury, type ComposedApp, type ConfirmDeps } from "./src/compose.ts";
20
+ export { loadChannels, type LoadedChannel } from "./src/router/channel-loader.ts";
21
+ export { createTerminalProvider } from "./src/router/terminal-provider.ts";
22
+ export { defineMercuryConfig, type MercuryConfig } from "./src/config/define-config.ts";
23
+ export { DEFAULT_PERSONA_IDENTITY, DEFAULT_PERSONA_TONE, type Persona } from "./src/session/system-prompt.ts";
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Admin panel's CLI status tab: lists each active CLI's allowlisted
3
+ * commands straight from the same `CliConfig` `src/index.ts` already
4
+ * built at startup (`activeCliConfigs`), and — where a CLI declares a
5
+ * plain, non-mutating `auth whoami`-shaped prefix — runs it through the
6
+ * exact same `runCli` used for real tool calls. Never constructs or runs
7
+ * anything else; this is read-only status, not a command console.
8
+ */
9
+ import type { CliConfig } from "@mercury-fw/cli-engine";
10
+ import type { runCli } from "@mercury-fw/cli-engine";
11
+ export type CliStatusEntry = {
12
+ binary: string;
13
+ allowedPrefixes: CliConfig["allowedPrefixes"];
14
+ whoami: {
15
+ available: false;
16
+ } | {
17
+ available: true;
18
+ ok: boolean;
19
+ output: string;
20
+ };
21
+ };
22
+ export declare function getCliStatus(configs: Record<string, CliConfig>, runCliFn: typeof runCli): Promise<CliStatusEntry[]>;
@@ -0,0 +1 @@
1
+ export declare function setEnvValue(content: string, key: string, value: string): string;
@@ -0,0 +1,26 @@
1
+ /** Live model names Ollama actually has pulled, or `null` if the host isn't reachable. */
2
+ export declare function getAvailableModels(host: string, fetchFn?: typeof fetch): Promise<string[] | null>;
3
+ export type ModelStatus = {
4
+ host: string;
5
+ model: string;
6
+ contextLength: number | null;
7
+ availableModels: string[] | null;
8
+ };
9
+ export declare function getModelStatus(host: string, model: string, fetchFn?: typeof fetch): Promise<ModelStatus>;
10
+ export type SelfHealth = {
11
+ uptimeSeconds: number;
12
+ memory: {
13
+ rss: number;
14
+ heapUsed: number;
15
+ heapTotal: number;
16
+ };
17
+ qdrantReachable: boolean;
18
+ ollamaReachable: boolean;
19
+ };
20
+ export declare function getSelfHealth(deps: {
21
+ qdrant: {
22
+ getCollections(): Promise<unknown>;
23
+ };
24
+ ollamaHost: string;
25
+ fetchFn?: typeof fetch;
26
+ }): Promise<SelfHealth>;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Read-only point enumeration for the admin panel's Qdrant inspection tab —
3
+ * `episodic-store.ts`/`semantic-facts-store.ts` only wrap similarity
4
+ * `search`, there's no existing way to just list what's in a collection.
5
+ * One page per call (not an auto-paging fetch-everything loop): a real
6
+ * collection can be arbitrarily large, so the caller decides whether to
7
+ * request another page via the returned `nextOffset`.
8
+ */
9
+ type ScrollOffset = string | number | Record<string, unknown> | null;
10
+ export type ScrollableQdrantClient = {
11
+ scroll(collection: string, params: {
12
+ limit: number;
13
+ offset?: ScrollOffset;
14
+ with_payload: boolean;
15
+ }): Promise<{
16
+ points: Array<{
17
+ id: string | number;
18
+ payload?: Record<string, unknown> | null;
19
+ }>;
20
+ next_page_offset?: ScrollOffset;
21
+ }>;
22
+ };
23
+ export type ScrollPage = {
24
+ points: Array<{
25
+ id: string | number;
26
+ payload: Record<string, unknown> | null;
27
+ }>;
28
+ nextOffset: ScrollOffset;
29
+ };
30
+ export declare function scrollCollection(client: ScrollableQdrantClient, collectionName: string, opts: {
31
+ limit: number;
32
+ offset?: ScrollOffset;
33
+ }): Promise<ScrollPage>;
34
+ export {};
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Admin panel POC — a separate `Bun.serve()` instance running inside the
3
+ * same Mercury process (see plan: reuses the already-constructed `qdrant`
4
+ * client, `activeCliConfigs`, model, and vault path directly, no IPC).
5
+ * Dev-only, unauthenticated by design (see docs/DECISIONS.md-adjacent
6
+ * scoping conversation) — never started unless `ADMIN_PANEL_ENABLED` is
7
+ * set (see `src/index.ts`).
8
+ */
9
+ import type { LanguageModel } from "ai";
10
+ import type { CliConfig } from "@mercury-fw/cli-engine";
11
+ import type { runCli } from "@mercury-fw/cli-engine";
12
+ import { type ScrollableQdrantClient } from "./qdrant-scroll.ts";
13
+ type QdrantDeps = ScrollableQdrantClient & {
14
+ getCollections(): Promise<{
15
+ collections: Array<{
16
+ name: string;
17
+ }>;
18
+ }>;
19
+ };
20
+ export type AdminServerDeps = {
21
+ port: number;
22
+ vaultPath: string;
23
+ model: LanguageModel;
24
+ qdrant: QdrantDeps;
25
+ qdrantCollections: {
26
+ episodic: string;
27
+ semanticFacts: string;
28
+ };
29
+ activeCliConfigs: Record<string, CliConfig>;
30
+ runCliFn: typeof runCli;
31
+ ollamaHost: string;
32
+ ollamaModel: string;
33
+ systemPrompts: {
34
+ terminal: string;
35
+ googleChat: string;
36
+ };
37
+ envFilePath: string;
38
+ };
39
+ export declare function startAdminServer(deps: AdminServerDeps): ReturnType<typeof Bun.serve>;
40
+ export {};
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Admin panel's Wiki tab. `list`/`read`/`grep` walk the whole vault
3
+ * directly (same trusted-admin-context choice `vault-cli.ts` already
4
+ * makes — bypassing `wiki-read.ts`'s per-user `allowedRoots` scoping,
5
+ * which exists to isolate what the MODEL can see per caller, not what an
6
+ * operator running this panel can see).
7
+ *
8
+ * Curated edits go through a real, short-lived agent turn instead of
9
+ * calling `writeCuratedNote` directly — see `editWikiViaModel`. Raw
10
+ * writes and deletes have no model-facing tool today, so they stay
11
+ * direct calls into `wiki-note.ts`, same as `vault-cli.ts`.
12
+ */
13
+ import type { LanguageModel } from "ai";
14
+ import type { WikiGrepMatch } from "../wiki/wiki-read.ts";
15
+ export declare function listWikiVault(vaultPath: string): Promise<string[]>;
16
+ export declare function readWikiVaultFile(vaultPath: string, relativePath: string): Promise<string>;
17
+ export declare function grepWikiVault(vaultPath: string, pattern: string): Promise<WikiGrepMatch[]>;
18
+ /** `vaultRelativePath` must start with `raw/`, matching `vault-cli.ts`'s `write-raw`. */
19
+ export declare function writeRawWikiEntry(vaultPath: string, vaultRelativePath: string, body: string): Promise<void>;
20
+ /** `vaultRelativePath` must start with `curated/` or `raw/` — nothing else is deletable from here. */
21
+ export declare function deleteWikiEntry(vaultPath: string, vaultRelativePath: string): Promise<void>;
22
+ /**
23
+ * Sends `instruction` through a real, short-lived agent turn scoped
24
+ * ONLY to the four wiki tools (`createWikiTools`) — never the full
25
+ * toolset a normal channel gets, so this box can't touch Jira/Bitbucket.
26
+ * Reuses the exact commit path a real conversation would take
27
+ * (`write_file` -> `writeCuratedNote` -> git commit, D-16); this
28
+ * function has no write logic of its own. History is fresh per call,
29
+ * never persisted — this isn't a real session.
30
+ */
31
+ export declare function editWikiViaModel(model: LanguageModel, vaultPath: string, instruction: string): Promise<string>;
@@ -0,0 +1,42 @@
1
+ import { type ConfirmationStore } from "@mercury-fw/confirm-engine";
2
+ import type { MercuryConfig } from "./config/define-config.ts";
3
+ import type { HandleTurn, ChannelRuntimeContext, ChannelPlugin } from "@mercury-fw/channel-types";
4
+ import { writeConfirmationNote } from "./wiki/wiki-note.ts";
5
+ /** A stoppable subsystem (cron, server). */
6
+ type Stoppable = {
7
+ stop: () => void;
8
+ };
9
+ /** The confirm capability's binding to this instance's store/vault/note-writer, shared by the terminal and the channel runtime. */
10
+ export type ConfirmDeps = {
11
+ store: ConfirmationStore;
12
+ vaultPath: string;
13
+ writeConfirmationNoteFn: typeof writeConfirmationNote;
14
+ };
15
+ /**
16
+ * The built Mercury instance. `build`, not `start`: the channels, crons and
17
+ * admin are not running until an entrypoint starts them.
18
+ */
19
+ export type ComposedApp = {
20
+ /** The turn driver every channel funnels through (see `turn-runner.ts`). */
21
+ handleTurn: HandleTurn;
22
+ /** The declared channel plugins (from `mercury.config.ts`). */
23
+ channels: ChannelPlugin[];
24
+ /** The runtime context each channel's `build()` gets — confirm + in-process reads injected by the core. */
25
+ channelRuntime: ChannelRuntimeContext;
26
+ /** The confirm binding, for a caller (the terminal) that intercepts tokens directly. */
27
+ confirmDeps: ConfirmDeps;
28
+ ollamaHost: string;
29
+ ollamaModel: string;
30
+ /** Starts the Layer-3 idle-capture and self-review crons; returns a single stopper for shutdown. */
31
+ startCrons: () => Stoppable;
32
+ /** Starts the POC admin panel if `ADMIN_PANEL_ENABLED`; returns it (to stop on shutdown), or undefined. */
33
+ startAdmin: () => Stoppable | undefined;
34
+ };
35
+ /**
36
+ * Builds a Mercury instance from the composition `config` it is given + env. The
37
+ * config is a parameter, never imported here: that's what makes the core
38
+ * app-agnostic — an entrypoint (the service, the dev REPL, a future scaffolded
39
+ * app) reads its own `mercury.config.ts` and passes it in. See {@link ComposedApp}.
40
+ */
41
+ export declare function composeMercury(config: MercuryConfig): Promise<ComposedApp>;
42
+ export {};
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The instance composition config contract and its `defineMercuryConfig`
3
+ * helper. A Mercury instance is composed by declaring, in one place, which
4
+ * plugins it runs — the way a Nuxt/Vite project declares its config through a
5
+ * `defineConfig` call in a `*.config.ts` file. `defineMercuryConfig` adds no
6
+ * runtime behavior; it exists purely so the app-root `mercury.config.ts` gets
7
+ * editor/compiler support (the argument is checked against `MercuryConfig`) and
8
+ * so there is a single, stable seam the composition root reads from.
9
+ *
10
+ * This is the minimal foundational slice of a broader composition rethink: for
11
+ * now the config carries only the plugin list. Per-plugin configuration and the
12
+ * eventual retirement of the file-based cli-configs are deliberately not here.
13
+ */
14
+ import type { Plugin } from "@mercury-fw/plugin-types";
15
+ import type { ChannelPlugin } from "@mercury-fw/channel-types";
16
+ import type { Persona } from "../session/system-prompt.ts";
17
+ /** The shape of a Mercury instance's composition config: the tool plugins
18
+ * (gated by MERCURY_CLIS) and the channel plugins (enabled by being declared
19
+ * here — declared = active), each loaded by its own loader, plus the
20
+ * assistant's persona (its identity and tone; the defaults when left out). */
21
+ export type MercuryConfig = {
22
+ plugins: Plugin[];
23
+ channels?: ChannelPlugin[];
24
+ persona?: Persona;
25
+ };
26
+ /**
27
+ * Identity + typing helper for `mercury.config.ts`. Returns its argument
28
+ * unchanged; its only job is to type the config literal against `MercuryConfig`
29
+ * at the call site, exactly like `defineConfig` in the Vite/Nuxt ecosystem.
30
+ */
31
+ export declare function defineMercuryConfig(config: MercuryConfig): MercuryConfig;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The session-persistence cron loop: periodically sweeps for sessions idle past the
3
+ * configured timeout, summarizes each one (episodic-summarizer.ts),
4
+ * writes the result to Qdrant (episodic-store.ts), and discards the raw
5
+ * transcript (`deps.closeSession`). Every dependency is injected — this
6
+ * file owns only the sweep/interval mechanics, not session storage, the
7
+ * LLM call, or Qdrant itself.
8
+ */
9
+ import type { IdleSessionScanner } from "./idle-session-scanner.ts";
10
+ import type { Message } from "../session/history.ts";
11
+ import type { EpisodicSummary } from "../memory/episodic-store.ts";
12
+ import type { SemanticFact } from "../session/semantic-fact-extractor.ts";
13
+ import type { SemanticFactEntry } from "../memory/semantic-facts-store.ts";
14
+ export type IdleSession = {
15
+ key: string;
16
+ userId: string;
17
+ messages: Message[];
18
+ };
19
+ /**
20
+ * Dependencies for `captureSessionToMemory` — a subset of
21
+ * `IdleSessionSweepDeps`, without `getSession`/`closeSession`: this
22
+ * function never decides *whether* a session should be captured or
23
+ * whether it should be closed afterwards, only *how* to capture a given
24
+ * slice of messages. Reused by the idle sweep below (final capture +
25
+ * close) and, without touching this file's own tests, by the two
26
+ * mid-conversation triggers wired in `index.ts` (message-count threshold,
27
+ * Layer 1 compression) — neither of which closes the session.
28
+ */
29
+ export type CaptureDeps = {
30
+ summarize: (messages: Message[]) => Promise<string>;
31
+ store: (entry: EpisodicSummary) => Promise<void>;
32
+ /**
33
+ * Semantic fact extraction/consolidation (D-22/D-34) — an enrichment on
34
+ * top of the episodic summary above, not a required part of it: omit
35
+ * all three and capture behaves exactly as before. When present, a
36
+ * failure here is logged and never propagates — the episodic write
37
+ * already succeeded and is the source of truth being preserved, same
38
+ * "system must work when this enrichment is absent" boundary as every
39
+ * other Layer 2/3 store in Mercury.
40
+ */
41
+ extractFacts?: (messages: Message[]) => Promise<SemanticFact[]>;
42
+ storeFact?: (entry: SemanticFactEntry) => Promise<void>;
43
+ consolidateFact?: (userId: string, topic: string) => Promise<void>;
44
+ log?: (msg: string) => void;
45
+ };
46
+ /**
47
+ * Summarizes+stores an episodic entry for `messages`, then (when the
48
+ * semantic deps are provided) extracts and consolidates semantic facts —
49
+ * the one place this logic lives, shared by every capture trigger. A
50
+ * failure summarizing/storing propagates to the caller (it decides what
51
+ * "capture failed" means for its own trigger — e.g. the idle sweep below
52
+ * leaves the session tracked for retry and skips `closeSession`); a
53
+ * failure in the semantic enrichment layer is caught and logged here,
54
+ * never propagated, since it must never undo an episodic write that
55
+ * already succeeded.
56
+ */
57
+ export declare function captureSessionToMemory(userId: string, sessionKey: string, messages: Message[], now: number, deps: CaptureDeps): Promise<void>;
58
+ export type IdleSessionSweepDeps = CaptureDeps & {
59
+ /** Looks up a session's current content by key; `undefined` if it's already gone (e.g. closed by something else in the meantime). */
60
+ getSession: (key: string) => IdleSession | undefined;
61
+ /** Discards the session's raw transcript — called only after a successful summarize+store. */
62
+ closeSession: (key: string) => void;
63
+ };
64
+ /**
65
+ * Runs one sweep at `now`: every session `scanner` reports idle (past
66
+ * `idleTimeoutMs`) gets captured (`captureSessionToMemory`) and closed,
67
+ * then cleared from `scanner`. A failure capturing is logged and leaves
68
+ * that session's tracking untouched (retried on the next sweep) — it must
69
+ * never stop the sweep from processing the others (hard-won convention:
70
+ * one bad tick can't take down the rest of Mercury).
71
+ */
72
+ export declare function runIdleSessionSweep(scanner: IdleSessionScanner, now: number, idleTimeoutMs: number, deps: IdleSessionSweepDeps): Promise<void>;
73
+ export type IdleSessionCron = {
74
+ stop: () => void;
75
+ };
76
+ /** Starts the periodic sweep on `opts.checkIntervalMs`, gated on `opts.idleTimeoutMs`. `stop()` halts it. */
77
+ export declare function startIdleSessionCron(scanner: IdleSessionScanner, deps: IdleSessionSweepDeps, opts: {
78
+ idleTimeoutMs: number;
79
+ checkIntervalMs: number;
80
+ }): IdleSessionCron;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Pure idle-tracking for session persistence: remembers the last
3
+ * activity time per session key and reports which are idle at a given
4
+ * moment. Time is always passed in, never read internally (`Date.now()`
5
+ * lives in the caller, e.g. `src/index.ts`/`idle-session-cron.ts`) — this
6
+ * is what makes `scanIdle`'s threshold behavior exactly testable.
7
+ */
8
+ export type IdleSessionScanner = {
9
+ /** Records activity for `key` at `now`, resetting its idle clock. */
10
+ touch(key: string, now: number): void;
11
+ /** Returns every tracked key whose last activity is at least `idleTimeoutMs` before `now`. */
12
+ scanIdle(now: number, idleTimeoutMs: number): string[];
13
+ /** Stops tracking `key` — call after a session has been consolidated and its raw transcript discarded. */
14
+ clear(key: string): void;
15
+ };
16
+ export declare function createIdleSessionScanner(): IdleSessionScanner;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The wiki self-review cron: runs once nightly (a fixed local hour,
3
+ * hardcoded, no env override — deliberately different from every other
4
+ * interval in this codebase, per an explicit request to keep this one
5
+ * out of runtime configurability), running raw/ triage, index.md/orphan
6
+ * maintenance, and a contradiction check as three independent sub-passes
7
+ * (see `self-review-runner.ts` for why they're independent, not one
8
+ * multi-step call). Raw-triage and index/orphan each skip when their own
9
+ * cheap pre-check finds nothing to do; the contradiction check has no
10
+ * such pre-check and always runs on a triggered tick — running the whole
11
+ * thing at night, when nothing else contends for the shared local model,
12
+ * is what makes that affordable.
13
+ *
14
+ * This file owns only the scheduling/orchestration mechanics — vault
15
+ * access and the actual LLM calls are injected, same separation
16
+ * `idle-session-cron.ts` uses for session storage and the LLM summarizer.
17
+ */
18
+ export type SelfReviewTickDeps = {
19
+ listRawEntries: () => Promise<string[]>;
20
+ findOrphans: () => Promise<string[]>;
21
+ runRawTriage: (rawEntries: string[]) => Promise<void>;
22
+ runIndexAndOrphan: (orphans: string[]) => Promise<void>;
23
+ runContradictionCheck: () => Promise<void>;
24
+ log?: (msg: string) => void;
25
+ };
26
+ /**
27
+ * Runs one nightly pass: raw-triage and index/orphan each run only if
28
+ * their own cheap signal found something; the contradiction check always
29
+ * runs, since nothing cheap can tell it whether there's anything to find.
30
+ * Each sub-pass is wrapped in its own try/catch — one failing must not
31
+ * stop the other two from running in the same pass (hard-won convention:
32
+ * one bad tick can't take down the rest of Mercury).
33
+ */
34
+ export declare function runSelfReviewTick(deps: SelfReviewTickDeps): Promise<void>;
35
+ /** 3 AM local time — hardcoded on purpose, see file header. */
36
+ export declare const SELF_REVIEW_HOUR = 3;
37
+ /** How often to check whether it's time to run — mirrors idle-session-cron's
38
+ * check-vs-timeout split, hardcoded for the same reason as the hour above. */
39
+ export declare const SELF_REVIEW_CHECK_INTERVAL_MS: number;
40
+ export type SelfReviewCron = {
41
+ stop: () => void;
42
+ };
43
+ /**
44
+ * Checks every `opts.checkIntervalMs` whether the current local hour
45
+ * matches `opts.hour` and today hasn't run yet; if so, runs one tick.
46
+ * `lastRunDate` is in-memory only — if the process restarts mid-window it
47
+ * just waits for tomorrow's, which is fine, nothing here needs to survive
48
+ * a restart. `opts.now`/`opts.hour`/`opts.checkIntervalMs` exist purely as
49
+ * test seams; production (`index.ts`) never overrides them.
50
+ */
51
+ export declare function startSelfReviewCron(deps: SelfReviewTickDeps, opts?: {
52
+ hour?: number;
53
+ checkIntervalMs?: number;
54
+ now?: () => Date;
55
+ }): SelfReviewCron;
@@ -0,0 +1,71 @@
1
+ import type { readWikiFile } from "../wiki/wiki-read.ts";
2
+ import type { writeInferredNote, writeToolCorrectionNote } from "../wiki/wiki-note.ts";
3
+ import type { SemanticFactEntry } from "../memory/semantic-facts-store.ts";
4
+ import type { ToolCorrectionEntry } from "../memory/tool-corrections-store.ts";
5
+ type ClusterFn = (userId: string, topic: string, limit: number) => Promise<SemanticFactEntry[]>;
6
+ type Confidence = "low" | "medium" | "high";
7
+ export type ConsolidationDeps = {
8
+ vaultPath: string;
9
+ clusterFn: ClusterFn;
10
+ readWikiFileFn: typeof readWikiFile;
11
+ writeInferredNoteFn: typeof writeInferredNote;
12
+ k?: number;
13
+ confidenceForCount?: (dominantCount: number, k: number) => Confidence;
14
+ now?: () => string;
15
+ };
16
+ type ToolCorrectionClusterFn = (tool: string, topic: string, limit: number) => Promise<ToolCorrectionEntry[]>;
17
+ /**
18
+ * Same shape as `ConsolidationDeps`, keyed by `tool` instead of `userId` —
19
+ * `readNoteFn`/`writeNoteFn` deliberately don't take a userId at all
20
+ * (unlike `readWikiFileFn`/`writeInferredNoteFn` above): a procedural
21
+ * correction lives under `curated/standards/`, visible to every session
22
+ * regardless of who asks, never scoped to one user's own
23
+ * `inferred/users/<userId>/`.
24
+ */
25
+ export type ToolCorrectionConsolidationDeps = {
26
+ vaultPath: string;
27
+ clusterFn: ToolCorrectionClusterFn;
28
+ readNoteFn: (vaultPath: string, relativePath: string) => Promise<string>;
29
+ writeNoteFn: typeof writeToolCorrectionNote;
30
+ k?: number;
31
+ confidenceForCount?: (dominantCount: number, k: number) => Confidence;
32
+ now?: () => string;
33
+ };
34
+ /** Window size for consolidation — how many recent occurrences of a topic to consider. Uncalibrated: chosen without real usage data, to revisit once there's actual traffic to tune against. */
35
+ export declare const DEFAULT_CONSOLIDATION_K = 3;
36
+ /**
37
+ * Uncalibrated confidence bands, count relative to `k`: a single
38
+ * occurrence is unconfirmed (low); repeated but not unanimous within the
39
+ * tracked window is medium; the dominant value filling the whole window
40
+ * is high. Same "revisit with real usage" caveat as `DEFAULT_CONSOLIDATION_K`.
41
+ */
42
+ export declare function defaultConfidenceForCount(dominantCount: number, k: number): Confidence;
43
+ /**
44
+ * Re-clusters `topic` for `userId`, and promotes the dominant value to a
45
+ * wiki note if it beats the current incumbent's count. No-op if the
46
+ * cluster is empty or has no single dominant value.
47
+ *
48
+ * `userId` here is Qdrant's own storage form (e.g. `"users/42"`, a raw
49
+ * Google Chat resource name) — `clusterFn` above searches with it as-is,
50
+ * matching how `storeSemanticFact` wrote it. The wiki's
51
+ * `inferred/users/<userId>/` convention expects a different,
52
+ * `encodeURIComponent`-encoded form instead (the same one the model's
53
+ * own wiki tools already use) — a raw userId containing "/" would
54
+ * otherwise be rejected outright by
55
+ * `writeInferredNote`'s own path-separator guard, and even without that
56
+ * guard would land in a directory the model's wiki tools never look at.
57
+ * Two different representations of the same identity, for two different
58
+ * purposes — `wikiUserId` below is used only for the wiki-facing calls,
59
+ * never for `clusterFn`.
60
+ */
61
+ export declare function consolidateSemanticFact(userId: string, topic: string, deps: ConsolidationDeps): Promise<void>;
62
+ /**
63
+ * Same promotion logic as `consolidateSemanticFact` — re-clusters `topic`
64
+ * for `tool`, promotes the dominant value if it beats the incumbent's
65
+ * count — but keyed by `tool` (a CLI, not a person) and writing to
66
+ * `curated/standards/<tool>-<topic>.md` (global) instead of
67
+ * `inferred/users/<userId>/<topic>.md` (per-user). No userId encoding
68
+ * needed here: a tool name (e.g. "jira") never contains a path separator.
69
+ */
70
+ export declare function consolidateToolCorrection(tool: string, topic: string, deps: ToolCorrectionConsolidationDeps): Promise<void>;
71
+ export {};
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Thin glue turning an embedding model into the `(text) => Promise<number[]>`
3
+ * shape `episodic-store.ts`'s `storeEpisodicSummary` expects. Same "not
4
+ * worth mocking deeply" reasoning as `session/summarizer.ts` — one line
5
+ * of glue around the AI SDK's `embed`, no dedicated test file.
6
+ */
7
+ import { type EmbeddingModel } from "ai";
8
+ /** Returns a function that embeds a string using `model`. */
9
+ export declare function createEmbedder(model: EmbeddingModel): (text: string) => Promise<number[]>;