diff --git a/CHANGELOG.md b/CHANGELOG.md
index ee4a412..f3c9d47 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,32 @@
# Changelog
+## 1.0.3 - 2026-05-28
+
+Housekeeping release. Nothing about how Audrey behaves has changed — this is
+all under-the-hood tidying plus a friendlier README. Safe to upgrade from 1.0.2
+without touching anything.
+
+### Cleaner code under the hood
+
+- Started breaking up the big `mcp-server/index.ts` file (it had grown to ~3,600
+ lines that did everything at once). The memory-tool input schemas and the
+ shared validation helpers now live in their own small files
+ (`tool-schemas.ts`, `tool-validation.ts`). Same behavior, just easier to read
+ and work on. More of this tidying will follow.
+
+### More reliable tests
+
+- The test suite used to need a slow, multi-step "build all the benchmark and
+ paper files first" step before it could run. It now sets those up
+ automatically, so `npm test` (or a plain `vitest run`) just works from a fresh
+ checkout. 785 tests pass with nothing extra to remember.
+
+### Friendlier docs
+
+- The README now opens with a short "In Plain English" section that explains
+ what Audrey is for in everyday language, before diving into the technical
+ detail.
+
## 1.0.2 - 2026-05-28
Maintenance and engineering-quality release. No runtime behavior change — the
diff --git a/README.md b/README.md
index 67c1af2..1a1dcb7 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,14 @@
+## In Plain English
+
+AI coding assistants are brilliant but forgetful. They'll happily rerun the same broken command they ran yesterday, forget the rules your team agreed on last week, and treat every new session like it's day one.
+
+Audrey is the memory they're missing. It quietly keeps track of what worked, what failed, and what you told it — then checks that memory **before** the agent does something, so it can say "hold on, this exact command failed last time, and here's what fixed it" instead of repeating the mistake. Everything lives in one local file on your machine: no cloud, no account, and nothing about your code ever leaves your computer.
+
+That's the whole idea. The rest of this README is the detail.
+
## Why Audrey Exists
Agents forget the exact mistakes they made yesterday. They repeat broken commands, lose project-specific rules, miss contradictions, and treat every new session like a cold start.
diff --git a/mcp-server/config.ts b/mcp-server/config.ts
index de4f9e6..664eda1 100644
--- a/mcp-server/config.ts
+++ b/mcp-server/config.ts
@@ -3,7 +3,7 @@ import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import type { AudreyConfig, EmbeddingConfig, LLMConfig } from '../src/types.js';
-export const VERSION = '1.0.2';
+export const VERSION = '1.0.3';
export const SERVER_NAME = 'audrey-memory';
export const DEFAULT_AGENT = 'local-agent';
export const DEFAULT_DATA_DIR = join(homedir(), '.audrey', 'data');
diff --git a/mcp-server/index.ts b/mcp-server/index.ts
index 28d278f..be78316 100644
--- a/mcp-server/index.ts
+++ b/mcp-server/index.ts
@@ -15,11 +15,9 @@ import { execFileSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { Audrey, MemoryController } from '../src/index.js';
import { readStoredDimensions } from '../src/db.js';
-import { importSnapshotSchema } from '../src/import.js';
import { isAudreyProfileEnabled, type ProfileDiagnostics } from '../src/profile.js';
import type {
AudreyConfig,
- EmbeddingProvider,
IntrospectResult,
MemoryStatusResult,
RecallResults,
@@ -38,332 +36,44 @@ import {
resolveEmbeddingProvider,
resolveLLMProvider,
} from './config.js';
-
-const VALID_SOURCES = [
- 'direct-observation',
- 'told-by-user',
- 'tool-result',
- 'inference',
- 'model-generated',
-] as const;
-
-const VALID_TYPES = ['episodic', 'semantic', 'procedural'] as const;
-
-export const MAX_MEMORY_CONTENT_LENGTH = 50_000;
-export const ADMIN_TOOLS_ENV = 'AUDREY_ENABLE_ADMIN_TOOLS';
+import {
+ initializeEmbeddingProvider,
+ requireAdminTools,
+ validateForgetSelection,
+ validateMemoryContent,
+} from './tool-validation.js';
+import {
+ memoryEncodeToolSchema,
+ memoryForgetToolSchema,
+ memoryGuardAfterToolSchema,
+ memoryGuardBeforeToolSchema,
+ memoryImportToolSchema,
+ memoryPreflightToolSchema,
+ memoryRecallToolSchema,
+ memoryReflexesToolSchema,
+ memoryValidateToolSchema,
+} from './tool-schemas.js';
+
+// Re-export the tool-validation and tool-schema public surface so existing
+// importers of `mcp-server/index.js` (tests, embedders) keep resolving.
+export {
+ ADMIN_TOOLS_ENV,
+ MAX_MEMORY_CONTENT_LENGTH,
+ initializeEmbeddingProvider,
+ isAdminToolsEnabled,
+ requireAdminTools,
+ validateForgetSelection,
+ validateMemoryContent,
+} from './tool-validation.js';
+export * from './tool-schemas.js';
const subcommand = (process.argv[2] || '').trim() || undefined;
-function isNonEmptyText(value: unknown): boolean {
- return typeof value === 'string' && value.trim().length > 0;
-}
-
-export function validateMemoryContent(content: string): void {
- if (!isNonEmptyText(content)) {
- throw new Error('content must be a non-empty string');
- }
- if (content.length > MAX_MEMORY_CONTENT_LENGTH) {
- throw new Error(`content exceeds maximum length of ${MAX_MEMORY_CONTENT_LENGTH} characters`);
- }
-}
-
-export function validateForgetSelection(id?: string, query?: string): void {
- if ((id && query) || (!id && !query)) {
- throw new Error('Provide exactly one of id or query');
- }
-}
-
-export function isAdminToolsEnabled(
- env: Record = process.env,
-): boolean {
- const value = env[ADMIN_TOOLS_ENV]?.toLowerCase();
- return value === '1' || value === 'true' || value === 'yes';
-}
-
-export function requireAdminTools(env: Record = process.env): void {
- if (!isAdminToolsEnabled(env)) {
- throw new Error(
- `Admin memory tools are disabled. Set ${ADMIN_TOOLS_ENV}=1 to enable export, import, and forget operations.`,
- );
- }
-}
-
-export async function initializeEmbeddingProvider(provider: EmbeddingProvider): Promise {
- if (provider && typeof provider.ready === 'function') {
- await provider.ready();
- }
-}
-
function isEmbeddingWarmupDisabled(env: Record = process.env): boolean {
const value = env['AUDREY_DISABLE_WARMUP'];
return value === '1' || value?.toLowerCase() === 'true' || value?.toLowerCase() === 'yes';
}
-export const memoryEncodeToolSchema = {
- content: z
- .string()
- .max(MAX_MEMORY_CONTENT_LENGTH)
- .refine(isNonEmptyText, 'Content must not be empty')
- .describe('The memory content to encode'),
- source: z.enum(VALID_SOURCES).describe('Source type of the memory'),
- tags: z.array(z.string()).optional().describe('Optional tags for categorization'),
- salience: z.number().min(0).max(1).optional().describe('Importance weight 0-1'),
- context: z
- .record(z.string(), z.string())
- .optional()
- .describe(
- 'Situational context as key-value pairs (e.g., {task: "debugging", domain: "payments"})',
- ),
- affect: z
- .object({
- valence: z
- .number()
- .min(-1)
- .max(1)
- .describe('Emotional valence: -1 (very negative) to 1 (very positive)'),
- arousal: z
- .number()
- .min(0)
- .max(1)
- .optional()
- .describe('Emotional arousal: 0 (calm) to 1 (highly activated)'),
- label: z
- .string()
- .optional()
- .describe('Human-readable emotion label (e.g., "curiosity", "frustration", "relief")'),
- })
- .optional()
- .describe('Emotional affect - how this memory feels'),
- private: z
- .boolean()
- .optional()
- .describe('If true, memory is only visible to the AI and excluded from public recall results'),
- wait_for_consolidation: z
- .boolean()
- .optional()
- .describe(
- 'If true, wait for post-encode validation/interference/resonance work before returning. Defaults to false.',
- ),
-};
-
-export const memoryRecallToolSchema = {
- query: z.string().describe('Search query to match against memories'),
- limit: z.number().min(1).max(50).optional().describe('Max results (default 10)'),
- types: z.array(z.enum(VALID_TYPES)).optional().describe('Memory types to search'),
- min_confidence: z.number().min(0).max(1).optional().describe('Minimum confidence threshold'),
- tags: z.array(z.string()).optional().describe('Only return episodic memories with these tags'),
- sources: z
- .array(z.enum(VALID_SOURCES))
- .optional()
- .describe('Only return episodic memories from these sources'),
- after: z.string().optional().describe('Only return memories created after this ISO date'),
- before: z.string().optional().describe('Only return memories created before this ISO date'),
- context: z
- .record(z.string(), z.string())
- .optional()
- .describe('Retrieval context - memories encoded in matching context get boosted'),
- mood: z
- .object({
- valence: z
- .number()
- .min(-1)
- .max(1)
- .describe('Current emotional valence: -1 (negative) to 1 (positive)'),
- arousal: z
- .number()
- .min(0)
- .max(1)
- .optional()
- .describe('Current arousal: 0 (calm) to 1 (activated)'),
- })
- .optional()
- .describe('Current mood - boosts recall of memories encoded in similar emotional state'),
- retrieval: z
- .enum(['hybrid', 'vector'])
- .optional()
- .describe(
- 'Retrieval strategy. hybrid is the default (vector + FTS/BM25 fusion); vector bypasses FTS for lower latency but loses lexical exact-match signal.',
- ),
- scope: z
- .enum(['agent', 'shared'])
- .optional()
- .describe(
- 'agent restricts recall to this MCP server agent identity. shared searches the whole store. Defaults to shared for backward compatibility.',
- ),
-};
-
-export const memoryImportToolSchema = {
- snapshot: importSnapshotSchema.describe('A validated snapshot from memory_export'),
-};
-
-export const memoryForgetToolSchema = {
- id: z.string().optional().describe('ID of the memory to forget'),
- query: z
- .string()
- .optional()
- .describe('Semantic query to find and forget the closest matching memory'),
- min_similarity: z
- .number()
- .min(0)
- .max(1)
- .optional()
- .describe('Minimum similarity for query-based forget (default 0.9)'),
- purge: z
- .boolean()
- .optional()
- .describe('Hard-delete the memory permanently (default false, soft-delete)'),
-};
-
-export const memoryValidateToolSchema = {
- id: z.string().describe('ID of the memory to validate'),
- outcome: z
- .enum(['used', 'helpful', 'wrong'])
- .describe(
- 'How the memory played out: "used" (referenced without obvious value), "helpful" (drove a correct action — reinforces salience and retrieval), "wrong" (memory was misleading — bumps challenge_count and decreases salience).',
- ),
-};
-
-export const memoryPreflightToolSchema = {
- action: z
- .string()
- .refine(isNonEmptyText, 'Action must not be empty')
- .describe('Natural-language description of the action the agent is about to take.'),
- tool: z
- .string()
- .optional()
- .describe('Tool or command family about to be used, e.g. Bash, npm test, Edit, deploy.'),
- session_id: z
- .string()
- .optional()
- .describe('Session identifier for grouping the optional preflight event.'),
- cwd: z.string().optional().describe('Working directory for the action.'),
- files: z
- .array(z.string())
- .optional()
- .describe('File paths to fingerprint if record_event is true.'),
- strict: z
- .boolean()
- .optional()
- .describe('If true, high-severity memory warnings produce decision=block instead of caution.'),
- limit: z
- .number()
- .int()
- .min(1)
- .max(50)
- .optional()
- .describe('Max recall results to consider before preflight categorization.'),
- budget_chars: z
- .number()
- .int()
- .min(200)
- .max(32000)
- .optional()
- .describe('Capsule budget in characters.'),
- mode: z
- .enum(['balanced', 'conservative', 'aggressive'])
- .optional()
- .describe('Underlying capsule mode. Defaults to conservative.'),
- failure_window_hours: z
- .number()
- .int()
- .min(1)
- .max(8760)
- .optional()
- .describe('How far back to check failed tool events. Defaults to 168 hours.'),
- include_status: z
- .boolean()
- .optional()
- .describe('Include memory health in the response and warning calculation. Defaults to true.'),
- record_event: z
- .boolean()
- .optional()
- .describe('Record a redacted PreToolUse event for this preflight. Defaults to false.'),
- include_capsule: z
- .boolean()
- .optional()
- .describe('If false, omit the embedded Memory Capsule from the response.'),
- scope: z
- .enum(['agent', 'shared'])
- .optional()
- .describe(
- 'agent restricts memory recall to this server agent identity. shared searches the whole store. Defaults to agent.',
- ),
-};
-
-const { record_event: _preflightRecordEvent, ...memoryGuardBeforeFields } =
- memoryPreflightToolSchema;
-export const memoryGuardBeforeToolSchema = {
- ...memoryGuardBeforeFields,
- session_id: z
- .string()
- .optional()
- .describe('Session identifier for grouping the required guard receipt event.'),
- files: z
- .array(z.string())
- .optional()
- .describe('File paths to fingerprint in the required guard receipt.'),
-};
-
-export const memoryGuardAfterToolSchema = {
- receipt_id: z
- .string()
- .refine(isNonEmptyText, 'Receipt id must not be empty')
- .describe('Receipt id returned by memory_guard_before.'),
- tool: z
- .string()
- .optional()
- .describe('Tool or command family that completed, e.g. Bash, npm test, Edit, deploy.'),
- session_id: z
- .string()
- .optional()
- .describe('Session identifier for grouping related guard events.'),
- input: z
- .unknown()
- .optional()
- .describe(
- 'Tool input. Hashed and never stored raw; redacted metadata is only stored when retain_details is true.',
- ),
- output: z
- .unknown()
- .optional()
- .describe('Tool output. Same redaction and storage policy as input.'),
- outcome: z
- .enum(['succeeded', 'failed', 'blocked', 'skipped', 'unknown'])
- .optional()
- .describe('Outcome classification'),
- error_summary: z
- .string()
- .optional()
- .describe('Short error description if the action failed. Redacted and truncated to 2 KB.'),
- cwd: z.string().optional().describe('Working directory at the time of the action.'),
- files: z
- .array(z.string())
- .optional()
- .describe('File paths to fingerprint (size + mtime + content hash).'),
- metadata: z
- .record(z.string(), z.unknown())
- .optional()
- .describe('Arbitrary structured metadata (redacted before storage).'),
- retain_details: z
- .boolean()
- .optional()
- .describe(
- 'If true, redacted input and output payloads are stored alongside hashes. Defaults to false.',
- ),
- evidence_feedback: z
- .record(z.string(), z.enum(['used', 'helpful', 'wrong']))
- .optional()
- .describe('Map of evidence ids from the guard receipt to memory validation outcomes.'),
-};
-
-export const memoryReflexesToolSchema = {
- ...memoryPreflightToolSchema,
- include_preflight: z
- .boolean()
- .optional()
- .describe('If true, include the full underlying preflight report.'),
-};
-
// ---------------------------------------------------------------------------
// Local interface for status reporting
// ---------------------------------------------------------------------------
diff --git a/mcp-server/tool-schemas.ts b/mcp-server/tool-schemas.ts
new file mode 100644
index 0000000..17b3c53
--- /dev/null
+++ b/mcp-server/tool-schemas.ts
@@ -0,0 +1,278 @@
+/**
+ * Zod tool-input schemas for the MCP memory tools. These are pure schema
+ * declarations consumed by the MCP server registration in index.ts and
+ * re-exported from there for tests and embedders.
+ */
+import { z } from 'zod';
+import { importSnapshotSchema } from '../src/import.js';
+import {
+ MAX_MEMORY_CONTENT_LENGTH,
+ VALID_SOURCES,
+ VALID_TYPES,
+ isNonEmptyText,
+} from './tool-validation.js';
+
+export const memoryEncodeToolSchema = {
+ content: z
+ .string()
+ .max(MAX_MEMORY_CONTENT_LENGTH)
+ .refine(isNonEmptyText, 'Content must not be empty')
+ .describe('The memory content to encode'),
+ source: z.enum(VALID_SOURCES).describe('Source type of the memory'),
+ tags: z.array(z.string()).optional().describe('Optional tags for categorization'),
+ salience: z.number().min(0).max(1).optional().describe('Importance weight 0-1'),
+ context: z
+ .record(z.string(), z.string())
+ .optional()
+ .describe(
+ 'Situational context as key-value pairs (e.g., {task: "debugging", domain: "payments"})',
+ ),
+ affect: z
+ .object({
+ valence: z
+ .number()
+ .min(-1)
+ .max(1)
+ .describe('Emotional valence: -1 (very negative) to 1 (very positive)'),
+ arousal: z
+ .number()
+ .min(0)
+ .max(1)
+ .optional()
+ .describe('Emotional arousal: 0 (calm) to 1 (highly activated)'),
+ label: z
+ .string()
+ .optional()
+ .describe('Human-readable emotion label (e.g., "curiosity", "frustration", "relief")'),
+ })
+ .optional()
+ .describe('Emotional affect - how this memory feels'),
+ private: z
+ .boolean()
+ .optional()
+ .describe('If true, memory is only visible to the AI and excluded from public recall results'),
+ wait_for_consolidation: z
+ .boolean()
+ .optional()
+ .describe(
+ 'If true, wait for post-encode validation/interference/resonance work before returning. Defaults to false.',
+ ),
+};
+
+export const memoryRecallToolSchema = {
+ query: z.string().describe('Search query to match against memories'),
+ limit: z.number().min(1).max(50).optional().describe('Max results (default 10)'),
+ types: z.array(z.enum(VALID_TYPES)).optional().describe('Memory types to search'),
+ min_confidence: z.number().min(0).max(1).optional().describe('Minimum confidence threshold'),
+ tags: z.array(z.string()).optional().describe('Only return episodic memories with these tags'),
+ sources: z
+ .array(z.enum(VALID_SOURCES))
+ .optional()
+ .describe('Only return episodic memories from these sources'),
+ after: z.string().optional().describe('Only return memories created after this ISO date'),
+ before: z.string().optional().describe('Only return memories created before this ISO date'),
+ context: z
+ .record(z.string(), z.string())
+ .optional()
+ .describe('Retrieval context - memories encoded in matching context get boosted'),
+ mood: z
+ .object({
+ valence: z
+ .number()
+ .min(-1)
+ .max(1)
+ .describe('Current emotional valence: -1 (negative) to 1 (positive)'),
+ arousal: z
+ .number()
+ .min(0)
+ .max(1)
+ .optional()
+ .describe('Current arousal: 0 (calm) to 1 (activated)'),
+ })
+ .optional()
+ .describe('Current mood - boosts recall of memories encoded in similar emotional state'),
+ retrieval: z
+ .enum(['hybrid', 'vector'])
+ .optional()
+ .describe(
+ 'Retrieval strategy. hybrid is the default (vector + FTS/BM25 fusion); vector bypasses FTS for lower latency but loses lexical exact-match signal.',
+ ),
+ scope: z
+ .enum(['agent', 'shared'])
+ .optional()
+ .describe(
+ 'agent restricts recall to this MCP server agent identity. shared searches the whole store. Defaults to shared for backward compatibility.',
+ ),
+};
+
+export const memoryImportToolSchema = {
+ snapshot: importSnapshotSchema.describe('A validated snapshot from memory_export'),
+};
+
+export const memoryForgetToolSchema = {
+ id: z.string().optional().describe('ID of the memory to forget'),
+ query: z
+ .string()
+ .optional()
+ .describe('Semantic query to find and forget the closest matching memory'),
+ min_similarity: z
+ .number()
+ .min(0)
+ .max(1)
+ .optional()
+ .describe('Minimum similarity for query-based forget (default 0.9)'),
+ purge: z
+ .boolean()
+ .optional()
+ .describe('Hard-delete the memory permanently (default false, soft-delete)'),
+};
+
+export const memoryValidateToolSchema = {
+ id: z.string().describe('ID of the memory to validate'),
+ outcome: z
+ .enum(['used', 'helpful', 'wrong'])
+ .describe(
+ 'How the memory played out: "used" (referenced without obvious value), "helpful" (drove a correct action — reinforces salience and retrieval), "wrong" (memory was misleading — bumps challenge_count and decreases salience).',
+ ),
+};
+
+export const memoryPreflightToolSchema = {
+ action: z
+ .string()
+ .refine(isNonEmptyText, 'Action must not be empty')
+ .describe('Natural-language description of the action the agent is about to take.'),
+ tool: z
+ .string()
+ .optional()
+ .describe('Tool or command family about to be used, e.g. Bash, npm test, Edit, deploy.'),
+ session_id: z
+ .string()
+ .optional()
+ .describe('Session identifier for grouping the optional preflight event.'),
+ cwd: z.string().optional().describe('Working directory for the action.'),
+ files: z
+ .array(z.string())
+ .optional()
+ .describe('File paths to fingerprint if record_event is true.'),
+ strict: z
+ .boolean()
+ .optional()
+ .describe('If true, high-severity memory warnings produce decision=block instead of caution.'),
+ limit: z
+ .number()
+ .int()
+ .min(1)
+ .max(50)
+ .optional()
+ .describe('Max recall results to consider before preflight categorization.'),
+ budget_chars: z
+ .number()
+ .int()
+ .min(200)
+ .max(32000)
+ .optional()
+ .describe('Capsule budget in characters.'),
+ mode: z
+ .enum(['balanced', 'conservative', 'aggressive'])
+ .optional()
+ .describe('Underlying capsule mode. Defaults to conservative.'),
+ failure_window_hours: z
+ .number()
+ .int()
+ .min(1)
+ .max(8760)
+ .optional()
+ .describe('How far back to check failed tool events. Defaults to 168 hours.'),
+ include_status: z
+ .boolean()
+ .optional()
+ .describe('Include memory health in the response and warning calculation. Defaults to true.'),
+ record_event: z
+ .boolean()
+ .optional()
+ .describe('Record a redacted PreToolUse event for this preflight. Defaults to false.'),
+ include_capsule: z
+ .boolean()
+ .optional()
+ .describe('If false, omit the embedded Memory Capsule from the response.'),
+ scope: z
+ .enum(['agent', 'shared'])
+ .optional()
+ .describe(
+ 'agent restricts memory recall to this server agent identity. shared searches the whole store. Defaults to agent.',
+ ),
+};
+
+const { record_event: _preflightRecordEvent, ...memoryGuardBeforeFields } =
+ memoryPreflightToolSchema;
+export const memoryGuardBeforeToolSchema = {
+ ...memoryGuardBeforeFields,
+ session_id: z
+ .string()
+ .optional()
+ .describe('Session identifier for grouping the required guard receipt event.'),
+ files: z
+ .array(z.string())
+ .optional()
+ .describe('File paths to fingerprint in the required guard receipt.'),
+};
+
+export const memoryGuardAfterToolSchema = {
+ receipt_id: z
+ .string()
+ .refine(isNonEmptyText, 'Receipt id must not be empty')
+ .describe('Receipt id returned by memory_guard_before.'),
+ tool: z
+ .string()
+ .optional()
+ .describe('Tool or command family that completed, e.g. Bash, npm test, Edit, deploy.'),
+ session_id: z
+ .string()
+ .optional()
+ .describe('Session identifier for grouping related guard events.'),
+ input: z
+ .unknown()
+ .optional()
+ .describe(
+ 'Tool input. Hashed and never stored raw; redacted metadata is only stored when retain_details is true.',
+ ),
+ output: z
+ .unknown()
+ .optional()
+ .describe('Tool output. Same redaction and storage policy as input.'),
+ outcome: z
+ .enum(['succeeded', 'failed', 'blocked', 'skipped', 'unknown'])
+ .optional()
+ .describe('Outcome classification'),
+ error_summary: z
+ .string()
+ .optional()
+ .describe('Short error description if the action failed. Redacted and truncated to 2 KB.'),
+ cwd: z.string().optional().describe('Working directory at the time of the action.'),
+ files: z
+ .array(z.string())
+ .optional()
+ .describe('File paths to fingerprint (size + mtime + content hash).'),
+ metadata: z
+ .record(z.string(), z.unknown())
+ .optional()
+ .describe('Arbitrary structured metadata (redacted before storage).'),
+ retain_details: z
+ .boolean()
+ .optional()
+ .describe(
+ 'If true, redacted input and output payloads are stored alongside hashes. Defaults to false.',
+ ),
+ evidence_feedback: z
+ .record(z.string(), z.enum(['used', 'helpful', 'wrong']))
+ .optional()
+ .describe('Map of evidence ids from the guard receipt to memory validation outcomes.'),
+};
+
+export const memoryReflexesToolSchema = {
+ ...memoryPreflightToolSchema,
+ include_preflight: z
+ .boolean()
+ .optional()
+ .describe('If true, include the full underlying preflight report.'),
+};
diff --git a/mcp-server/tool-validation.ts b/mcp-server/tool-validation.ts
new file mode 100644
index 0000000..4a27912
--- /dev/null
+++ b/mcp-server/tool-validation.ts
@@ -0,0 +1,59 @@
+/**
+ * Shared validation primitives and admin/embedding guards for the MCP tool and
+ * CLI surfaces. Kept separate from tool-schemas.ts and index.ts so both can
+ * import them without a circular dependency.
+ */
+import type { EmbeddingProvider } from '../src/types.js';
+
+export const VALID_SOURCES = [
+ 'direct-observation',
+ 'told-by-user',
+ 'tool-result',
+ 'inference',
+ 'model-generated',
+] as const;
+
+export const VALID_TYPES = ['episodic', 'semantic', 'procedural'] as const;
+
+export const MAX_MEMORY_CONTENT_LENGTH = 50_000;
+export const ADMIN_TOOLS_ENV = 'AUDREY_ENABLE_ADMIN_TOOLS';
+
+export function isNonEmptyText(value: unknown): boolean {
+ return typeof value === 'string' && value.trim().length > 0;
+}
+
+export function validateMemoryContent(content: string): void {
+ if (!isNonEmptyText(content)) {
+ throw new Error('content must be a non-empty string');
+ }
+ if (content.length > MAX_MEMORY_CONTENT_LENGTH) {
+ throw new Error(`content exceeds maximum length of ${MAX_MEMORY_CONTENT_LENGTH} characters`);
+ }
+}
+
+export function validateForgetSelection(id?: string, query?: string): void {
+ if ((id && query) || (!id && !query)) {
+ throw new Error('Provide exactly one of id or query');
+ }
+}
+
+export function isAdminToolsEnabled(
+ env: Record = process.env,
+): boolean {
+ const value = env[ADMIN_TOOLS_ENV]?.toLowerCase();
+ return value === '1' || value === 'true' || value === 'yes';
+}
+
+export function requireAdminTools(env: Record = process.env): void {
+ if (!isAdminToolsEnabled(env)) {
+ throw new Error(
+ `Admin memory tools are disabled. Set ${ADMIN_TOOLS_ENV}=1 to enable export, import, and forget operations.`,
+ );
+ }
+}
+
+export async function initializeEmbeddingProvider(provider: EmbeddingProvider): Promise {
+ if (provider && typeof provider.ready === 'function') {
+ await provider.ready();
+ }
+}
diff --git a/package-lock.json b/package-lock.json
index 71a84a5..66e5ba3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "audrey",
- "version": "1.0.2",
+ "version": "1.0.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "audrey",
- "version": "1.0.2",
+ "version": "1.0.3",
"license": "MIT",
"dependencies": {
"@hono/node-server": "^1.19.14",
diff --git a/package.json b/package.json
index 34a612b..137ee29 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "audrey",
- "version": "1.0.2",
+ "version": "1.0.3",
"description": "Local-first memory runtime for AI agents with recall, consolidation, memory reflexes, contradiction detection, and tool-trace learning",
"type": "module",
"main": "dist/src/index.js",
diff --git a/python/audrey_memory/_version.py b/python/audrey_memory/_version.py
index 7863915..976498a 100644
--- a/python/audrey_memory/_version.py
+++ b/python/audrey_memory/_version.py
@@ -1 +1 @@
-__version__ = "1.0.2"
+__version__ = "1.0.3"
diff --git a/tests/guardbench.test.js b/tests/guardbench.test.js
index e59ec1e..52f6dcb 100644
--- a/tests/guardbench.test.js
+++ b/tests/guardbench.test.js
@@ -652,7 +652,7 @@ describe('GuardBench harness', () => {
});
it('reports 1.0 release readiness without hiding publish blockers', async () => {
- const report = await verifyReleaseReadiness({ targetVersion: '1.0.2', allowPending: true });
+ const report = await verifyReleaseReadiness({ targetVersion: '1.0.3', allowPending: true });
expect(report.ok).toBe(true);
expect(report.ready).toBe(false);
@@ -672,11 +672,11 @@ describe('GuardBench harness', () => {
});
it('keeps the 1.0 release cut idempotent after it is applied', () => {
- const report = prepareReleaseCut({ targetVersion: '1.0.2', date: '2026-05-28' });
+ const report = prepareReleaseCut({ targetVersion: '1.0.3', date: '2026-05-28' });
expect(report.ok).toBe(true);
expect(report.apply).toBe(false);
- expect(report.currentVersions.packageJson).toBe('1.0.2');
+ expect(report.currentVersions.packageJson).toBe('1.0.3');
expect(report.files.filter(file => file.changed).map(file => file.path)).toEqual([]);
expect(report.nextCommands).toContain('npm run release:gate:paper');
});