@kisev/memomatic 1.0.0-dev.61.gdf462654550b → 1.0.0-dev.63.g16bac7f2191d
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/README.md +8 -6
- package/README.ru.md +8 -6
- package/dist/executor.js +13 -3
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -2
- package/dist/ingest.d.ts +0 -11
- package/dist/ingest.js +25 -54
- package/dist/service.js +18 -10
- package/dist/settings.d.ts +0 -1
- package/dist/settings.js +0 -10
- package/package.json +2 -2
- package/dist/bootstrap.d.ts +0 -19
- package/dist/bootstrap.js +0 -84
package/README.md
CHANGED
|
@@ -15,8 +15,9 @@ npm install --global @kisev/memomatic
|
|
|
15
15
|
|
|
16
16
|
The corpus lives under `$XDG_STATE_HOME/memomatic/` (`MEMORY.md`, `USER.md`,
|
|
17
17
|
daily notes, `DREAMS.md`); rules live in `$XDG_CONFIG_HOME/memomatic/MEMORY_RULES.md`.
|
|
18
|
-
Requires Node.js 22+.
|
|
19
|
-
|
|
18
|
+
Requires Node.js 22+. Connect `memomatic mcp-serve` as a local stdio MCP server
|
|
19
|
+
in OpenCode, Kilo, MiMo, or another MCP host and restart that host. No plugin or
|
|
20
|
+
automatic context injection is used. For background processing, run the CLI:
|
|
20
21
|
|
|
21
22
|
```bash
|
|
22
23
|
memomatic process # validate the inbox and index accepted entries (no model)
|
|
@@ -49,16 +50,17 @@ Every entry can carry a `source` annotation. Visibility derives from it:
|
|
|
49
50
|
`team-*`, `gitlab`, and `spec-manage` entries may be quoted in team-facing
|
|
50
51
|
artifacts; every other source (`people-journal`, `stopit`,
|
|
51
52
|
`mattermost-triage`, `task-*`, `docs-*`, `user`) is personal-only. The label
|
|
52
|
-
is exposed in search responses
|
|
53
|
+
is exposed in search responses.
|
|
53
54
|
|
|
54
55
|
## Surfaces
|
|
55
56
|
|
|
56
57
|
- `memomatic` CLI: `process`, `dream`, `search`, `status`, `index`.
|
|
57
58
|
- MCP stdio server with `memory_search`, `memory_get`, `memory_write`, and
|
|
58
59
|
`memory_forget` tools.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
|
|
61
|
+
The model initiates memory searches through visible MCP tool calls. Dream
|
|
62
|
+
processes OpenCode history separately; connecting another host does not import
|
|
63
|
+
its session history. Agentomatic and memomatic are installed independently.
|
|
62
64
|
|
|
63
65
|
Nothing is deleted without the explicit directives documented in
|
|
64
66
|
`MEMORY_RULES.md`.
|
package/README.ru.md
CHANGED
|
@@ -15,8 +15,9 @@ npm install --global @kisev/memomatic
|
|
|
15
15
|
|
|
16
16
|
Корпус живёт в `$XDG_STATE_HOME/memomatic/` (`MEMORY.md`, `USER.md`, ежедневные
|
|
17
17
|
записи, `DREAMS.md`); правила — в `$XDG_CONFIG_HOME/memomatic/MEMORY_RULES.md`.
|
|
18
|
-
Нужен Node.js 22+.
|
|
19
|
-
|
|
18
|
+
Нужен Node.js 22+. Подключите `memomatic mcp-serve` как локальный MCP-сервер stdio
|
|
19
|
+
в OpenCode, Kilo, MiMo или другом MCP-хосте и перезапустите хост. Плагин и
|
|
20
|
+
автоматическая подстановка контекста не используются. Фоновая обработка доступна через CLI:
|
|
20
21
|
|
|
21
22
|
```bash
|
|
22
23
|
memomatic process # валидация inbox и индексация принятого (без модели)
|
|
@@ -49,15 +50,16 @@ memomatic dream # полный проход: inbox + сессии + кон
|
|
|
49
50
|
записи `team-*`, `gitlab` и `spec-manage` можно цитировать в командных
|
|
50
51
|
артефактах; остальные источники (`people-journal`, `stopit`,
|
|
51
52
|
`mattermost-triage`, `task-*`, `docs-*`, `user`) — personal-only. Метка
|
|
52
|
-
возвращается в
|
|
53
|
+
возвращается в поиске.
|
|
53
54
|
|
|
54
55
|
## Поверхности
|
|
55
56
|
|
|
56
57
|
- CLI `memomatic`: `process`, `dream`, `search`, `status`, `index`.
|
|
57
58
|
- MCP-сервер stdio с инструментами `memory_search`, `memory_get`,
|
|
58
59
|
`memory_write` и `memory_forget`.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
|
|
61
|
+
Модель сама инициирует поиск памяти через видимые MCP-вызовы. Dream отдельно
|
|
62
|
+
обрабатывает историю OpenCode; подключение другого хоста не импортирует его
|
|
63
|
+
сессии. Agentomatic и memomatic устанавливаются независимо.
|
|
62
64
|
|
|
63
65
|
Без явных директив, описанных в `MEMORY_RULES.md`, ничего не удаляется.
|
package/dist/executor.js
CHANGED
|
@@ -23,16 +23,26 @@ export class OpenCodeExecutor {
|
|
|
23
23
|
this.options = options;
|
|
24
24
|
}
|
|
25
25
|
async complete(request) {
|
|
26
|
-
const args = ["run", "--agent", "build"];
|
|
26
|
+
const args = ["run", "--agent", "build", "--format", "json"];
|
|
27
27
|
if (this.options.model)
|
|
28
28
|
args.push("--model", this.options.model);
|
|
29
29
|
if (this.options.variant)
|
|
30
30
|
args.push("--variant", this.options.variant);
|
|
31
|
-
args.push("--
|
|
31
|
+
args.push("--", `[memomatic-internal] ${request.system}\n\n${request.prompt}`);
|
|
32
32
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
33
33
|
const output = await this.spawnOnce(args);
|
|
34
34
|
try {
|
|
35
|
-
const
|
|
35
|
+
const texts = [];
|
|
36
|
+
for (const line of output.split("\n")) {
|
|
37
|
+
if (!line.trim())
|
|
38
|
+
continue;
|
|
39
|
+
const event = JSON.parse(line);
|
|
40
|
+
if (event.type === "error")
|
|
41
|
+
throw new Error("opencode returned an error event");
|
|
42
|
+
if (event.type === "text" && typeof event.part?.text === "string")
|
|
43
|
+
texts.push(event.part.text);
|
|
44
|
+
}
|
|
45
|
+
const parsed = extractJson(texts.join("\n"));
|
|
36
46
|
return JSON.stringify(parsed);
|
|
37
47
|
}
|
|
38
48
|
catch {
|
package/dist/index.d.ts
CHANGED
|
@@ -4,5 +4,4 @@ export { visibilityForSource } from "./visibility.js";
|
|
|
4
4
|
export { dropToInbox, processInbox, withRunLock } from "./inbox.js";
|
|
5
5
|
export { forgetEntry, getEntry, openMemomatic, searchMemory, searchMemoryResults, writeEntry, } from "./service.js";
|
|
6
6
|
export type { MemomaticContext } from "./service.js";
|
|
7
|
-
export {
|
|
8
|
-
export { opencodeDatabasePath, sessionFacts } from "./ingest.js";
|
|
7
|
+
export { opencodeDatabasePath } from "./ingest.js";
|
package/dist/index.js
CHANGED
|
@@ -3,5 +3,4 @@ export { entryLine, parseEntryLine } from "./entries.js";
|
|
|
3
3
|
export { visibilityForSource } from "./visibility.js";
|
|
4
4
|
export { dropToInbox, processInbox, withRunLock } from "./inbox.js";
|
|
5
5
|
export { forgetEntry, getEntry, openMemomatic, searchMemory, searchMemoryResults, writeEntry, } from "./service.js";
|
|
6
|
-
export {
|
|
7
|
-
export { opencodeDatabasePath, sessionFacts } from "./ingest.js";
|
|
6
|
+
export { opencodeDatabasePath } from "./ingest.js";
|
package/dist/ingest.d.ts
CHANGED
|
@@ -9,17 +9,6 @@ export type IngestSession = {
|
|
|
9
9
|
}>;
|
|
10
10
|
};
|
|
11
11
|
export declare function opencodeDatabasePath(): string;
|
|
12
|
-
export type SessionFacts = {
|
|
13
|
-
directory: string | null;
|
|
14
|
-
title: string | null;
|
|
15
|
-
firstMessage: string | null;
|
|
16
|
-
};
|
|
17
|
-
/**
|
|
18
|
-
* Resolve session context for the bootstrap block: working directory, title,
|
|
19
|
-
* and the first user message, read-only from the OpenCode database. Returns
|
|
20
|
-
* nulls when the session or database is unavailable.
|
|
21
|
-
*/
|
|
22
|
-
export declare function sessionFacts(databaseFile: string, sessionId: string): SessionFacts;
|
|
23
12
|
export declare function loadRecentSessions(databaseFile: string, afterTimeCreated: number, options?: {
|
|
24
13
|
maxSessions: number;
|
|
25
14
|
maxCharsPerSession: number;
|
package/dist/ingest.js
CHANGED
|
@@ -9,60 +9,33 @@ export function opencodeDatabasePath() {
|
|
|
9
9
|
throw new Error("XDG_DATA_HOME must be an absolute path");
|
|
10
10
|
return join(base, "opencode", "opencode.db");
|
|
11
11
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
.get(sessionId);
|
|
29
|
-
let firstMessage = null;
|
|
30
|
-
const rows = db
|
|
31
|
-
.prepare(`SELECT data FROM message WHERE session_id = ? ORDER BY time_created ASC LIMIT 20;`)
|
|
32
|
-
.all(sessionId);
|
|
33
|
-
for (const row of rows) {
|
|
34
|
-
try {
|
|
35
|
-
const parsed = JSON.parse(row.data);
|
|
36
|
-
if (parsed.role !== "user")
|
|
37
|
-
continue;
|
|
38
|
-
const texts = [];
|
|
39
|
-
for (const part of parsed.parts ?? []) {
|
|
40
|
-
const value = part;
|
|
41
|
-
if (value.type === "text" && typeof value.text === "string")
|
|
42
|
-
texts.push(value.text);
|
|
12
|
+
function readMessages(db, sessionId) {
|
|
13
|
+
const hasParts = db
|
|
14
|
+
.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'part'")
|
|
15
|
+
.get();
|
|
16
|
+
const rows = db
|
|
17
|
+
.prepare(`SELECT ${hasParts ? "id, " : ""}data FROM message WHERE session_id = ? ORDER BY time_created ASC;`)
|
|
18
|
+
.all(sessionId);
|
|
19
|
+
if (!hasParts)
|
|
20
|
+
return rows;
|
|
21
|
+
const parts = db.prepare("SELECT data FROM part WHERE message_id = ? ORDER BY time_created ASC, id ASC");
|
|
22
|
+
return rows.map((row) => {
|
|
23
|
+
try {
|
|
24
|
+
const message = JSON.parse(row.data);
|
|
25
|
+
message.parts = parts.all(row.id).flatMap((part) => {
|
|
26
|
+
try {
|
|
27
|
+
return [JSON.parse(part.data)];
|
|
43
28
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
firstMessage = text;
|
|
47
|
-
break;
|
|
29
|
+
catch {
|
|
30
|
+
return [];
|
|
48
31
|
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
32
|
+
});
|
|
33
|
+
return { data: JSON.stringify(message) };
|
|
53
34
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
catch {
|
|
61
|
-
return { directory: null, title: null, firstMessage: null };
|
|
62
|
-
}
|
|
63
|
-
finally {
|
|
64
|
-
db.close();
|
|
65
|
-
}
|
|
35
|
+
catch {
|
|
36
|
+
return row;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
66
39
|
}
|
|
67
40
|
export function loadRecentSessions(databaseFile, afterTimeCreated, options = {
|
|
68
41
|
maxSessions: 20,
|
|
@@ -86,9 +59,7 @@ export function loadRecentSessions(databaseFile, afterTimeCreated, options = {
|
|
|
86
59
|
const messages = [];
|
|
87
60
|
let budget = options.maxCharsPerSession;
|
|
88
61
|
let internal = false;
|
|
89
|
-
const rows = db
|
|
90
|
-
.prepare(`SELECT data FROM message WHERE session_id = ? ORDER BY time_created ASC;`)
|
|
91
|
-
.all(session.id);
|
|
62
|
+
const rows = readMessages(db, session.id);
|
|
92
63
|
for (const row of rows) {
|
|
93
64
|
if (budget <= 0)
|
|
94
65
|
break;
|
package/dist/service.js
CHANGED
|
@@ -3,7 +3,7 @@ import { readTextIfExists, replaceEntryLine, writeCorpusFile } from "./corpus.js
|
|
|
3
3
|
import { assertSafePath } from "@kisev/safe-fs";
|
|
4
4
|
import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { rm } from "node:fs/promises";
|
|
6
|
-
import { dropToInbox } from "./inbox.js";
|
|
6
|
+
import { dropToInbox, withRunLock } from "./inbox.js";
|
|
7
7
|
import { isForbidden, loadRules } from "./rules.js";
|
|
8
8
|
import { memomaticPaths } from "./paths.js";
|
|
9
9
|
import { loadSettings } from "./settings.js";
|
|
@@ -79,15 +79,23 @@ export async function getEntry(context, file, line) {
|
|
|
79
79
|
return { file: target, line, content: lines[line - 1] };
|
|
80
80
|
}
|
|
81
81
|
export async function forgetEntry(context, target) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
82
|
+
return withRunLock(context.paths, async () => {
|
|
83
|
+
const file = await memoryPath(context, target.file);
|
|
84
|
+
const content = await readTextIfExists(file);
|
|
85
|
+
if (content === undefined)
|
|
86
|
+
throw new Error(`memory file not found: ${target.file}`);
|
|
87
|
+
const lines = content.split("\n");
|
|
88
|
+
if (!Number.isSafeInteger(target.line) || target.line < 1 || target.line > lines.length)
|
|
89
|
+
throw new Error("line is out of range");
|
|
90
|
+
await replaceEntryLine(context.paths, file, target.line, null);
|
|
91
|
+
context.store.replaceEntries(context.store
|
|
92
|
+
.allEntries()
|
|
93
|
+
.filter((entry) => entry.file !== file || entry.line !== target.line)
|
|
94
|
+
.map((entry) => entry.file === file && entry.line > target.line
|
|
95
|
+
? { ...entry, line: entry.line - 1 }
|
|
96
|
+
: entry));
|
|
97
|
+
return file;
|
|
98
|
+
});
|
|
91
99
|
}
|
|
92
100
|
export async function archiveOldEpisodic(context) {
|
|
93
101
|
const archived = [];
|
package/dist/settings.d.ts
CHANGED
package/dist/settings.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
export const defaultSettings = () => ({
|
|
3
3
|
embedding: null,
|
|
4
|
-
projects: {},
|
|
5
4
|
dream: {
|
|
6
5
|
model: null,
|
|
7
6
|
variant: null,
|
|
@@ -29,19 +28,10 @@ export function normalizeSettings(raw) {
|
|
|
29
28
|
const defaults = defaultSettings();
|
|
30
29
|
const source = (raw ?? {});
|
|
31
30
|
const embedding = source.embedding;
|
|
32
|
-
const rawProjects = source.projects;
|
|
33
|
-
const projects = {};
|
|
34
|
-
if (rawProjects && typeof rawProjects === "object") {
|
|
35
|
-
for (const [prefix, name] of Object.entries(rawProjects)) {
|
|
36
|
-
if (typeof name === "string" && name.trim() && prefix.trim())
|
|
37
|
-
projects[prefix] = name.trim();
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
31
|
return {
|
|
41
32
|
embedding: embedding && typeof embedding.url === "string" && typeof embedding.model === "string"
|
|
42
33
|
? { url: embedding.url, model: embedding.model }
|
|
43
34
|
: null,
|
|
44
|
-
projects,
|
|
45
35
|
dream: mergeSection(defaults.dream, source.dream),
|
|
46
36
|
search: mergeSection(defaults.search, source.search),
|
|
47
37
|
archive: mergeSection(defaults.archive, source.archive),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kisev/memomatic",
|
|
3
|
-
"version": "1.0.0-dev.
|
|
3
|
+
"version": "1.0.0-dev.63.g16bac7f2191d",
|
|
4
4
|
"description": "Personal learning memory for agents: tiered Markdown corpus, rebuildable index, dream sweep.",
|
|
5
5
|
"homepage": "https://github.com/kisev/skills#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"pack:check": "node test/pack-allowlist.mjs"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@kisev/safe-fs": "1.0.0-dev.
|
|
37
|
+
"@kisev/safe-fs": "1.0.0-dev.63.g16bac7f2191d"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/node": "22.15.30",
|
package/dist/bootstrap.d.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { MemomaticContext } from "./service.js";
|
|
2
|
-
export type SessionFacts = {
|
|
3
|
-
directory: string | null;
|
|
4
|
-
title: string | null;
|
|
5
|
-
firstMessage: string | null;
|
|
6
|
-
};
|
|
7
|
-
export type BootstrapOptions = {
|
|
8
|
-
curatedBudgetChars?: number;
|
|
9
|
-
episodicBudgetChars?: number;
|
|
10
|
-
};
|
|
11
|
-
/** Longest-prefix match of a working directory against the projects map. */
|
|
12
|
-
export declare function resolveProject(directory: string | null, projects: Record<string, string>): string | null;
|
|
13
|
-
/**
|
|
14
|
-
* Build the session bootstrap context: curated MEMORY.md and USER.md heads,
|
|
15
|
-
* plus episodic entries matched by project annotation and trigger phrases.
|
|
16
|
-
* Every episodic line carries its source visibility so team-facing artifacts
|
|
17
|
-
* never quote personal-only memory.
|
|
18
|
-
*/
|
|
19
|
-
export declare function bootstrapContext(context: MemomaticContext, facts: SessionFacts, options?: BootstrapOptions): Promise<string | null>;
|
package/dist/bootstrap.js
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { visibilityForSource } from "./visibility.js";
|
|
3
|
-
const DEFAULT_CURATED_BUDGET = 4_000;
|
|
4
|
-
const DEFAULT_EPISODIC_BUDGET = 1_500;
|
|
5
|
-
const MAX_BLOCK_ENTRIES = 8;
|
|
6
|
-
/** Longest-prefix match of a working directory against the projects map. */
|
|
7
|
-
export function resolveProject(directory, projects) {
|
|
8
|
-
if (!directory)
|
|
9
|
-
return null;
|
|
10
|
-
let best = null;
|
|
11
|
-
for (const [prefix, name] of Object.entries(projects)) {
|
|
12
|
-
if (directory === prefix || directory.startsWith(prefix.endsWith("/") ? prefix : `${prefix}/`))
|
|
13
|
-
if (!best || prefix.length > best.prefix.length)
|
|
14
|
-
best = { prefix, name };
|
|
15
|
-
}
|
|
16
|
-
return best?.name ?? null;
|
|
17
|
-
}
|
|
18
|
-
function renderEntry(entry) {
|
|
19
|
-
const visibility = visibilityForSource(entry.source);
|
|
20
|
-
const label = entry.source ? `source: ${entry.source}, ${visibility}-only` : `${visibility}-only`;
|
|
21
|
-
const text = entry.text.startsWith("- ") ? entry.text.slice(2) : entry.text;
|
|
22
|
-
return `- ${text} (${label})`;
|
|
23
|
-
}
|
|
24
|
-
function rankEntries(entries) {
|
|
25
|
-
return [...entries].sort((left, right) => {
|
|
26
|
-
if (right.importance !== left.importance)
|
|
27
|
-
return right.importance - left.importance;
|
|
28
|
-
return (right.observedAt ?? 0) - (left.observedAt ?? 0);
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
function fitsBudget(lines, budget) {
|
|
32
|
-
const result = [];
|
|
33
|
-
let used = 0;
|
|
34
|
-
for (const line of lines) {
|
|
35
|
-
if (result.length >= MAX_BLOCK_ENTRIES)
|
|
36
|
-
break;
|
|
37
|
-
if (used + line.length > budget)
|
|
38
|
-
break;
|
|
39
|
-
result.push(line);
|
|
40
|
-
used += line.length + 1;
|
|
41
|
-
}
|
|
42
|
-
return result;
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Build the session bootstrap context: curated MEMORY.md and USER.md heads,
|
|
46
|
-
* plus episodic entries matched by project annotation and trigger phrases.
|
|
47
|
-
* Every episodic line carries its source visibility so team-facing artifacts
|
|
48
|
-
* never quote personal-only memory.
|
|
49
|
-
*/
|
|
50
|
-
export async function bootstrapContext(context, facts, options = {}) {
|
|
51
|
-
const curatedBudget = options.curatedBudgetChars ?? DEFAULT_CURATED_BUDGET;
|
|
52
|
-
const episodicBudget = options.episodicBudgetChars ?? DEFAULT_EPISODIC_BUDGET;
|
|
53
|
-
const blocks = [];
|
|
54
|
-
for (const file of [context.paths.memoryFile, context.paths.userFile]) {
|
|
55
|
-
const content = await readFile(file, "utf8").catch(() => undefined);
|
|
56
|
-
if (content === undefined || !content.trim())
|
|
57
|
-
continue;
|
|
58
|
-
const name = file.slice(file.lastIndexOf("/") + 1);
|
|
59
|
-
blocks.push(`# Memomatic ${name}\n\n${content.trim().slice(0, curatedBudget)}`);
|
|
60
|
-
}
|
|
61
|
-
const entries = context.store.allEntries().filter((entry) => entry.status === null);
|
|
62
|
-
const project = resolveProject(facts.directory, context.settings.projects);
|
|
63
|
-
const usedTexts = new Set();
|
|
64
|
-
if (project) {
|
|
65
|
-
const lines = rankEntries(entries.filter((entry) => entry.kind === "episodic" && entry.project === project)).map((entry) => {
|
|
66
|
-
usedTexts.add(entry.text);
|
|
67
|
-
return renderEntry(entry);
|
|
68
|
-
});
|
|
69
|
-
const fitted = fitsBudget(lines, episodicBudget);
|
|
70
|
-
if (fitted.length)
|
|
71
|
-
blocks.push(`# Memomatic project recall (${project})\n\n${fitted.join("\n")}\n\nEntries marked personal-only must never be quoted in team-facing artifacts.`);
|
|
72
|
-
}
|
|
73
|
-
const haystack = [facts.title ?? "", facts.firstMessage ?? ""].join("\n").toLowerCase();
|
|
74
|
-
if (haystack.trim()) {
|
|
75
|
-
const triggered = rankEntries(entries.filter((entry) => !usedTexts.has(entry.text) &&
|
|
76
|
-
entry.trigger.some((phrase) => phrase.trim() && haystack.includes(phrase.toLowerCase())))).map((entry) => renderEntry(entry));
|
|
77
|
-
const fitted = fitsBudget(triggered, episodicBudget);
|
|
78
|
-
if (fitted.length)
|
|
79
|
-
blocks.push(`# Memomatic triggered recall\n\n${fitted.join("\n")}\n\nEntries marked personal-only must never be quoted in team-facing artifacts.`);
|
|
80
|
-
}
|
|
81
|
-
if (!blocks.length)
|
|
82
|
-
return null;
|
|
83
|
-
return `${blocks.join("\n\n")}\n\nUse memory_search for details and memory_write to queue durable outcomes. Never re-save content that is already present in this memory.`;
|
|
84
|
-
}
|