@kisev/memomatic 1.0.0-dev.69.gd644a007a7ee → 1.0.0-dev.71.gee4e37a5bb61
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 +10 -1
- package/README.ru.md +10 -1
- package/dist/cli.js +201 -92
- package/dist/dream.d.ts +17 -3
- package/dist/dream.js +197 -40
- package/dist/executor.d.ts +29 -6
- package/dist/executor.js +256 -45
- package/dist/generated/cli.d.ts +28 -0
- package/dist/generated/cli.js +181 -0
- package/dist/inbox.d.ts +3 -1
- package/dist/inbox.js +27 -6
- package/dist/ingest.d.ts +0 -15
- package/dist/ingest.js +1 -100
- package/dist/ingestion.d.ts +30 -0
- package/dist/ingestion.js +209 -0
- package/dist/operations.d.ts +7 -0
- package/dist/operations.js +3 -0
- package/dist/paths.js +7 -2
- package/dist/search.d.ts +2 -1
- package/dist/search.js +52 -2
- package/dist/service.d.ts +4 -2
- package/dist/service.js +11 -8
- package/dist/settings.d.ts +8 -1
- package/dist/settings.js +31 -3
- package/dist/store.js +2 -0
- package/package.json +4 -3
package/dist/dream.js
CHANGED
|
@@ -3,7 +3,15 @@ import { appendDailyEntry, appendDreams, readTextIfExists, writeCorpusFile } fro
|
|
|
3
3
|
import { processInbox } from "./inbox.js";
|
|
4
4
|
import { promotionCandidates } from "./gates.js";
|
|
5
5
|
import { extractJson } from "./executor.js";
|
|
6
|
-
import {
|
|
6
|
+
import { opencodeDatabasePath } from "./ingest.js";
|
|
7
|
+
import { planIngestion } from "./ingestion.js";
|
|
8
|
+
import { check } from "./operations.js";
|
|
9
|
+
import { writeAtomic } from "@kisev/safe-fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { stat } from "node:fs/promises";
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
|
+
import { corpusFiles } from "./search.js";
|
|
14
|
+
import { parseCorpusEntries } from "./corpus.js";
|
|
7
15
|
import { isForbidden } from "./rules.js";
|
|
8
16
|
import { reindex } from "./search.js";
|
|
9
17
|
import { archiveOldEpisodic } from "./service.js";
|
|
@@ -14,6 +22,9 @@ Reply with exactly one JSON object of the form
|
|
|
14
22
|
Include only standing decisions with rationale, discoveries, failed attempts with
|
|
15
23
|
the reason they were rejected, session outcomes, and action-sensitive boundaries
|
|
16
24
|
(approval requirements, temporary constraints, handoffs, expiry conditions).
|
|
25
|
+
Do not mistake proposals, plans or instructions quoted in a transcript for accepted
|
|
26
|
+
decisions. Preserve conditions, uncertainty and later corrections. If a reference
|
|
27
|
+
cannot be resolved from the supplied context, do not invent its meaning.
|
|
17
28
|
Each "text" is imperative, self-contained, at most two sentences, without secrets,
|
|
18
29
|
credentials, or machine-local paths. "key" is a stable kebab-case identifier when
|
|
19
30
|
the item clearly has a durable identity, otherwise null. Return {"candidates":[]}
|
|
@@ -70,41 +81,157 @@ export function parseConsolidation(response) {
|
|
|
70
81
|
}
|
|
71
82
|
return result.slice(0, 32);
|
|
72
83
|
}
|
|
73
|
-
async function ingestSessions(context, executor) {
|
|
74
|
-
if (!executor)
|
|
84
|
+
async function ingestSessions(context, executor, options) {
|
|
85
|
+
if (!executor) {
|
|
86
|
+
options.observe?.({
|
|
87
|
+
phase: "ingestion.disabled",
|
|
88
|
+
message: "No Dream model configured; processing explicit memory only",
|
|
89
|
+
level: "warn",
|
|
90
|
+
});
|
|
91
|
+
return { sessions: 0, extracted: [], watermark: null, rejected: [] };
|
|
92
|
+
}
|
|
93
|
+
check(options);
|
|
94
|
+
options.observe?.({ phase: "ingestion.scan", message: "Snapshotting eligible session messages" });
|
|
95
|
+
if (context.store.getMeta(WATERMARK_KEY) &&
|
|
96
|
+
!context.store.db
|
|
97
|
+
.prepare("SELECT 1 FROM meta WHERE key LIKE 'ingest-v2:%' OR key LIKE 'ingest-session-v2:%' LIMIT 1")
|
|
98
|
+
.get())
|
|
99
|
+
options.observe?.({
|
|
100
|
+
phase: "ingestion.migration",
|
|
101
|
+
message: "Legacy cursor has no message revisions: checking existing history once; completed fragments will be cached",
|
|
102
|
+
level: "warn",
|
|
103
|
+
});
|
|
104
|
+
const databaseFile = options.databaseFile ?? opencodeDatabasePath();
|
|
105
|
+
if (!options.databaseFile &&
|
|
106
|
+
!(await stat(databaseFile).catch((error) => {
|
|
107
|
+
if (error.code === "ENOENT")
|
|
108
|
+
return null;
|
|
109
|
+
throw error;
|
|
110
|
+
}))) {
|
|
111
|
+
options.observe?.({
|
|
112
|
+
phase: "ingestion.unavailable",
|
|
113
|
+
message: "No OpenCode database exists yet; skipping session extraction",
|
|
114
|
+
level: "warn",
|
|
115
|
+
});
|
|
75
116
|
return { sessions: 0, extracted: [], watermark: null, rejected: [] };
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
117
|
+
}
|
|
118
|
+
let plan;
|
|
119
|
+
try {
|
|
120
|
+
plan = planIngestion(options.databaseFile ?? opencodeDatabasePath(), context.store, {
|
|
121
|
+
before: Date.now() - context.settings.dream.idleMs,
|
|
122
|
+
maxChars: context.settings.dream.maxChars,
|
|
123
|
+
maxSessions: context.settings.dream.maxSessions,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
throw new Error(`cannot read OpenCode sessions: ${error.message}`);
|
|
128
|
+
}
|
|
129
|
+
const progress = {
|
|
130
|
+
messages: plan.messages,
|
|
131
|
+
fragments: plan.fragments.length,
|
|
132
|
+
completed: 0,
|
|
133
|
+
cached: plan.cached,
|
|
134
|
+
characters: plan.characters,
|
|
135
|
+
remaining: plan.fragments.length,
|
|
136
|
+
cachedSessions: plan.cachedSessions,
|
|
137
|
+
unscannedSessions: plan.unscannedSessions,
|
|
138
|
+
};
|
|
139
|
+
if (!options.dryRun)
|
|
140
|
+
for (const [key, value] of plan.seals)
|
|
141
|
+
context.store.setMeta(key, value);
|
|
142
|
+
options.observe?.({
|
|
143
|
+
phase: "ingestion.plan",
|
|
144
|
+
message: "Session snapshot ready",
|
|
145
|
+
total: plan.fragments.length,
|
|
146
|
+
completed: 0,
|
|
147
|
+
messages: plan.messages,
|
|
148
|
+
cached: plan.cached,
|
|
149
|
+
characters: plan.characters,
|
|
81
150
|
});
|
|
151
|
+
const seen = new Set();
|
|
152
|
+
for (const file of await corpusFiles(context.paths))
|
|
153
|
+
for (const entry of parseCorpusEntries((await readTextIfExists(file)) ?? "", file))
|
|
154
|
+
seen.add(entry.text);
|
|
82
155
|
const extracted = [];
|
|
83
156
|
const rejected = [];
|
|
84
|
-
|
|
85
|
-
for (const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
prompt: `Session title: ${session.title}\nWorking directory: ${session.directory}\n\n${transcript}`,
|
|
157
|
+
const sessions = new Set();
|
|
158
|
+
for (const fragment of plan.fragments) {
|
|
159
|
+
check(options);
|
|
160
|
+
options.observe?.({
|
|
161
|
+
phase: "ingestion.extract",
|
|
162
|
+
message: "Extracting a new or changed fragment",
|
|
163
|
+
completed: progress.completed,
|
|
164
|
+
total: progress.fragments,
|
|
165
|
+
fragment: fragment.id,
|
|
94
166
|
});
|
|
95
|
-
|
|
167
|
+
const receipt = join(context.paths.historyDir, "ingestion", `${fragment.id}.json`);
|
|
168
|
+
const cached = await readTextIfExists(receipt);
|
|
169
|
+
let candidates;
|
|
170
|
+
if (cached) {
|
|
171
|
+
const value = JSON.parse(cached);
|
|
172
|
+
if (value.version !== 1 || value.id !== fragment.id)
|
|
173
|
+
throw new Error("invalid ingestion checkpoint");
|
|
174
|
+
candidates = parseExtraction(JSON.stringify({ candidates: value.candidates }));
|
|
175
|
+
options.observe?.({
|
|
176
|
+
phase: "ingestion.resume",
|
|
177
|
+
message: "Reusing completed model response",
|
|
178
|
+
fragment: fragment.id,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
else
|
|
182
|
+
candidates = parseExtraction(await executor.complete({
|
|
183
|
+
system: EXTRACT_SYSTEM,
|
|
184
|
+
prompt: `Session: ${fragment.sessionId}\nTitle: ${fragment.title}\nWorking directory: ${fragment.directory}\nMessage IDs: ${fragment.messageIds.join(",")}\nPrevious context (do not extract again):\n${fragment.context}\n\nNew or changed fragment:\n${fragment.text}`,
|
|
185
|
+
}));
|
|
186
|
+
if (!options.dryRun && !cached)
|
|
187
|
+
await writeAtomic(receipt, Buffer.from(JSON.stringify({
|
|
188
|
+
version: 1,
|
|
189
|
+
id: fragment.id,
|
|
190
|
+
sessionId: fragment.sessionId,
|
|
191
|
+
messageIds: fragment.messageIds,
|
|
192
|
+
candidates: candidates.filter((candidate) => !isForbidden(candidate.text, context.rules)),
|
|
193
|
+
})), 0o600);
|
|
194
|
+
check(options);
|
|
195
|
+
for (const candidate of candidates) {
|
|
96
196
|
if (isForbidden(candidate.text, context.rules)) {
|
|
97
197
|
rejected.push({ text: candidate.text, reason: "never-save rule" });
|
|
98
198
|
continue;
|
|
99
199
|
}
|
|
100
200
|
extracted.push(candidate);
|
|
201
|
+
const line = entryLine(candidate.text, {
|
|
202
|
+
key: candidate.key ?? undefined,
|
|
203
|
+
origin: "agent",
|
|
204
|
+
source: "opencode",
|
|
205
|
+
observed: new Date().toISOString().slice(0, 10),
|
|
206
|
+
});
|
|
207
|
+
const parsed = parseCorpusEntries(`${line}\n`, "")[0];
|
|
208
|
+
const text = parsed?.text ?? `- ${candidate.text}`;
|
|
209
|
+
if (!options.dryRun && !seen.has(text))
|
|
210
|
+
await appendDailyEntry(context.paths, line);
|
|
211
|
+
seen.add(text);
|
|
101
212
|
}
|
|
213
|
+
if (!options.dryRun) {
|
|
214
|
+
for (const key of fragment.checkpointKeys)
|
|
215
|
+
context.store.setMeta(key, fragment.id);
|
|
216
|
+
if (fragment.seal)
|
|
217
|
+
context.store.setMeta(...fragment.seal);
|
|
218
|
+
}
|
|
219
|
+
sessions.add(fragment.sessionId);
|
|
220
|
+
progress.completed++;
|
|
221
|
+
progress.remaining--;
|
|
222
|
+
options.observe?.({
|
|
223
|
+
phase: "ingestion.checkpoint",
|
|
224
|
+
message: "Fragment checkpoint committed",
|
|
225
|
+
completed: progress.completed,
|
|
226
|
+
total: progress.fragments,
|
|
227
|
+
});
|
|
102
228
|
}
|
|
103
229
|
return {
|
|
104
|
-
sessions: sessions.
|
|
230
|
+
sessions: sessions.size,
|
|
105
231
|
extracted,
|
|
106
|
-
watermark: sessions.
|
|
232
|
+
watermark: sessions.size ? plan.watermark : null,
|
|
107
233
|
rejected,
|
|
234
|
+
progress,
|
|
108
235
|
};
|
|
109
236
|
}
|
|
110
237
|
export async function runDream(context, executor, options = {}) {
|
|
@@ -119,6 +246,7 @@ export async function runDream(context, executor, options = {}) {
|
|
|
119
246
|
}
|
|
120
247
|
}
|
|
121
248
|
async function executeDream(context, executor, options) {
|
|
249
|
+
const started = Date.now();
|
|
122
250
|
const dryRun = options.dryRun === true;
|
|
123
251
|
const report = {
|
|
124
252
|
inbox: {
|
|
@@ -141,24 +269,21 @@ async function executeDream(context, executor, options) {
|
|
|
141
269
|
appendOnlyFallback: false,
|
|
142
270
|
dryRun,
|
|
143
271
|
};
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
272
|
+
options.observe?.({
|
|
273
|
+
phase: "dream.start",
|
|
274
|
+
message: dryRun ? "Previewing Dream without persistent writes" : "Starting Dream",
|
|
275
|
+
});
|
|
276
|
+
report.inbox = await processInbox(context, { ...options, dryRun, deferIndex: true });
|
|
277
|
+
const ingestion = await ingestSessions(context, executor, options);
|
|
278
|
+
report.ingestion = ingestion.progress;
|
|
147
279
|
report.sessionsIngested = ingestion.sessions;
|
|
148
280
|
report.candidatesExtracted = ingestion.extracted.length;
|
|
149
281
|
report.rejected = ingestion.rejected;
|
|
150
282
|
if (!dryRun) {
|
|
151
|
-
for (const candidate of ingestion.extracted) {
|
|
152
|
-
await appendDailyEntry(context.paths, entryLine(candidate.text, {
|
|
153
|
-
key: candidate.key ?? undefined,
|
|
154
|
-
origin: "agent",
|
|
155
|
-
observed: new Date().toISOString().slice(0, 10),
|
|
156
|
-
}));
|
|
157
|
-
}
|
|
158
283
|
if (ingestion.watermark !== null)
|
|
159
284
|
context.store.setMeta(WATERMARK_KEY, String(ingestion.watermark));
|
|
160
285
|
}
|
|
161
|
-
await reindex(context.paths, context.settings, context.store);
|
|
286
|
+
await reindex(context.paths, context.settings, context.store, options);
|
|
162
287
|
const relevance = new Map();
|
|
163
288
|
for (const entry of context.store.allEntries()) {
|
|
164
289
|
if (entry.kind !== "episodic")
|
|
@@ -172,13 +297,29 @@ async function executeDream(context, executor, options) {
|
|
|
172
297
|
}
|
|
173
298
|
const finalCandidates = promotionCandidates(context.store, context.settings, relevance);
|
|
174
299
|
if (executor && finalCandidates.length) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
.join("\n")}`,
|
|
300
|
+
check(options);
|
|
301
|
+
options.observe?.({
|
|
302
|
+
phase: "consolidation",
|
|
303
|
+
message: "Consolidating eligible memory",
|
|
304
|
+
candidates: finalCandidates.length,
|
|
181
305
|
});
|
|
306
|
+
const current = (await readTextIfExists(context.paths.memoryFile)) ?? "";
|
|
307
|
+
const consolidationKey = `consolidation-v1:${createHash("sha256")
|
|
308
|
+
.update(JSON.stringify([
|
|
309
|
+
current,
|
|
310
|
+
finalCandidates.map((candidate) => [candidate.entry.stableId, candidate.entry.text]),
|
|
311
|
+
context.settings.dream.model,
|
|
312
|
+
context.settings.dream.variant,
|
|
313
|
+
]))
|
|
314
|
+
.digest("hex")}`;
|
|
315
|
+
const cachedResponse = context.store.getMeta(consolidationKey);
|
|
316
|
+
const response = cachedResponse ??
|
|
317
|
+
(await executor.complete({
|
|
318
|
+
system: CONSOLIDATE_SYSTEM,
|
|
319
|
+
prompt: `Current MEMORY.md:\n${current || "(empty)"}\n\nCandidates:\n${finalCandidates
|
|
320
|
+
.map((candidate) => `- ${candidate.entry.text} <!-- key: ${candidate.entry.key ?? candidate.entry.stableId} -->`)
|
|
321
|
+
.join("\n")}`,
|
|
322
|
+
}));
|
|
182
323
|
let operations;
|
|
183
324
|
let invalidResponse = false;
|
|
184
325
|
try {
|
|
@@ -195,15 +336,24 @@ async function executeDream(context, executor, options) {
|
|
|
195
336
|
}),
|
|
196
337
|
}));
|
|
197
338
|
}
|
|
339
|
+
check(options);
|
|
198
340
|
const outcome = await applyConsolidation(context, operations, current, dryRun);
|
|
341
|
+
if (!dryRun)
|
|
342
|
+
context.store.setMeta(consolidationKey, response);
|
|
343
|
+
if (cachedResponse)
|
|
344
|
+
options.observe?.({
|
|
345
|
+
phase: "consolidation.cached",
|
|
346
|
+
message: "Reused unchanged consolidation input",
|
|
347
|
+
});
|
|
199
348
|
report.promoted = outcome.promoted;
|
|
200
349
|
report.superseded = outcome.superseded;
|
|
201
350
|
report.dropped = outcome.dropped;
|
|
202
351
|
report.appendOnlyFallback = invalidResponse || outcome.appendOnlyFallback;
|
|
203
352
|
}
|
|
204
353
|
if (!dryRun)
|
|
205
|
-
report.archived = await archiveOldEpisodic(context);
|
|
206
|
-
|
|
354
|
+
report.archived = await archiveOldEpisodic(context, options);
|
|
355
|
+
if (report.promoted.length || report.superseded.length || report.archived.length)
|
|
356
|
+
await reindex(context.paths, context.settings, context.store, options);
|
|
207
357
|
const summary = [
|
|
208
358
|
`- inbox files processed: ${report.inbox.filesProcessed}`,
|
|
209
359
|
`- inbox entries appended: ${report.inbox.entriesAppended}`,
|
|
@@ -221,6 +371,13 @@ async function executeDream(context, executor, options) {
|
|
|
221
371
|
].join("\n");
|
|
222
372
|
if (!dryRun)
|
|
223
373
|
await appendDreams(context.paths, summary);
|
|
374
|
+
report.elapsedMs = Date.now() - started;
|
|
375
|
+
options.observe?.({
|
|
376
|
+
phase: "dream.done",
|
|
377
|
+
message: "Dream complete",
|
|
378
|
+
elapsedMs: report.elapsedMs,
|
|
379
|
+
...report.ingestion,
|
|
380
|
+
});
|
|
224
381
|
return report;
|
|
225
382
|
}
|
|
226
383
|
async function applyConsolidation(context, operations, currentMemory, dryRun) {
|
package/dist/executor.d.ts
CHANGED
|
@@ -1,22 +1,45 @@
|
|
|
1
|
+
import type { OperationOptions } from "./operations.js";
|
|
1
2
|
export type ModelExecutor = {
|
|
2
3
|
name: string;
|
|
3
4
|
complete(request: {
|
|
4
5
|
system: string;
|
|
5
6
|
prompt: string;
|
|
6
7
|
}): Promise<string>;
|
|
8
|
+
close?: () => Promise<void>;
|
|
7
9
|
};
|
|
8
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
|
+
};
|
|
9
19
|
export declare class OpenCodeExecutor implements ModelExecutor {
|
|
10
20
|
private readonly options;
|
|
11
21
|
readonly name = "opencode";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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;
|
|
17
39
|
complete(request: {
|
|
18
40
|
system: string;
|
|
19
41
|
prompt: string;
|
|
20
42
|
}): Promise<string>;
|
|
21
|
-
|
|
43
|
+
close(): Promise<void>;
|
|
22
44
|
}
|
|
45
|
+
export {};
|