feat(memory): enforce English-only memory indexing with CJK guardrail
Memory layer is now English-indexed: memories() query terms and remember() summaries must be English. Adds a runtime assertion that rejects CJK text in both paths, guiding the agent to translate non-English user requests before querying or writing memories. Ensures consistent retrieval regardless of conversation language.
This commit is contained in:
@@ -59,7 +59,7 @@ session evidence before deciding whether a detail pass is needed:
|
|||||||
```js
|
```js
|
||||||
const map = overview({ limit: 6 });
|
const map = overview({ limit: 6 });
|
||||||
const project = map.current.project?.project;
|
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 {
|
return {
|
||||||
orientation: map.current_project,
|
orientation: map.current_project,
|
||||||
@@ -161,7 +161,7 @@ tiny sample before relying on less common filters.
|
|||||||
- `trace(uuid)` -- parent chain from root to message.
|
- `trace(uuid)` -- parent chain from root to message.
|
||||||
- `thread(sessionId)` -- full session messages; last resort only.
|
- `thread(sessionId)` -- full session messages; last resort only.
|
||||||
- `raw(uuid, opts?)` -- windowed access to the original JSONL line.
|
- `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
|
## 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
|
depends on it. Raw session data is the evidence layer, but one hit is not a
|
||||||
complete truth; query and cite it compactly.
|
complete truth; query and cite it compactly.
|
||||||
|
|
||||||
**Recall:** query `memories({ query: 'topic terms', project: '...' })` to find
|
The memory layer is English-indexed. Use English terms in `memories({ query })`
|
||||||
prior conclusions relevant to the current task. Like other list helpers,
|
even when the user asks in another language. Write every `remember().summary`
|
||||||
passing a string is treated as `sessionId`, and passing a number is treated as
|
in English, regardless of the current conversation language. The runtime rejects
|
||||||
`limit`. Read the file at `path` for full content.
|
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
|
Good memory candidates include design decisions, project conventions, abandoned
|
||||||
alternatives, repeated failure causes, workflow patterns, and conclusions
|
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
|
`session_id` is provided, then stored as normalized absolute paths. Prefer
|
||||||
project-relative paths such as `.obelisk/memories/...` plus `session_id`.
|
project-relative paths such as `.obelisk/memories/...` plus `session_id`.
|
||||||
|
|
||||||
`summary` should be detailed enough that `memories()` results alone can judge
|
`summary` must be English and detailed enough that `memories()` results alone
|
||||||
relevance without reading the file. Include the decision, the reasoning, and
|
can judge relevance without reading the file. Include the decision, the
|
||||||
the key constraints — not just a title.
|
reasoning, and the key constraints — not just a title.
|
||||||
|
|
||||||
The `message_start`/`message_end` range marks where in the conversation this
|
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.
|
conclusion was drawn. Use it later to trace back to the original evidence.
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ faceted detail pass if the first pass reveals useful projects, sessions, files,
|
|||||||
or terms.
|
or terms.
|
||||||
|
|
||||||
```js
|
```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 map = overview({ limit: 6 });
|
||||||
const project = map.current.project?.project;
|
const project = map.current.project?.project;
|
||||||
const scoped = 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
|
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
|
session evidence. Keep memory as prior notes, not final authority; compare it
|
||||||
with session evidence in your final answer when correctness matters.
|
with session evidence in your final answer when correctness matters.
|
||||||
|
Memory query terms are English even when the user asks in another language.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const project = '%quiet-zero%';
|
const project = '%quiet-zero%';
|
||||||
|
|||||||
@@ -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
|
Then synthesize the conclusion in the final answer. Do not pretend the raw
|
||||||
evidence view is itself a stored Obelisk entity.
|
evidence view is itself a stored Obelisk entity.
|
||||||
|
|
||||||
|
|||||||
+10
-7
@@ -527,7 +527,7 @@ is treated as `sessionId`, and passing a number is treated as `limit`.
|
|||||||
|
|
||||||
| Param | Type | Description |
|
| 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.project` | `string` | SQL `LIKE` pattern over `memories.project` |
|
||||||
| `opts.sessionId` | `string` | Restrict to one source session |
|
| `opts.sessionId` | `string` | Restrict to one source session |
|
||||||
| `opts.sessions` | `string[]` | Restrict to a set of source session IDs |
|
| `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<memory_row>` ordered by `created_at` descending.
|
**Returns:** `Array<memory_row>` ordered by `created_at` descending.
|
||||||
|
|
||||||
`query` is a lightweight term filter, not FTS5 ranking. Use it to avoid pulling
|
`query` is a lightweight English term filter, not FTS5 ranking. Translate
|
||||||
all recent memories, then read the markdown file at `path` when a memory looks
|
non-English user requests into concise English query terms before calling
|
||||||
relevant.
|
`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
|
```js
|
||||||
const prior = memories({
|
const prior = memories({
|
||||||
@@ -569,14 +571,15 @@ normal `--query` script.
|
|||||||
| Param | Type | Description |
|
| 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.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.session_id` | `string` | Source session ID, if known |
|
||||||
| `record.message_start` | `string` | First relevant source message UUID, 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.message_end` | `string` | Last relevant source message UUID, if known |
|
||||||
| `record.project` | `string` | Project slug override. Defaults from `sessions.project` for `session_id` |
|
| `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
|
`remember()` validates that `path` exists and is a regular file, and rejects
|
||||||
normalized absolute path in `memories.path`.
|
obvious CJK text in `summary`. It stores the normalized absolute path in
|
||||||
|
`memories.path`.
|
||||||
|
|
||||||
**Returns:** `{ id, path, project, created_at }`.
|
**Returns:** `{ id, path, project, created_at }`.
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
function createQueryApi(db) {
|
||||||
const q = (sql, ...p) => {
|
const q = (sql, ...p) => {
|
||||||
assertReadOnlySql(sql);
|
assertReadOnlySql(sql);
|
||||||
@@ -403,6 +414,7 @@ function createQueryApi(db) {
|
|||||||
const memories = (optsOrSid) => {
|
const memories = (optsOrSid) => {
|
||||||
const opts = normalizeOpts(optsOrSid);
|
const opts = normalizeOpts(optsOrSid);
|
||||||
const { limit = 50, query } = opts;
|
const { limit = 50, query } = opts;
|
||||||
|
assertEnglishMemoryText(query, 'memories() query');
|
||||||
const needsJoin = opts.branch;
|
const needsJoin = opts.branch;
|
||||||
const { where: baseWhere, params } = buildWhere(opts, {
|
const { where: baseWhere, params } = buildWhere(opts, {
|
||||||
sessionId: 'mem.session_id',
|
sessionId: 'mem.session_id',
|
||||||
@@ -449,6 +461,7 @@ function createRememberApi(db) {
|
|||||||
|
|
||||||
const remember = ({ path: memoryPath, session_id, message_start, message_end, summary, project }) => {
|
const remember = ({ path: memoryPath, session_id, message_start, message_end, summary, project }) => {
|
||||||
if (!memoryPath || !summary) throw new Error('remember() requires path and summary');
|
if (!memoryPath || !summary) throw new Error('remember() requires path and summary');
|
||||||
|
assertEnglishMemoryText(summary, 'remember() summary');
|
||||||
const normalizedPath = resolveMemoryPath(memoryPath, session_id);
|
const normalizedPath = resolveMemoryPath(memoryPath, session_id);
|
||||||
const id = `mem-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
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;
|
const proj = project || db.prepare('SELECT project FROM sessions WHERE id=?').get(session_id)?.project || null;
|
||||||
|
|||||||
Reference in New Issue
Block a user