diff --git a/SKILL.md b/SKILL.md index 49b4045..b15a6e9 100644 --- a/SKILL.md +++ b/SKILL.md @@ -59,7 +59,7 @@ session evidence before deciding whether a detail pass is needed: ```js const map = overview({ limit: 6 }); const project = map.current.project?.project; -const topic = 'topic terms from the user request'; +const topic = 'English topic terms translated from the user request'; return { orientation: map.current_project, @@ -161,7 +161,7 @@ tiny sample before relying on less common filters. - `trace(uuid)` -- parent chain from root to message. - `thread(sessionId)` -- full session messages; last resort only. - `raw(uuid, opts?)` -- windowed access to the original JSONL line. -- `memories(opts?)` -- recall memory layer, newest first. opts: `{ query, project, sessionId, sessions, after, before, branch, limit }`. `query` filters summary/path by terms. Returns registered memory records (id, path, summary, project, session_id, created_at). Read the file at `path` for full content. +- `memories(opts?)` -- recall memory layer, newest first. opts: `{ query, project, sessionId, sessions, after, before, branch, limit }`. `query` filters summary/path by English terms. Returns registered memory records (id, path, summary, project, session_id, created_at). Read the file at `path` for full content. ## Retrieval Contract @@ -189,10 +189,16 @@ previously recorded, and compare it with raw session evidence when correctness depends on it. Raw session data is the evidence layer, but one hit is not a complete truth; query and cite it compactly. -**Recall:** query `memories({ query: 'topic terms', project: '...' })` to find -prior conclusions relevant to the current task. Like other list helpers, -passing a string is treated as `sessionId`, and passing a number is treated as -`limit`. Read the file at `path` for full content. +The memory layer is English-indexed. Use English terms in `memories({ query })` +even when the user asks in another language. Write every `remember().summary` +in English, regardless of the current conversation language. The runtime rejects +obvious CJK text in memory queries and summaries as a guardrail. + +**Recall:** query `memories({ query: 'English topic terms', project: '...' })` +to find prior conclusions relevant to the current task. Translate non-English +user requests into concise English query terms before calling `memories()`. Like +other list helpers, passing a string is treated as `sessionId`, and passing a +number is treated as `limit`. Read the file at `path` for full content. Good memory candidates include design decisions, project conventions, abandoned alternatives, repeated failure causes, workflow patterns, and conclusions @@ -231,9 +237,9 @@ paths are resolved against the source session's `project_path` when `session_id` is provided, then stored as normalized absolute paths. Prefer project-relative paths such as `.obelisk/memories/...` plus `session_id`. -`summary` should be detailed enough that `memories()` results alone can judge -relevance without reading the file. Include the decision, the reasoning, and -the key constraints — not just a title. +`summary` must be English and detailed enough that `memories()` results alone +can judge relevance without reading the file. Include the decision, the +reasoning, and the key constraints — not just a title. The `message_start`/`message_end` range marks where in the conversation this conclusion was drawn. Use it later to trace back to the original evidence. diff --git a/references/query-patterns.md b/references/query-patterns.md index e48a2b5..3a25cd4 100644 --- a/references/query-patterns.md +++ b/references/query-patterns.md @@ -18,7 +18,7 @@ faceted detail pass if the first pass reveals useful projects, sessions, files, or terms. ```js -const topic = 'topic terms from the user request'; +const topic = 'English topic terms translated from the user request'; const map = overview({ limit: 6 }); const project = map.current.project?.project; const scoped = project ? { project } : {}; @@ -124,6 +124,7 @@ return hits.slice(0, 5).map(h => { Use this when prior conclusions may exist but the answer still depends on raw session evidence. Keep memory as prior notes, not final authority; compare it with session evidence in your final answer when correctness matters. +Memory query terms are English even when the user asks in another language. ```js const project = '%quiet-zero%'; diff --git a/references/retrieval-semantics.md b/references/retrieval-semantics.md index ae7baf6..74d5c6c 100644 --- a/references/retrieval-semantics.md +++ b/references/retrieval-semantics.md @@ -106,6 +106,11 @@ For semantic questions, build a task-local evidence view: } ``` +Memory recall is English-indexed: translate non-English user requests into +concise English query terms before calling `memories({ query })`. Memory +summaries registered with `remember()` are also English, regardless of the +conversation language. + Then synthesize the conclusion in the final answer. Do not pretend the raw evidence view is itself a stored Obelisk entity. diff --git a/references/schema.md b/references/schema.md index 517fb04..835005b 100644 --- a/references/schema.md +++ b/references/schema.md @@ -527,7 +527,7 @@ is treated as `sessionId`, and passing a number is treated as `limit`. | Param | Type | Description | |-------|------|-------------| -| `opts.query` | `string` | Term filter over `summary` and `path`; hyphens/underscores are treated as spaces | +| `opts.query` | `string` | English term filter over `summary` and `path`; hyphens/underscores are treated as spaces | | `opts.project` | `string` | SQL `LIKE` pattern over `memories.project` | | `opts.sessionId` | `string` | Restrict to one source session | | `opts.sessions` | `string[]` | Restrict to a set of source session IDs | @@ -538,9 +538,11 @@ is treated as `sessionId`, and passing a number is treated as `limit`. **Returns:** `Array` ordered by `created_at` descending. -`query` is a lightweight term filter, not FTS5 ranking. Use it to avoid pulling -all recent memories, then read the markdown file at `path` when a memory looks -relevant. +`query` is a lightweight English term filter, not FTS5 ranking. Translate +non-English user requests into concise English query terms before calling +`memories()`. Use it to avoid pulling all recent memories, then read the +markdown file at `path` when a memory looks relevant. The runtime rejects +obvious CJK text in memory queries. ```js const prior = memories({ @@ -569,14 +571,15 @@ normal `--query` script. | Param | Type | Description | |-------|------|-------------| | `record.path` | `string` | Existing markdown file path. Relative paths resolve against the source session `project_path` when `session_id` is provided, otherwise against the runtime cwd | -| `record.summary` | `string` | Required retrieval summary: decision, reasoning, constraints | +| `record.summary` | `string` | Required English retrieval summary: decision, reasoning, constraints | | `record.session_id` | `string` | Source session ID, if known | | `record.message_start` | `string` | First relevant source message UUID, if known | | `record.message_end` | `string` | Last relevant source message UUID, if known | | `record.project` | `string` | Project slug override. Defaults from `sessions.project` for `session_id` | -`remember()` validates that `path` exists and is a regular file. It stores the -normalized absolute path in `memories.path`. +`remember()` validates that `path` exists and is a regular file, and rejects +obvious CJK text in `summary`. It stores the normalized absolute path in +`memories.path`. **Returns:** `{ id, path, project, created_at }`. diff --git a/scripts/query.mjs b/scripts/query.mjs index 12152c1..f8a1ea5 100644 --- a/scripts/query.mjs +++ b/scripts/query.mjs @@ -34,6 +34,17 @@ function assertReadOnlySql(sql) { } } +const CJK_TEXT_RE = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u; + +function assertEnglishMemoryText(value, label) { + const text = String(value || ''); + if (!text.trim()) return; + if (CJK_TEXT_RE.test(text)) { + const requirement = label.includes('query') ? 'must use English terms' : 'must be written in English'; + throw new Error(`${label} ${requirement}; translate user-language terms before using the memory layer`); + } +} + function createQueryApi(db) { const q = (sql, ...p) => { assertReadOnlySql(sql); @@ -403,6 +414,7 @@ function createQueryApi(db) { const memories = (optsOrSid) => { const opts = normalizeOpts(optsOrSid); const { limit = 50, query } = opts; + assertEnglishMemoryText(query, 'memories() query'); const needsJoin = opts.branch; const { where: baseWhere, params } = buildWhere(opts, { sessionId: 'mem.session_id', @@ -449,6 +461,7 @@ function createRememberApi(db) { const remember = ({ path: memoryPath, session_id, message_start, message_end, summary, project }) => { if (!memoryPath || !summary) throw new Error('remember() requires path and summary'); + assertEnglishMemoryText(summary, 'remember() summary'); const normalizedPath = resolveMemoryPath(memoryPath, session_id); const id = `mem-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; const proj = project || db.prepare('SELECT project FROM sessions WHERE id=?').get(session_id)?.project || null;