@kisev/memomatic 1.0.0-dev.100.g21366c4ad740
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 +97 -0
- package/README.ru.md +95 -0
- package/assets/systemd/memomatic-dream.service +9 -0
- package/assets/systemd/memomatic-dream.timer +10 -0
- package/assets/systemd/memomatic-sessions.service +9 -0
- package/assets/systemd/memomatic-sessions.timer +10 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +309 -0
- package/dist/corpus.d.ts +12 -0
- package/dist/corpus.js +138 -0
- package/dist/dream.d.ts +31 -0
- package/dist/dream.js +234 -0
- package/dist/entries.d.ts +31 -0
- package/dist/entries.js +119 -0
- package/dist/executor.d.ts +45 -0
- package/dist/executor.js +267 -0
- package/dist/gates.d.ts +27 -0
- package/dist/gates.js +69 -0
- package/dist/generated/cli.d.ts +28 -0
- package/dist/generated/cli.js +181 -0
- package/dist/inbox.d.ts +34 -0
- package/dist/inbox.js +224 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +6 -0
- package/dist/ingest.d.ts +1 -0
- package/dist/ingest.js +15 -0
- package/dist/ingestion.d.ts +30 -0
- package/dist/ingestion.js +179 -0
- package/dist/mcp.d.ts +15 -0
- package/dist/mcp.js +205 -0
- package/dist/opencode-config.d.ts +1 -0
- package/dist/opencode-config.js +84 -0
- package/dist/operations.d.ts +7 -0
- package/dist/operations.js +3 -0
- package/dist/paths.d.ts +18 -0
- package/dist/paths.js +55 -0
- package/dist/rules.d.ts +13 -0
- package/dist/rules.js +39 -0
- package/dist/search.d.ts +28 -0
- package/dist/search.js +191 -0
- package/dist/service.d.ts +63 -0
- package/dist/service.js +163 -0
- package/dist/sessions.d.ts +36 -0
- package/dist/sessions.js +238 -0
- package/dist/settings.d.ts +48 -0
- package/dist/settings.js +189 -0
- package/dist/store.d.ts +55 -0
- package/dist/store.js +286 -0
- package/dist/visibility.d.ts +7 -0
- package/dist/visibility.js +13 -0
- package/package.json +48 -0
package/dist/corpus.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstat, mkdir, open, readFile, rm } from "node:fs/promises";
|
|
3
|
+
import { dirname, join, relative } from "node:path";
|
|
4
|
+
import { writeAtomic } from "@kisev/safe-fs";
|
|
5
|
+
import { parseEntryLine } from "./entries.js";
|
|
6
|
+
const CURATED_FILES = ["MEMORY.md", "USER.md"];
|
|
7
|
+
function inside(root, target) {
|
|
8
|
+
const value = relative(root, target);
|
|
9
|
+
return value === "" || (!value.startsWith("..") && value !== "..");
|
|
10
|
+
}
|
|
11
|
+
async function safeDirectory(path, boundary, create) {
|
|
12
|
+
if (!inside(boundary, path))
|
|
13
|
+
throw new Error("memomatic path escapes its root");
|
|
14
|
+
if (create)
|
|
15
|
+
await mkdir(path, { mode: 0o700, recursive: true });
|
|
16
|
+
let current = boundary;
|
|
17
|
+
for (const piece of relative(current, path).split("/").filter(Boolean)) {
|
|
18
|
+
current = join(current, piece);
|
|
19
|
+
const info = await lstat(current);
|
|
20
|
+
if (!info.isDirectory() || info.isSymbolicLink())
|
|
21
|
+
throw new Error(`memomatic directory is unsafe: ${current}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function writePreImage(paths, source, previous) {
|
|
25
|
+
await safeDirectory(paths.historyDir, paths.stateRoot, true);
|
|
26
|
+
const name = `${createHash("sha256").update(previous).digest("hex")}.md`;
|
|
27
|
+
const target = join(paths.historyDir, name);
|
|
28
|
+
try {
|
|
29
|
+
const file = await open(target, "wx", 0o600);
|
|
30
|
+
try {
|
|
31
|
+
await file.writeFile(previous);
|
|
32
|
+
await file.sync();
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
await file.close();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (error.code !== "EEXIST")
|
|
40
|
+
throw error;
|
|
41
|
+
const info = await lstat(target);
|
|
42
|
+
if (!info.isFile() || info.isSymbolicLink() || !(await readFile(target)).equals(previous))
|
|
43
|
+
throw new Error("memomatic history was changed");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export async function readTextIfExists(path) {
|
|
47
|
+
try {
|
|
48
|
+
return await readFile(path, "utf8");
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (error.code === "ENOENT")
|
|
52
|
+
return undefined;
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export async function writeCorpusFile(paths, file, next) {
|
|
57
|
+
if (!inside(paths.stateRoot, file))
|
|
58
|
+
throw new Error("memomatic path escapes its root");
|
|
59
|
+
const previous = await readFile(file).catch(() => undefined);
|
|
60
|
+
if (previous !== undefined && previous.toString("utf8") === next)
|
|
61
|
+
return;
|
|
62
|
+
await safeDirectory(dirname(file), paths.stateRoot, true);
|
|
63
|
+
if (previous !== undefined)
|
|
64
|
+
await writePreImage(paths, file, previous);
|
|
65
|
+
await writeAtomic(file, Buffer.from(next), 0o600);
|
|
66
|
+
}
|
|
67
|
+
export function dailyNotePath(paths, date = new Date()) {
|
|
68
|
+
const iso = date.toISOString().slice(0, 10);
|
|
69
|
+
return join(paths.dailyDir, `${iso}.md`);
|
|
70
|
+
}
|
|
71
|
+
export function entryKind(file) {
|
|
72
|
+
const name = file.slice(file.lastIndexOf("/") + 1);
|
|
73
|
+
if (name === "MEMORY.md")
|
|
74
|
+
return "curated";
|
|
75
|
+
if (name === "USER.md")
|
|
76
|
+
return "user";
|
|
77
|
+
return "episodic";
|
|
78
|
+
}
|
|
79
|
+
export function parseCorpusEntries(markdown, file) {
|
|
80
|
+
const entries = [];
|
|
81
|
+
const lines = markdown.split("\n");
|
|
82
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
83
|
+
const parsed = parseEntryLine(lines[index]);
|
|
84
|
+
if (!parsed)
|
|
85
|
+
continue;
|
|
86
|
+
entries.push({ file, line: index + 1, text: parsed.text, annotations: parsed.annotations });
|
|
87
|
+
}
|
|
88
|
+
return entries;
|
|
89
|
+
}
|
|
90
|
+
export async function appendDailyEntry(paths, line, date = new Date()) {
|
|
91
|
+
const file = dailyNotePath(paths, date);
|
|
92
|
+
const current = (await readTextIfExists(file)) ?? "";
|
|
93
|
+
const next = current.length ? `${current.trimEnd()}\n${line}\n` : `${line}\n`;
|
|
94
|
+
await writeCorpusFile(paths, file, next);
|
|
95
|
+
return file;
|
|
96
|
+
}
|
|
97
|
+
export async function replaceEntryLine(paths, file, line, replacement) {
|
|
98
|
+
const content = await readFile(file, "utf8");
|
|
99
|
+
const lines = content.split("\n");
|
|
100
|
+
if (line < 1 || line > lines.length)
|
|
101
|
+
throw new Error("entry line is out of range");
|
|
102
|
+
if (replacement === null) {
|
|
103
|
+
lines.splice(line - 1, 1);
|
|
104
|
+
const rest = lines.join("\n");
|
|
105
|
+
await writeCorpusFile(paths, file, rest.length ? `${rest}` : "");
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
lines[line - 1] = replacement;
|
|
109
|
+
await writeCorpusFile(paths, file, lines.join("\n"));
|
|
110
|
+
}
|
|
111
|
+
export async function appendDreams(paths, report) {
|
|
112
|
+
const stamp = new Date().toISOString().replace("T", " ").slice(0, 16);
|
|
113
|
+
const current = (await readTextIfExists(paths.dreamsFile)) ?? "";
|
|
114
|
+
const block = `## ${stamp}\n\n${report.trim()}\n\n`;
|
|
115
|
+
const next = `${current}${current.length ? "\n" : ""}${block}`;
|
|
116
|
+
await writeCorpusFile(paths, paths.dreamsFile, next);
|
|
117
|
+
}
|
|
118
|
+
export async function archiveFile(paths, file) {
|
|
119
|
+
if (!inside(paths.stateRoot, file))
|
|
120
|
+
throw new Error("memomatic path escapes its root");
|
|
121
|
+
const name = file.slice(file.lastIndexOf("/") + 1);
|
|
122
|
+
const target = join(paths.archiveDir, `${name}`);
|
|
123
|
+
await safeDirectory(paths.archiveDir, paths.stateRoot, true);
|
|
124
|
+
const content = await readFile(file);
|
|
125
|
+
const existing = await readFile(target).catch(() => undefined);
|
|
126
|
+
if (existing !== undefined) {
|
|
127
|
+
const merged = `${existing.toString("utf8").trimEnd()}\n${content.toString("utf8").trimEnd()}\n`;
|
|
128
|
+
await writeAtomic(target, Buffer.from(merged), 0o600);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
await writeAtomic(target, content, 0o600);
|
|
132
|
+
}
|
|
133
|
+
await rm(file);
|
|
134
|
+
return target;
|
|
135
|
+
}
|
|
136
|
+
export function isCuratedFile(file) {
|
|
137
|
+
return CURATED_FILES.some((name) => file.endsWith(name));
|
|
138
|
+
}
|
package/dist/dream.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type InboxReport } from "./inbox.js";
|
|
2
|
+
import { type ModelExecutor } from "./executor.js";
|
|
3
|
+
import { type OperationOptions } from "./operations.js";
|
|
4
|
+
import type { MemomaticContext } from "./service.js";
|
|
5
|
+
export declare const LAST_RUN_KEY = "last-dream-at";
|
|
6
|
+
export type DreamReport = {
|
|
7
|
+
inbox: InboxReport;
|
|
8
|
+
promoted: string[];
|
|
9
|
+
superseded: string[];
|
|
10
|
+
dropped: string[];
|
|
11
|
+
archived: string[];
|
|
12
|
+
appendOnlyFallback: boolean;
|
|
13
|
+
dryRun: boolean;
|
|
14
|
+
elapsedMs?: number;
|
|
15
|
+
};
|
|
16
|
+
export type DreamOptions = OperationOptions & {
|
|
17
|
+
dryRun?: boolean;
|
|
18
|
+
};
|
|
19
|
+
export type ConsolidationOperation = {
|
|
20
|
+
op: "add";
|
|
21
|
+
line: string;
|
|
22
|
+
} | {
|
|
23
|
+
op: "supersede";
|
|
24
|
+
key: string;
|
|
25
|
+
line: string;
|
|
26
|
+
} | {
|
|
27
|
+
op: "drop";
|
|
28
|
+
key: string;
|
|
29
|
+
};
|
|
30
|
+
export declare function parseConsolidation(response: string): ConsolidationOperation[];
|
|
31
|
+
export declare function runDream(context: MemomaticContext, executor: ModelExecutor | null, options?: DreamOptions): Promise<DreamReport>;
|
package/dist/dream.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { entryLine, parseEntryLine } from "./entries.js";
|
|
2
|
+
import { appendDreams, readTextIfExists, writeCorpusFile } from "./corpus.js";
|
|
3
|
+
import { processInbox } from "./inbox.js";
|
|
4
|
+
import { promotionCandidates, relevanceByFts } from "./gates.js";
|
|
5
|
+
import { extractJson } from "./executor.js";
|
|
6
|
+
import { check } from "./operations.js";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { reindex } from "./search.js";
|
|
9
|
+
import { archiveOldEpisodic } from "./service.js";
|
|
10
|
+
export const LAST_RUN_KEY = "last-dream-at";
|
|
11
|
+
const CONSOLIDATE_SYSTEM = `You consolidate a long-term memory file.
|
|
12
|
+
Reply with exactly one JSON object of the form
|
|
13
|
+
{"operations":[{"op":"add","line":string},{"op":"supersede","key":string,"line":string},{"op":"drop","key":string}]}.
|
|
14
|
+
Merge duplicates, supersede outdated entries sharing a key instead of appending,
|
|
15
|
+
keep every line compact (at most two sentences), keep or assign "key" annotations
|
|
16
|
+
in the form <!-- key: ... -->, and add <!-- trigger: ... --> phrases only when a
|
|
17
|
+
clear activation context exists. Never invent keys for "supersede" or "drop" that
|
|
18
|
+
are not present in the current file.`;
|
|
19
|
+
export function parseConsolidation(response) {
|
|
20
|
+
const parsed = extractJson(response);
|
|
21
|
+
if (!Array.isArray(parsed.operations))
|
|
22
|
+
throw new Error("consolidation response has no operations");
|
|
23
|
+
const result = [];
|
|
24
|
+
for (const raw of parsed.operations) {
|
|
25
|
+
const value = raw;
|
|
26
|
+
if (value.op === "add" && typeof value.line === "string") {
|
|
27
|
+
const line = value.line.trim();
|
|
28
|
+
if (parseEntryLine(line))
|
|
29
|
+
result.push({ op: "add", line });
|
|
30
|
+
}
|
|
31
|
+
else if (value.op === "supersede" &&
|
|
32
|
+
typeof value.key === "string" &&
|
|
33
|
+
typeof value.line === "string") {
|
|
34
|
+
const line = value.line.trim();
|
|
35
|
+
if (parseEntryLine(line))
|
|
36
|
+
result.push({ op: "supersede", key: value.key, line });
|
|
37
|
+
}
|
|
38
|
+
else if (value.op === "drop" && typeof value.key === "string") {
|
|
39
|
+
result.push({ op: "drop", key: value.key });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return result.slice(0, 32);
|
|
43
|
+
}
|
|
44
|
+
export async function runDream(context, executor, options = {}) {
|
|
45
|
+
if (!options.dryRun)
|
|
46
|
+
return executeDream(context, executor, options);
|
|
47
|
+
const store = context.store.fork();
|
|
48
|
+
try {
|
|
49
|
+
return await executeDream({ ...context, store }, executor, options);
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
store.close();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function executeDream(context, executor, options) {
|
|
56
|
+
const started = Date.now();
|
|
57
|
+
const dryRun = options.dryRun === true;
|
|
58
|
+
const report = {
|
|
59
|
+
inbox: {
|
|
60
|
+
filesProcessed: 0,
|
|
61
|
+
filesRejected: 0,
|
|
62
|
+
entriesAppended: 0,
|
|
63
|
+
entriesSuperseded: 0,
|
|
64
|
+
entriesDuplicated: 0,
|
|
65
|
+
linesForbidden: 0,
|
|
66
|
+
linesInvalid: 0,
|
|
67
|
+
dryRun,
|
|
68
|
+
},
|
|
69
|
+
promoted: [],
|
|
70
|
+
superseded: [],
|
|
71
|
+
dropped: [],
|
|
72
|
+
archived: [],
|
|
73
|
+
appendOnlyFallback: false,
|
|
74
|
+
dryRun,
|
|
75
|
+
};
|
|
76
|
+
options.observe?.({
|
|
77
|
+
phase: "dream.start",
|
|
78
|
+
message: dryRun ? "Previewing Dream without persistent writes" : "Starting Dream",
|
|
79
|
+
});
|
|
80
|
+
report.inbox = await processInbox(context, { ...options, dryRun, deferIndex: true });
|
|
81
|
+
await reindex(context.paths, context.settings, context.store, options);
|
|
82
|
+
const relevance = relevanceByFts(context.store, context.settings);
|
|
83
|
+
const finalCandidates = promotionCandidates(context.store, context.settings, relevance);
|
|
84
|
+
if (executor && finalCandidates.length) {
|
|
85
|
+
check(options);
|
|
86
|
+
options.observe?.({
|
|
87
|
+
phase: "consolidation",
|
|
88
|
+
message: "Consolidating eligible memory",
|
|
89
|
+
candidates: finalCandidates.length,
|
|
90
|
+
});
|
|
91
|
+
const current = (await readTextIfExists(context.paths.memoryFile)) ?? "";
|
|
92
|
+
const consolidationKey = `consolidation-v1:${createHash("sha256")
|
|
93
|
+
.update(JSON.stringify([
|
|
94
|
+
current,
|
|
95
|
+
finalCandidates.map((candidate) => [candidate.entry.stableId, candidate.entry.text]),
|
|
96
|
+
context.settings.dream.model,
|
|
97
|
+
context.settings.dream.variant,
|
|
98
|
+
]))
|
|
99
|
+
.digest("hex")}`;
|
|
100
|
+
const cachedResponse = context.store.getMeta(consolidationKey);
|
|
101
|
+
const response = cachedResponse ??
|
|
102
|
+
(await executor.complete({
|
|
103
|
+
system: CONSOLIDATE_SYSTEM,
|
|
104
|
+
prompt: `Current MEMORY.md:\n${current || "(empty)"}\n\nCandidates:\n${finalCandidates
|
|
105
|
+
.map((candidate) => `- ${candidate.entry.text} <!-- key: ${candidate.entry.key ?? candidate.entry.stableId} -->`)
|
|
106
|
+
.join("\n")}`,
|
|
107
|
+
}));
|
|
108
|
+
let operations;
|
|
109
|
+
let invalidResponse = false;
|
|
110
|
+
try {
|
|
111
|
+
operations = parseConsolidation(response);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
invalidResponse = true;
|
|
115
|
+
operations = finalCandidates.map(({ entry }) => ({
|
|
116
|
+
op: "add",
|
|
117
|
+
line: entryLine(entry.text.replace(/^- /, ""), {
|
|
118
|
+
key: entry.key ?? undefined,
|
|
119
|
+
origin: entry.origin ?? "agent",
|
|
120
|
+
source: entry.source ?? undefined,
|
|
121
|
+
}),
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
check(options);
|
|
125
|
+
const outcome = await applyConsolidation(context, operations, current, dryRun);
|
|
126
|
+
if (!dryRun)
|
|
127
|
+
context.store.setMeta(consolidationKey, response);
|
|
128
|
+
if (cachedResponse)
|
|
129
|
+
options.observe?.({
|
|
130
|
+
phase: "consolidation.cached",
|
|
131
|
+
message: "Reused unchanged consolidation input",
|
|
132
|
+
});
|
|
133
|
+
report.promoted = outcome.promoted;
|
|
134
|
+
report.superseded = outcome.superseded;
|
|
135
|
+
report.dropped = outcome.dropped;
|
|
136
|
+
report.appendOnlyFallback = invalidResponse || outcome.appendOnlyFallback;
|
|
137
|
+
}
|
|
138
|
+
if (!dryRun) {
|
|
139
|
+
report.archived = await archiveOldEpisodic(context, options);
|
|
140
|
+
context.store.setMeta(LAST_RUN_KEY, new Date().toISOString());
|
|
141
|
+
}
|
|
142
|
+
if (report.promoted.length || report.superseded.length || report.archived.length)
|
|
143
|
+
await reindex(context.paths, context.settings, context.store, options);
|
|
144
|
+
const summary = [
|
|
145
|
+
`- inbox files processed: ${report.inbox.filesProcessed}`,
|
|
146
|
+
`- inbox entries appended: ${report.inbox.entriesAppended}`,
|
|
147
|
+
`- inbox entries superseded: ${report.inbox.entriesSuperseded}`,
|
|
148
|
+
`- inbox files rejected: ${report.inbox.filesRejected}`,
|
|
149
|
+
`- promoted: ${report.promoted.length}${report.promoted.length ? ` (${report.promoted.join(", ")})` : ""}`,
|
|
150
|
+
`- superseded: ${report.superseded.length ? report.superseded.join(", ") : "none"}`,
|
|
151
|
+
`- dropped: ${report.dropped.length ? report.dropped.join(", ") : "none"}`,
|
|
152
|
+
`- archived files: ${report.archived.length ? report.archived.join(", ") : "none"}`,
|
|
153
|
+
`- append-only fallback: ${report.appendOnlyFallback}`,
|
|
154
|
+
`- dry run: ${report.dryRun}`,
|
|
155
|
+
].join("\n");
|
|
156
|
+
if (!dryRun)
|
|
157
|
+
await appendDreams(context.paths, summary);
|
|
158
|
+
report.elapsedMs = Date.now() - started;
|
|
159
|
+
options.observe?.({
|
|
160
|
+
phase: "dream.done",
|
|
161
|
+
message: "Dream complete",
|
|
162
|
+
elapsedMs: report.elapsedMs,
|
|
163
|
+
});
|
|
164
|
+
return report;
|
|
165
|
+
}
|
|
166
|
+
async function applyConsolidation(context, operations, currentMemory, dryRun) {
|
|
167
|
+
const result = {
|
|
168
|
+
promoted: [],
|
|
169
|
+
superseded: [],
|
|
170
|
+
dropped: [],
|
|
171
|
+
appendOnlyFallback: false,
|
|
172
|
+
};
|
|
173
|
+
if (!operations.length)
|
|
174
|
+
return result;
|
|
175
|
+
const lines = currentMemory.split("\n").filter((line) => line.trim().length > 0);
|
|
176
|
+
const existingKeys = new Set(lines
|
|
177
|
+
.map((line) => parseEntryLine(line)?.annotations.key)
|
|
178
|
+
.filter((key) => Boolean(key)));
|
|
179
|
+
const removals = operations.filter((operation) => operation.op !== "add");
|
|
180
|
+
const lossRatio = lines.length ? removals.length / lines.length : 0;
|
|
181
|
+
const adds = operations.filter((operation) => operation.op === "add");
|
|
182
|
+
const supersessions = operations.filter((operation) => operation.op === "supersede" && existingKeys.has(operation.key));
|
|
183
|
+
const drops = operations.filter((operation) => operation.op === "drop" && existingKeys.has(operation.key));
|
|
184
|
+
// Consolidation never grants permission to delete curated memory.
|
|
185
|
+
if (drops.length)
|
|
186
|
+
result.appendOnlyFallback = true;
|
|
187
|
+
if (removals.length !== supersessions.length + drops.length)
|
|
188
|
+
result.appendOnlyFallback = true;
|
|
189
|
+
if (!result.appendOnlyFallback && lossRatio > context.settings.dream.maxPriorEntryLoss)
|
|
190
|
+
result.appendOnlyFallback = true;
|
|
191
|
+
if (result.appendOnlyFallback) {
|
|
192
|
+
const budget = context.settings.dream.memoryBudgetLines;
|
|
193
|
+
if (lines.length + adds.length > budget)
|
|
194
|
+
return result;
|
|
195
|
+
const next = `${lines.join("\n")}\n${adds.map((operation) => operation.line).join("\n")}\n`;
|
|
196
|
+
if (!dryRun)
|
|
197
|
+
await writeCorpusFile(context.paths, context.paths.memoryFile, next);
|
|
198
|
+
result.promoted = adds.map((operation) => operation.line);
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
201
|
+
const nextLines = [...lines];
|
|
202
|
+
for (const operation of supersessions) {
|
|
203
|
+
const index = nextLines.findIndex((line) => parseEntryLine(line)?.annotations.key === operation.key);
|
|
204
|
+
if (index === -1)
|
|
205
|
+
continue;
|
|
206
|
+
nextLines[index] = operation.line;
|
|
207
|
+
result.superseded.push(operation.key);
|
|
208
|
+
}
|
|
209
|
+
for (const operation of drops) {
|
|
210
|
+
const index = nextLines.findIndex((line) => parseEntryLine(line)?.annotations.key === operation.key);
|
|
211
|
+
if (index === -1)
|
|
212
|
+
continue;
|
|
213
|
+
nextLines.splice(index, 1);
|
|
214
|
+
result.dropped.push(operation.key);
|
|
215
|
+
}
|
|
216
|
+
for (const operation of adds) {
|
|
217
|
+
const key = parseEntryLine(operation.line)?.annotations.key;
|
|
218
|
+
if (key && nextLines.some((line) => parseEntryLine(line)?.annotations.key === key))
|
|
219
|
+
continue;
|
|
220
|
+
nextLines.push(operation.line);
|
|
221
|
+
result.promoted.push(operation.line);
|
|
222
|
+
}
|
|
223
|
+
if (nextLines.length > context.settings.dream.memoryBudgetLines) {
|
|
224
|
+
result.appendOnlyFallback = true;
|
|
225
|
+
result.promoted = [];
|
|
226
|
+
result.superseded = [];
|
|
227
|
+
result.dropped = [];
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
const next = `${nextLines.join("\n")}\n`;
|
|
231
|
+
if (!dryRun)
|
|
232
|
+
await writeCorpusFile(context.paths, context.paths.memoryFile, next);
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type EntryAnnotations = {
|
|
2
|
+
key?: string;
|
|
3
|
+
status?: "active" | "superseded";
|
|
4
|
+
origin?: "user" | "agent";
|
|
5
|
+
observed?: string;
|
|
6
|
+
project?: string;
|
|
7
|
+
importance?: number;
|
|
8
|
+
trigger?: string[];
|
|
9
|
+
pinned?: boolean;
|
|
10
|
+
source?: string;
|
|
11
|
+
target?: "episodic" | "curated" | "user";
|
|
12
|
+
};
|
|
13
|
+
export type CorpusEntry = {
|
|
14
|
+
file: string;
|
|
15
|
+
line: number;
|
|
16
|
+
text: string;
|
|
17
|
+
annotations: EntryAnnotations;
|
|
18
|
+
};
|
|
19
|
+
export declare function parseEntryLine(line: string): {
|
|
20
|
+
text: string;
|
|
21
|
+
annotations: EntryAnnotations;
|
|
22
|
+
} | null;
|
|
23
|
+
export declare function serializeAnnotations(annotations: EntryAnnotations, options?: {
|
|
24
|
+
dropTarget?: boolean;
|
|
25
|
+
}): string;
|
|
26
|
+
export declare function entryLine(text: string, annotations: EntryAnnotations, options?: {
|
|
27
|
+
dropTarget?: boolean;
|
|
28
|
+
}): string;
|
|
29
|
+
export declare function entryKey(entry: CorpusEntry): string | null;
|
|
30
|
+
export declare function entryImportance(entry: CorpusEntry): number;
|
|
31
|
+
export declare function entryObservedAt(entry: CorpusEntry): number;
|
package/dist/entries.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const SOURCE_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
|
|
2
|
+
const TRAILING_ANNOTATION = /\s*<!--\s*([a-z-]+):\s*([^<]*?)\s*-->\s*$/;
|
|
3
|
+
export function parseEntryLine(line) {
|
|
4
|
+
let value = line.trimEnd();
|
|
5
|
+
if (!value.startsWith("- "))
|
|
6
|
+
return null;
|
|
7
|
+
const annotations = {};
|
|
8
|
+
for (let index = 0; index < 8; index += 1) {
|
|
9
|
+
const match = TRAILING_ANNOTATION.exec(value);
|
|
10
|
+
if (!match)
|
|
11
|
+
break;
|
|
12
|
+
const name = match[1];
|
|
13
|
+
const raw = match[2];
|
|
14
|
+
let consumed = true;
|
|
15
|
+
if (name === "key" || name === "project" || name === "observed" || name === "source") {
|
|
16
|
+
if (name === "observed")
|
|
17
|
+
annotations.observed = raw;
|
|
18
|
+
else if (name === "project")
|
|
19
|
+
annotations.project = raw;
|
|
20
|
+
else if (name === "source") {
|
|
21
|
+
if (SOURCE_PATTERN.test(raw))
|
|
22
|
+
annotations.source = raw;
|
|
23
|
+
else
|
|
24
|
+
consumed = false;
|
|
25
|
+
}
|
|
26
|
+
else
|
|
27
|
+
annotations.key = raw;
|
|
28
|
+
}
|
|
29
|
+
else if (name === "target") {
|
|
30
|
+
if (raw === "episodic" || raw === "curated" || raw === "user")
|
|
31
|
+
annotations.target = raw;
|
|
32
|
+
else
|
|
33
|
+
consumed = false;
|
|
34
|
+
}
|
|
35
|
+
else if (name === "status") {
|
|
36
|
+
if (raw === "active" || raw === "superseded")
|
|
37
|
+
annotations.status = raw;
|
|
38
|
+
else
|
|
39
|
+
consumed = false;
|
|
40
|
+
}
|
|
41
|
+
else if (name === "origin") {
|
|
42
|
+
if (raw === "user" || raw === "agent")
|
|
43
|
+
annotations.origin = raw;
|
|
44
|
+
else
|
|
45
|
+
consumed = false;
|
|
46
|
+
}
|
|
47
|
+
else if (name === "importance") {
|
|
48
|
+
const parsed = Number.parseInt(raw, 10);
|
|
49
|
+
if (Number.isFinite(parsed) && parsed >= 1 && parsed <= 10)
|
|
50
|
+
annotations.importance = parsed;
|
|
51
|
+
else
|
|
52
|
+
consumed = false;
|
|
53
|
+
}
|
|
54
|
+
else if (name === "trigger") {
|
|
55
|
+
annotations.trigger = raw
|
|
56
|
+
.split(/[;,]\s*/)
|
|
57
|
+
.map((item) => item.trim())
|
|
58
|
+
.filter(Boolean);
|
|
59
|
+
if (!annotations.trigger.length)
|
|
60
|
+
consumed = false;
|
|
61
|
+
}
|
|
62
|
+
else if (name === "pinned") {
|
|
63
|
+
if (raw === "true")
|
|
64
|
+
annotations.pinned = true;
|
|
65
|
+
else
|
|
66
|
+
consumed = false;
|
|
67
|
+
}
|
|
68
|
+
else
|
|
69
|
+
consumed = false;
|
|
70
|
+
if (!consumed)
|
|
71
|
+
break;
|
|
72
|
+
value = value.slice(0, match.index).trimEnd();
|
|
73
|
+
}
|
|
74
|
+
return { text: value, annotations };
|
|
75
|
+
}
|
|
76
|
+
export function serializeAnnotations(annotations, options = {}) {
|
|
77
|
+
const parts = [];
|
|
78
|
+
if (annotations.key)
|
|
79
|
+
parts.push(`key: ${annotations.key}`);
|
|
80
|
+
if (annotations.status)
|
|
81
|
+
parts.push(`status: ${annotations.status}`);
|
|
82
|
+
if (annotations.origin)
|
|
83
|
+
parts.push(`origin: ${annotations.origin}`);
|
|
84
|
+
if (annotations.observed)
|
|
85
|
+
parts.push(`observed: ${annotations.observed}`);
|
|
86
|
+
if (annotations.project)
|
|
87
|
+
parts.push(`project: ${annotations.project}`);
|
|
88
|
+
if (annotations.importance !== undefined)
|
|
89
|
+
parts.push(`importance: ${annotations.importance}`);
|
|
90
|
+
if (annotations.trigger?.length)
|
|
91
|
+
parts.push(`trigger: ${annotations.trigger.join("; ")}`);
|
|
92
|
+
if (annotations.pinned)
|
|
93
|
+
parts.push("pinned: true");
|
|
94
|
+
if (annotations.source)
|
|
95
|
+
parts.push(`source: ${annotations.source}`);
|
|
96
|
+
if (!options.dropTarget && annotations.target)
|
|
97
|
+
parts.push(`target: ${annotations.target}`);
|
|
98
|
+
return parts.map((part) => `<!-- ${part} -->`).join(" ");
|
|
99
|
+
}
|
|
100
|
+
export function entryLine(text, annotations, options = {}) {
|
|
101
|
+
const suffix = serializeAnnotations(annotations, options);
|
|
102
|
+
return `- ${text}${suffix ? ` ${suffix}` : ""}`;
|
|
103
|
+
}
|
|
104
|
+
export function entryKey(entry) {
|
|
105
|
+
if (entry.annotations.key)
|
|
106
|
+
return entry.annotations.key;
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
export function entryImportance(entry) {
|
|
110
|
+
return entry.annotations.importance ?? 5;
|
|
111
|
+
}
|
|
112
|
+
export function entryObservedAt(entry) {
|
|
113
|
+
if (entry.annotations.observed) {
|
|
114
|
+
const parsed = Date.parse(entry.annotations.observed);
|
|
115
|
+
if (Number.isFinite(parsed))
|
|
116
|
+
return parsed;
|
|
117
|
+
}
|
|
118
|
+
return Number.NaN;
|
|
119
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { OperationOptions } from "./operations.js";
|
|
2
|
+
export type ModelExecutor = {
|
|
3
|
+
name: string;
|
|
4
|
+
complete(request: {
|
|
5
|
+
system: string;
|
|
6
|
+
prompt: string;
|
|
7
|
+
}): Promise<string>;
|
|
8
|
+
close?: () => Promise<void>;
|
|
9
|
+
};
|
|
10
|
+
export declare function extractJson(response: string): unknown;
|
|
11
|
+
type ExecutorOptions = OperationOptions & {
|
|
12
|
+
model: string | null;
|
|
13
|
+
variant: string | null;
|
|
14
|
+
binary?: string;
|
|
15
|
+
url?: string | null;
|
|
16
|
+
timeoutMs?: number;
|
|
17
|
+
retries?: number;
|
|
18
|
+
};
|
|
19
|
+
export declare class OpenCodeExecutor implements ModelExecutor {
|
|
20
|
+
private readonly options;
|
|
21
|
+
readonly name = "opencode";
|
|
22
|
+
readonly usage: {
|
|
23
|
+
calls: number;
|
|
24
|
+
retries: number;
|
|
25
|
+
inputTokens: number;
|
|
26
|
+
outputTokens: number;
|
|
27
|
+
cacheReadTokens: number;
|
|
28
|
+
cacheWriteTokens: number;
|
|
29
|
+
usageAvailable: boolean;
|
|
30
|
+
charactersSent: number;
|
|
31
|
+
};
|
|
32
|
+
private child?;
|
|
33
|
+
private directory?;
|
|
34
|
+
private url?;
|
|
35
|
+
private authorization?;
|
|
36
|
+
constructor(options: ExecutorOptions);
|
|
37
|
+
private initialize;
|
|
38
|
+
private request;
|
|
39
|
+
complete(request: {
|
|
40
|
+
system: string;
|
|
41
|
+
prompt: string;
|
|
42
|
+
}): Promise<string>;
|
|
43
|
+
close(): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
export {};
|