@klarkxy/dsh-memory 0.1.0

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/lib/index.js ADDED
@@ -0,0 +1,1515 @@
1
+ import { CHAT_EVENTS_SLOT, INJECT_KINDS, MAX_PROJECT_ID_CHARS, MEMORY_ACTIVATE_ID, MEMORY_DREAM_PURPOSE, MEMORY_INJECTION_SECTION, MEMORY_PLUGIN, MEMORY_RPC_CHANNEL, MEMORY_SOURCE_KIND, RECALL_EXCLUDED_STATUSES, cloneRecord, defaultSettings, fail as fail$1, ok, parseSessionId, projectIdFromCwd, scopeKey, scopesEqual, sessionCwd } from "./contracts.js";
2
+ import { createUserMessage } from "@deepseek-ai/dsh-llm";
3
+ import { registerHostRpc } from "@klarkxy/dsh-ai-services/host-rpc";
4
+ import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
5
+ import { z } from "zod";
6
+ import { createHash, randomUUID } from "node:crypto";
7
+ //#region src/idle.ts
8
+ function shouldRunIdleDream(input) {
9
+ if (!input.dreamIdleEnabled || !input.pluginActive || !input.agentIdle || input.dreamRunning) return false;
10
+ if (input.now - input.lastActivityAt < input.idleMs) return false;
11
+ if (input.lastAttemptAt !== void 0 && input.now - input.lastAttemptAt < (input.minIntervalMs ?? 864e5)) return false;
12
+ return input.materialCount >= (input.minMaterial ?? 3);
13
+ }
14
+ function countDreamMaterial(state, since) {
15
+ if (since === void 0) return state.records.length;
16
+ let count = 0;
17
+ for (const record of state.records) if (record.createdAt > since || record.updatedAt > since) count += 1;
18
+ for (const row of state.tombstones) if (row.deletedAt > since) count += 1;
19
+ return count;
20
+ }
21
+ //#endregion
22
+ //#region src/errors.ts
23
+ var MemoryError = class extends Error {
24
+ code;
25
+ constructor(message, code) {
26
+ super(message);
27
+ this.code = code;
28
+ this.name = "MemoryError";
29
+ }
30
+ };
31
+ const MEMORY_CONFLICT = "MEMORY_CONFLICT";
32
+ const MEMORY_INVALID = "MEMORY_INVALID";
33
+ const MEMORY_NOT_FOUND = "MEMORY_NOT_FOUND";
34
+ const MEMORY_DISABLED = "MEMORY_DISABLED";
35
+ const MEMORY_SAVE_FAILED = "MEMORY_SAVE_FAILED";
36
+ const MEMORY_CAPACITY = "MEMORY_CAPACITY";
37
+ const MEMORY_SCOPE = "MEMORY_SCOPE";
38
+ const MEMORY_EVIDENCE = "MEMORY_EVIDENCE";
39
+ const MEMORY_TOMBSTONE = "MEMORY_TOMBSTONE";
40
+ const MEMORY_STALE = "MEMORY_STALE";
41
+ const MEMORY_AI_UNAVAILABLE = "MEMORY_AI_UNAVAILABLE";
42
+ const MEMORY_CANCELLED = "MEMORY_CANCELLED";
43
+ const MEMORY_DELETED = "MEMORY_DELETED";
44
+ function fail(code, message) {
45
+ throw new MemoryError(message, code);
46
+ }
47
+ function isAbortError(error) {
48
+ if (!error || typeof error !== "object") return false;
49
+ const name = "name" in error ? String(error.name) : "";
50
+ const code = "code" in error ? String(error.code) : "";
51
+ return name === "AbortError" || name === "TimeoutError" || code === "ABORT_ERR" || code === "ABORTED";
52
+ }
53
+ //#endregion
54
+ //#region src/storage.ts
55
+ const evidenceSchema = z.object({
56
+ sessionId: z.string().min(1).max(200),
57
+ seq: z.number().int().nonnegative(),
58
+ kind: z.enum([
59
+ "user",
60
+ "tool",
61
+ "turn",
62
+ "manual"
63
+ ]),
64
+ excerpt: z.string().max(400).optional()
65
+ }).strict();
66
+ const knowledgeScopeSchema = z.discriminatedUnion("kind", [z.object({ kind: z.literal("global") }).strict(), z.object({
67
+ kind: z.literal("project"),
68
+ projectId: z.string().min(1).max(MAX_PROJECT_ID_CHARS)
69
+ }).strict()]);
70
+ const basisRefSchema = z.object({
71
+ id: z.string().min(1).max(80),
72
+ revision: z.number().int().nonnegative()
73
+ }).strict();
74
+ const memoryRecordSchema = z.object({
75
+ id: z.string().min(1).max(80),
76
+ revision: z.number().int().nonnegative(),
77
+ scope: knowledgeScopeSchema,
78
+ kind: z.enum([
79
+ "preference",
80
+ "project-fact",
81
+ "decision",
82
+ "lesson"
83
+ ]),
84
+ status: z.enum([
85
+ "candidate",
86
+ "active",
87
+ "rejected",
88
+ "superseded",
89
+ "revoked",
90
+ "deleted"
91
+ ]),
92
+ title: z.string().min(1).max(160),
93
+ content: z.string().min(1).max(4e3),
94
+ tags: z.array(z.string().min(1).max(40)).max(16),
95
+ evidence: z.array(evidenceSchema).max(16),
96
+ exceptions: z.array(z.string().max(200)).max(16),
97
+ source: z.enum([
98
+ "user",
99
+ "memory",
100
+ "dream",
101
+ "self-improvement"
102
+ ]),
103
+ createdAt: z.number().int().nonnegative(),
104
+ updatedAt: z.number().int().nonnegative(),
105
+ expiresAt: z.number().int().positive().optional(),
106
+ supersedes: z.array(z.string().min(1).max(80)).max(32).optional(),
107
+ basis: z.array(basisRefSchema).max(16).optional()
108
+ }).strict();
109
+ const newMemoryRecordSchema = memoryRecordSchema.omit({
110
+ id: true,
111
+ revision: true,
112
+ createdAt: true,
113
+ updatedAt: true
114
+ });
115
+ const settingsSchema = z.object({
116
+ revision: z.number().int().nonnegative(),
117
+ injectEnabled: z.boolean(),
118
+ dreamIdleEnabled: z.boolean(),
119
+ idleMs: z.number().int().min(6e4).max(108e5)
120
+ }).strict();
121
+ const updateSettingsSchema = z.object({
122
+ expectedRevision: z.number().int().nonnegative(),
123
+ settings: settingsSchema.omit({ revision: true })
124
+ }).strict();
125
+ const tombstoneSchema = z.object({
126
+ id: z.string().min(1).max(80),
127
+ deletedAt: z.number().int().nonnegative(),
128
+ lastRevision: z.number().int().nonnegative()
129
+ }).strict();
130
+ const dreamSnapshotSchema = z.object({
131
+ id: z.string().min(1).max(80),
132
+ revision: z.number().int().nonnegative(),
133
+ status: memoryRecordSchema.shape.status,
134
+ scope: knowledgeScopeSchema,
135
+ expiresAt: z.number().int().positive().optional()
136
+ }).strict();
137
+ const dreamProposalSchema = z.object({
138
+ title: z.string().min(1).max(160),
139
+ content: z.string().min(1).max(4e3),
140
+ kind: z.enum([
141
+ "preference",
142
+ "project-fact",
143
+ "decision"
144
+ ]),
145
+ tags: z.array(z.string().min(1).max(40)).max(16),
146
+ exceptions: z.array(z.string().max(200)).max(16),
147
+ evidence: z.array(evidenceSchema).max(16),
148
+ sourceIds: z.array(z.string().min(1).max(80)).min(1).max(16),
149
+ scope: knowledgeScopeSchema
150
+ }).strict();
151
+ const dreamPlanSchema = z.object({
152
+ id: z.string().min(1).max(80),
153
+ revision: z.number().int().nonnegative(),
154
+ sessionId: z.string().min(1).max(200),
155
+ projectId: z.string().min(1).max(MAX_PROJECT_ID_CHARS).optional(),
156
+ status: z.enum([
157
+ "preview",
158
+ "applied",
159
+ "cancelled",
160
+ "stale",
161
+ "failed",
162
+ "noop"
163
+ ]),
164
+ sourceVersion: z.string().min(1).max(64),
165
+ snapshot: z.array(dreamSnapshotSchema).max(64),
166
+ proposals: z.array(dreamProposalSchema).max(16),
167
+ generation: z.number().int().nonnegative(),
168
+ createdAt: z.number().int().nonnegative(),
169
+ updatedAt: z.number().int().nonnegative(),
170
+ error: z.string().max(240).optional()
171
+ }).strict();
172
+ const createRpcSchema = z.object({
173
+ sessionId: z.string().min(1).max(200),
174
+ title: z.string().min(1).max(160),
175
+ content: z.string().min(1).max(4e3),
176
+ kind: z.enum([
177
+ "preference",
178
+ "project-fact",
179
+ "decision",
180
+ "lesson"
181
+ ]),
182
+ global: z.boolean().optional(),
183
+ tags: z.array(z.string().min(1).max(40)).max(16).optional(),
184
+ exceptions: z.array(z.string().max(200)).max(16).optional(),
185
+ evidence: z.array(evidenceSchema).max(16).optional(),
186
+ expiresAt: z.number().int().positive().optional()
187
+ }).strict();
188
+ const patchRpcSchema = z.object({
189
+ sessionId: z.string().min(1).max(200),
190
+ id: z.string().min(1).max(80),
191
+ expectedRevision: z.number().int().nonnegative(),
192
+ title: z.string().min(1).max(160).optional(),
193
+ content: z.string().min(1).max(4e3).optional(),
194
+ tags: z.array(z.string().min(1).max(40)).max(16).optional(),
195
+ exceptions: z.array(z.string().max(200)).max(16).optional(),
196
+ status: memoryRecordSchema.shape.status.optional(),
197
+ expiresAt: z.number().int().positive().optional()
198
+ }).strict();
199
+ const revisionRpcSchema = z.object({
200
+ sessionId: z.string().min(1).max(200),
201
+ id: z.string().min(1).max(80),
202
+ expectedRevision: z.number().int().nonnegative()
203
+ }).strict();
204
+ const listRpcSchema = z.object({
205
+ sessionId: z.string().min(1).max(200),
206
+ query: z.string().max(200).optional(),
207
+ kinds: z.array(memoryRecordSchema.shape.kind).max(4).optional(),
208
+ statuses: z.array(memoryRecordSchema.shape.status).max(8).optional(),
209
+ global: z.boolean().optional(),
210
+ limit: z.number().int().min(1).max(100).optional()
211
+ }).strict();
212
+ const MAX_MEMORY_RECORDS = 4096;
213
+ const MAX_MEMORY_TOMBSTONES = 4096;
214
+ const memoryStateSchema = z.object({
215
+ settings: settingsSchema,
216
+ records: z.array(memoryRecordSchema).max(MAX_MEMORY_RECORDS),
217
+ tombstones: z.array(tombstoneSchema).max(MAX_MEMORY_TOMBSTONES),
218
+ dreams: z.array(dreamPlanSchema).max(256),
219
+ lastAttemptAt: z.number().int().nonnegative().optional()
220
+ }).strict();
221
+ const memoryDomain = defineDomain({
222
+ name: "dsh_editor_memory",
223
+ version: 2,
224
+ tables: { state: domainTable(memoryStateSchema) }
225
+ });
226
+ function storedSettings(value) {
227
+ return value ? {
228
+ ...defaultSettings(),
229
+ ...value
230
+ } : defaultSettings();
231
+ }
232
+ //#endregion
233
+ //#region src/rpc.ts
234
+ async function handleMemoryRpc(endpoint, payload, signal, runtime, sessions) {
235
+ if (signal.aborted) return fail$1("MEMORY_CANCELLED", "请求已取消");
236
+ try {
237
+ if (endpoint === "status") {
238
+ const sessionId = parseSessionId(payload);
239
+ return ok(await runtime.readStatus(sessionId, sessionId ? projectOf(sessions, sessionId) : void 0));
240
+ }
241
+ if (endpoint === "settings.update") {
242
+ const parsed = updateSettingsSchema.safeParse(payload);
243
+ if (!parsed.success) return fail$1("MEMORY_INVALID", "记忆设置格式无效。");
244
+ return ok(await runtime.updateSettings(parsed.data.settings, parsed.data.expectedRevision));
245
+ }
246
+ if (endpoint === "records.list") {
247
+ const parsed = listRpcSchema.safeParse(payload);
248
+ if (!parsed.success) return fail$1("MEMORY_INVALID", "查询格式无效。");
249
+ const projectId = projectOf(sessions, parsed.data.sessionId);
250
+ const scope = parsed.data.global === true || !projectId ? { kind: "global" } : {
251
+ kind: "project",
252
+ projectId
253
+ };
254
+ return ok(await runtime.list({
255
+ scope,
256
+ query: parsed.data.query,
257
+ kinds: parsed.data.kinds,
258
+ statuses: parsed.data.statuses,
259
+ limit: parsed.data.limit
260
+ }));
261
+ }
262
+ if (endpoint === "records.create") {
263
+ const parsed = createRpcSchema.safeParse(payload);
264
+ if (!parsed.success) return fail$1("MEMORY_INVALID", "新增条目格式无效。");
265
+ const projectId = projectOf(sessions, parsed.data.sessionId);
266
+ return ok(await runtime.createManualRecord({
267
+ sessionId: parsed.data.sessionId,
268
+ projectId,
269
+ explicitGlobal: parsed.data.global === true,
270
+ title: parsed.data.title,
271
+ content: parsed.data.content,
272
+ kind: parsed.data.kind,
273
+ tags: parsed.data.tags,
274
+ exceptions: parsed.data.exceptions,
275
+ evidence: parsed.data.evidence,
276
+ expiresAt: parsed.data.expiresAt
277
+ }));
278
+ }
279
+ if (endpoint === "records.update") {
280
+ const parsed = patchRpcSchema.safeParse(payload);
281
+ if (!parsed.success) return fail$1("MEMORY_INVALID", "更新格式无效。");
282
+ const { id, expectedRevision, sessionId: _sessionId, ...patch } = parsed.data;
283
+ return ok(await runtime.update(id, patch, expectedRevision));
284
+ }
285
+ if (endpoint === "records.remove") {
286
+ const parsed = revisionRpcSchema.safeParse(payload);
287
+ if (!parsed.success) return fail$1("MEMORY_INVALID", "删除参数无效。");
288
+ await runtime.remove(parsed.data.id, parsed.data.expectedRevision);
289
+ return ok({ id: parsed.data.id });
290
+ }
291
+ if (endpoint === "records.accept") return mutation(runtime.accept.bind(runtime), payload);
292
+ if (endpoint === "records.reject") return mutation(runtime.reject.bind(runtime), payload);
293
+ if (endpoint === "records.revoke") return mutation(runtime.revoke.bind(runtime), payload);
294
+ if (endpoint === "dream.run") {
295
+ const sessionId = parseSessionId(payload);
296
+ if (!sessionId) return fail$1("MEMORY_INVALID", "缺少会话。");
297
+ return ok(await runtime.runIdleDream(sessionId, projectOf(sessions, sessionId), "manual"));
298
+ }
299
+ return fail$1("MEMORY_INVALID", "未知操作。");
300
+ } catch (error) {
301
+ const code = error instanceof MemoryError ? error.code : "MEMORY_FAILED";
302
+ return fail$1(code, error instanceof Error ? error.message : "记忆操作失败。");
303
+ }
304
+ }
305
+ function projectOf(sessions, sessionId) {
306
+ return projectIdFromCwd(sessionCwd(sessions(sessionId)));
307
+ }
308
+ async function mutation(run, payload) {
309
+ const parsed = revisionRpcSchema.safeParse(payload);
310
+ if (!parsed.success) return fail$1("MEMORY_INVALID", "参数无效。");
311
+ return ok(await run(parsed.data.id, parsed.data.expectedRevision));
312
+ }
313
+ //#endregion
314
+ //#region src/recall.ts
315
+ function estimateTokens(text) {
316
+ let tokens = 0;
317
+ for (const char of text) tokens += char.charCodeAt(0) > 127 ? 1 : .25;
318
+ return Math.max(1, Math.ceil(tokens));
319
+ }
320
+ function isExpired(record, now) {
321
+ return typeof record.expiresAt === "number" && record.expiresAt <= now;
322
+ }
323
+ function scopeMatches(record, query) {
324
+ if (query.kind === "global") return record.kind === "global";
325
+ if (record.kind === "global") return true;
326
+ return record.kind === "project" && record.projectId === query.projectId;
327
+ }
328
+ function recordMatchesQuery(record, query, now) {
329
+ if (!scopeMatches(record.scope, query.scope)) return false;
330
+ if (query.kinds && query.kinds.length > 0 && !query.kinds.includes(record.kind)) return false;
331
+ if (query.statuses && query.statuses.length > 0 && !query.statuses.includes(record.status)) return false;
332
+ if (query.query) {
333
+ const needle = query.query.trim().toLowerCase();
334
+ if (needle) {
335
+ if (!`${record.title}\n${record.content}\n${record.tags.join("\n")}`.toLowerCase().includes(needle)) return false;
336
+ }
337
+ }
338
+ return true;
339
+ }
340
+ function isRecallable(record, now) {
341
+ if (record.status !== "active") return false;
342
+ if (RECALL_EXCLUDED_STATUSES.includes(record.status)) return false;
343
+ if (isExpired(record, now)) return false;
344
+ return true;
345
+ }
346
+ function grams(text) {
347
+ const parts = new Set(text.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((part) => part.length >= 2));
348
+ for (const run of text.match(/\p{Script=Han}+/gu) ?? []) {
349
+ if (run.length === 1) parts.add(run);
350
+ for (let index = 0; index < run.length - 1; index += 1) parts.add(run.slice(index, index + 2));
351
+ }
352
+ return parts;
353
+ }
354
+ function relevanceScore(record, requestText) {
355
+ const request = grams(requestText);
356
+ if (request.size === 0) return 0;
357
+ const hay = grams(`${record.title}\n${record.content}\n${record.tags.join(" ")}\n${record.exceptions.join(" ")}`);
358
+ let hits = 0;
359
+ for (const token of request) if (hay.has(token)) hits += 1;
360
+ return hits / request.size;
361
+ }
362
+ function formatMemoryEntry(record) {
363
+ const lines = [`- ${record.title} [${record.kind} | ${scopeKey(record.scope)}]`, record.content];
364
+ if (record.exceptions.length) lines.push(` exceptions: ${record.exceptions.join("; ")}`);
365
+ return lines.join("\n");
366
+ }
367
+ function memorySnapshotPrefix() {
368
+ return [`[memory recall | plugin=${MEMORY_PLUGIN} | active only; lessons omitted]`, "These are accepted preferences, project facts, and decisions. They are not a user request and do not authorize file edits. Novel canon is not stored here."].join("\n");
369
+ }
370
+ function formatMemorySnapshot(records) {
371
+ if (records.length === 0) return memorySnapshotPrefix();
372
+ return [memorySnapshotPrefix(), ...records.map(formatMemoryEntry)].join("\n");
373
+ }
374
+ /** Rank by request relevance when a query is present. Fit whole records inside the wrapper budget. */
375
+ function boundRecall(records, now, options = {}) {
376
+ const cap = Math.min(5, Math.max(1, options.limit ?? 5));
377
+ const requestText = options.query?.trim() ?? "";
378
+ const eligible = records.filter((record) => isRecallable(record, now));
379
+ const ranked = requestText ? eligible.map((record) => ({
380
+ record,
381
+ score: relevanceScore(record, requestText)
382
+ })).filter((item) => item.score > 0).sort((a, b) => b.score - a.score || b.record.updatedAt - a.record.updatedAt || a.record.id.localeCompare(b.record.id)).map((item) => item.record) : eligible.slice().sort((a, b) => b.updatedAt - a.updatedAt || a.id.localeCompare(b.id));
383
+ const selected = [];
384
+ for (const record of ranked) {
385
+ if (selected.length >= cap) break;
386
+ if (estimateTokens(formatMemorySnapshot([...selected, record])) > 800) continue;
387
+ selected.push(record);
388
+ }
389
+ return selected;
390
+ }
391
+ function injectKinds(kinds) {
392
+ if (!kinds || kinds.length === 0) return [...INJECT_KINDS];
393
+ return kinds.filter((kind) => INJECT_KINDS.includes(kind));
394
+ }
395
+ //#endregion
396
+ //#region src/dream.ts
397
+ const DREAM_SYSTEM = [
398
+ "Consolidate the supplied memory records into merge or dedup candidate previews.",
399
+ "Keep each proposal in the same scope as its sources. Never expand a project record to global.",
400
+ "Do not rewrite active records in place. Do not invent preferences without quoted evidence.",
401
+ "Do not treat novel canon or worldbook text as Memory.",
402
+ "Expiry is a retention bound. Do not infer that a planned event completed merely because a date has elapsed.",
403
+ "A derived candidate must not outlive its sources. Do not drop or extend expiry to make knowledge perpetual.",
404
+ "Reply with JSON {\"proposals\":[{\"title\":string,\"content\":string,\"kind\":\"preference\"|\"project-fact\"|\"decision\",\"sourceIds\":string[],\"exceptions\":string[]}]} or {\"proposals\":[]}."
405
+ ].join(" ");
406
+ function isDreamSource(record, now) {
407
+ if (record.kind === "lesson") return false;
408
+ if (record.status !== "active" && record.status !== "candidate") return false;
409
+ if (isExpired(record, now)) return false;
410
+ return true;
411
+ }
412
+ function snapshotRecords(records) {
413
+ return records.map((record) => ({
414
+ id: record.id,
415
+ revision: record.revision,
416
+ status: record.status,
417
+ scope: structuredClone(record.scope),
418
+ expiresAt: record.expiresAt
419
+ })).sort((a, b) => a.id.localeCompare(b.id));
420
+ }
421
+ /** Bounded CAS token. Exact revisions still live on snapshot/basis, not in this hash. */
422
+ function dreamSourceVersion(snapshot) {
423
+ const canonical = snapshot.map((entry) => `${entry.id}@${entry.revision}:${entry.status}:${scopeKey(entry.scope)}:${entry.expiresAt ?? ""}`).sort().join("\n");
424
+ return createHash("sha256").update(canonical).digest("hex");
425
+ }
426
+ function earliestExpiry(records) {
427
+ let min;
428
+ for (const record of records) {
429
+ if (typeof record.expiresAt !== "number") continue;
430
+ min = min === void 0 ? record.expiresAt : Math.min(min, record.expiresAt);
431
+ }
432
+ return min;
433
+ }
434
+ function parseDreamText(text, snapshot, fallbackScope) {
435
+ const trimmed = text.trim();
436
+ if (!trimmed) return [];
437
+ const body = (trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1] ?? trimmed).trim();
438
+ const start = body.indexOf("{");
439
+ const end = body.lastIndexOf("}");
440
+ if (start < 0 || end <= start) return [];
441
+ let parsed;
442
+ try {
443
+ parsed = JSON.parse(body.slice(start, end + 1));
444
+ } catch {
445
+ return [];
446
+ }
447
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return [];
448
+ const raw = parsed.proposals;
449
+ if (!Array.isArray(raw)) return [];
450
+ const byId = new Map(snapshot.map((entry) => [entry.id, entry]));
451
+ const proposals = [];
452
+ for (const item of raw.slice(0, 16)) {
453
+ if (!item || typeof item !== "object" || Array.isArray(item)) continue;
454
+ const row = item;
455
+ if (typeof row.title !== "string" || typeof row.content !== "string") continue;
456
+ const kind = row.kind === "preference" || row.kind === "project-fact" || row.kind === "decision" ? row.kind : void 0;
457
+ if (!kind) continue;
458
+ const sourceIds = Array.isArray(row.sourceIds) ? [...new Set(row.sourceIds.filter((id) => typeof id === "string" && byId.has(id)))].slice(0, 16) : [];
459
+ if (sourceIds.length === 0) continue;
460
+ const sources = sourceIds.map((id) => byId.get(id)).filter((entry) => entry.status === "active" || entry.status === "candidate");
461
+ if (sources.length === 0) continue;
462
+ const scope = sources[0].scope;
463
+ if (sources.some((entry) => !scopesEqual(entry.scope, scope))) continue;
464
+ if (scope.kind === "project" && fallbackScope.kind === "global") continue;
465
+ if (fallbackScope.kind === "project" && scope.kind === "global") continue;
466
+ const exceptions = Array.isArray(row.exceptions) ? row.exceptions.filter((value) => typeof value === "string").map((value) => value.trim()).filter(Boolean).slice(0, 8) : [];
467
+ proposals.push({
468
+ title: row.title.trim().slice(0, 160),
469
+ content: row.content.trim().slice(0, 4e3),
470
+ kind,
471
+ tags: ["dream"],
472
+ exceptions,
473
+ evidence: [],
474
+ sourceIds,
475
+ scope: structuredClone(scope)
476
+ });
477
+ }
478
+ return proposals.filter((proposal) => proposal.title && proposal.content);
479
+ }
480
+ function recordMap(state) {
481
+ return new Map(state.records.map((record) => [record.id, record]));
482
+ }
483
+ function tombstoneSet(state) {
484
+ return new Set(state.tombstones.map((row) => row.id));
485
+ }
486
+ function assertLiveUnexpired(record, tombstoned, id, now, missing) {
487
+ if (tombstoned.has(id)) fail(MEMORY_TOMBSTONE, "记录已删除,不能由梦境恢复。");
488
+ if (!record) fail(MEMORY_STALE, missing);
489
+ if (record.status === "deleted") fail(MEMORY_TOMBSTONE, "记录已删除,不能由梦境恢复。");
490
+ if (isExpired(record, now)) fail(MEMORY_STALE, "依据记录已过期,梦境预览作废。");
491
+ return record;
492
+ }
493
+ function assertDreamApply(plan, current, tombstoned, now) {
494
+ if (plan.status !== "preview") fail(MEMORY_INVALID, "只能应用未处理的梦境预览。");
495
+ if (dreamSourceVersion(plan.snapshot) !== plan.sourceVersion) fail(MEMORY_STALE, "梦境快照已失效。");
496
+ for (const entry of plan.snapshot) {
497
+ const record = assertLiveUnexpired(current.get(entry.id), tombstoned, entry.id, now, "梦境所依据的记录已不存在。");
498
+ if (record.revision !== entry.revision) fail(MEMORY_STALE, "记录已被修改或删除,梦境预览作废。");
499
+ if (!scopesEqual(record.scope, entry.scope)) fail(MEMORY_STALE, "记录范围已变化,未扩大或覆盖。");
500
+ if ((record.expiresAt ?? void 0) !== (entry.expiresAt ?? void 0)) fail(MEMORY_STALE, "记录有效期已变化,梦境预览作废。");
501
+ }
502
+ for (const proposal of plan.proposals) {
503
+ if (proposal.scope.kind === "global" && plan.snapshot.some((entry) => proposal.sourceIds.includes(entry.id) && entry.scope.kind === "project")) fail(MEMORY_INVALID, "梦境不能把项目记忆扩大为全局。");
504
+ for (const sourceId of proposal.sourceIds) assertLiveUnexpired(current.get(sourceId), tombstoned, sourceId, now, "合并来源已不存在。");
505
+ }
506
+ }
507
+ /** Exact source revisions behind a derived candidate. Missing basis skips (manual adds). */
508
+ function assertBasisCurrent(record, current, tombstoned, now) {
509
+ if (isExpired(record, now)) fail(MEMORY_STALE, "候选已过期,不能采纳。");
510
+ if (!record.basis?.length) return;
511
+ for (const ref of record.basis) {
512
+ if (tombstoned.has(ref.id)) fail(MEMORY_TOMBSTONE, "依据记录已删除,不能采纳。");
513
+ const live = current.get(ref.id);
514
+ if (!live) fail(MEMORY_STALE, "依据记录已不存在,候选作废。");
515
+ if (live.revision !== ref.revision) fail(MEMORY_STALE, "依据记录已修改,须按当前版本重新整理。");
516
+ if (live.status === "deleted") fail(MEMORY_TOMBSTONE, "依据记录已删除,不能采纳。");
517
+ if (isExpired(live, now)) fail(MEMORY_STALE, "依据记录已过期,不能采纳。");
518
+ }
519
+ }
520
+ function stampInheritedExpiry(record, sources) {
521
+ const inherited = earliestExpiry(sources);
522
+ if (inherited === void 0) return record;
523
+ const current = record.expiresAt;
524
+ record.expiresAt = current === void 0 ? inherited : Math.min(current, inherited);
525
+ return record;
526
+ }
527
+ function basisFromSnapshot(snapshot, sourceIds) {
528
+ const byId = new Map(snapshot.map((entry) => [entry.id, entry]));
529
+ const basis = [];
530
+ for (const id of sourceIds) {
531
+ const entry = byId.get(id);
532
+ if (!entry) continue;
533
+ basis.push({
534
+ id: entry.id,
535
+ revision: entry.revision
536
+ });
537
+ }
538
+ return basis;
539
+ }
540
+ function inheritEvidence(records, sourceIds) {
541
+ const seen = /* @__PURE__ */ new Set();
542
+ const evidence = [];
543
+ for (const id of sourceIds) {
544
+ const record = records.find((item) => item.id === id);
545
+ if (!record) continue;
546
+ for (const ref of record.evidence) {
547
+ const key = `${ref.sessionId}:${ref.seq}:${ref.kind}`;
548
+ if (seen.has(key)) continue;
549
+ seen.add(key);
550
+ evidence.push({ ...ref });
551
+ }
552
+ }
553
+ return evidence.slice(0, 16);
554
+ }
555
+ //#endregion
556
+ //#region src/evidence.ts
557
+ const HUMAN_KINDS = ["preference", "project-fact"];
558
+ function hasEvidence(refs) {
559
+ return refs.some((ref) => ref.sessionId.trim().length > 0 && Number.isFinite(ref.seq) && ref.seq >= 0);
560
+ }
561
+ /** Preference/fact candidates need evidence or an explicit manual (user) add. Never infer global. */
562
+ function assertCreatable(record) {
563
+ if (record.scope.kind === "project" && !record.scope.projectId.trim()) fail(MEMORY_SCOPE, "项目记忆需要会话工作目录,不能改写为全局。");
564
+ if (record.kind === "lesson") {
565
+ if (record.source !== "self-improvement" && record.source !== "user") fail(MEMORY_INVALID, "教训条目只能由自我改进或手动添加写入。");
566
+ return;
567
+ }
568
+ if (HUMAN_KINDS.includes(record.kind)) {
569
+ if (record.source === "user") return;
570
+ if (record.source === "dream" && hasEvidence(record.evidence)) return;
571
+ if (hasEvidence(record.evidence) && (record.source === "memory" || record.source === "dream")) return;
572
+ fail(MEMORY_EVIDENCE, "偏好和项目事实需要原文依据,或由作者手动添加。");
573
+ }
574
+ }
575
+ function assertNoSilentGlobal(explicitGlobal, projectId) {
576
+ if (explicitGlobal) return;
577
+ if (!projectId) fail(MEMORY_SCOPE, "当前会话没有项目目录。写入全局须明确勾选。");
578
+ }
579
+ //#endregion
580
+ //#region src/inject.ts
581
+ function isMemoryInjectMessage(message) {
582
+ if (!message || typeof message !== "object") return false;
583
+ const row = message;
584
+ if (row.source?.kind !== "plugin:@klarkxy/dsh-memory" || row.source.plugin !== "@klarkxy/dsh-memory") return false;
585
+ return row.source.form === "snapshot" && Boolean(row.source.sections?.some((section) => section.name === "dsh-memory:recall"));
586
+ }
587
+ function asRecord(value) {
588
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
589
+ }
590
+ function textFromContent(content) {
591
+ if (!Array.isArray(content)) return typeof content === "string" ? content : "";
592
+ const parts = [];
593
+ for (const block of content) {
594
+ const row = asRecord(block);
595
+ if (!row) continue;
596
+ if (row.type === "text" && typeof row.text === "string") parts.push(row.text);
597
+ }
598
+ return parts.join("\n");
599
+ }
600
+ /** Latest real human user text. Plugin snapshots are not human context. */
601
+ function requestTextFromMessages(messages) {
602
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
603
+ const row = asRecord(messages[index]);
604
+ if (!row) continue;
605
+ if (asRecord(row.source)?.kind !== "user") continue;
606
+ const text = textFromContent(row.content).trim();
607
+ if (text) return text;
608
+ }
609
+ return "";
610
+ }
611
+ function memoryInjectPayload(records) {
612
+ const text = formatMemorySnapshot(records);
613
+ return {
614
+ content: [{
615
+ type: "text",
616
+ text
617
+ }],
618
+ source: {
619
+ kind: MEMORY_SOURCE_KIND,
620
+ plugin: MEMORY_PLUGIN,
621
+ form: "snapshot",
622
+ sections: [{
623
+ name: MEMORY_INJECTION_SECTION,
624
+ text
625
+ }]
626
+ }
627
+ };
628
+ }
629
+ function applyMemoryInjection(decision, records, createMessage) {
630
+ if (decision.kind !== "enter") return decision;
631
+ const without = decision.messages.filter((message) => !isMemoryInjectMessage(message));
632
+ if (records.length === 0) return {
633
+ ...decision,
634
+ messages: without
635
+ };
636
+ return {
637
+ ...decision,
638
+ messages: [createMessage(memoryInjectPayload(records)), ...without]
639
+ };
640
+ }
641
+ function stripMemoryInjection(decision) {
642
+ if (decision.kind !== "enter") return decision;
643
+ return {
644
+ ...decision,
645
+ messages: decision.messages.filter((message) => !isMemoryInjectMessage(message))
646
+ };
647
+ }
648
+ //#endregion
649
+ //#region src/capacity.ts
650
+ const TERMINAL_DREAM = /* @__PURE__ */ new Set([
651
+ "applied",
652
+ "cancelled",
653
+ "stale",
654
+ "failed",
655
+ "noop"
656
+ ]);
657
+ function memoryStateOverCapacity(state) {
658
+ return state.records.length > 4096 || state.tombstones.length > 4096 || state.dreams.length > 256;
659
+ }
660
+ function protectedTombstoneIds(state, activePlanIds = /* @__PURE__ */ new Set()) {
661
+ const ids = new Set(activePlanIds);
662
+ for (const record of state.records) {
663
+ for (const ref of record.basis ?? []) ids.add(ref.id);
664
+ for (const sourceId of record.supersedes ?? []) ids.add(sourceId);
665
+ }
666
+ for (const plan of state.dreams) {
667
+ for (const entry of plan.snapshot) ids.add(entry.id);
668
+ for (const proposal of plan.proposals) for (const sourceId of proposal.sourceIds) ids.add(sourceId);
669
+ }
670
+ return ids;
671
+ }
672
+ /** Drop oldest unused terminal dreams, then unreferenced tombstones, only when a bound is exceeded. */
673
+ function compactMemoryState(state, activePlanIds = /* @__PURE__ */ new Set()) {
674
+ if (state.dreams.length > 256) {
675
+ const droppable = state.dreams.filter((plan) => TERMINAL_DREAM.has(plan.status) && !activePlanIds.has(plan.id)).sort((left, right) => left.createdAt - right.createdAt || left.updatedAt - right.updatedAt || left.id.localeCompare(right.id));
676
+ const dropIds = new Set(droppable.slice(0, state.dreams.length - 256).map((plan) => plan.id));
677
+ if (dropIds.size) state.dreams = state.dreams.filter((plan) => !dropIds.has(plan.id));
678
+ }
679
+ if (state.tombstones.length > 4096) {
680
+ const protectedIds = protectedTombstoneIds(state, activePlanIds);
681
+ const droppable = state.tombstones.filter((row) => !protectedIds.has(row.id)).sort((left, right) => left.deletedAt - right.deletedAt || left.id.localeCompare(right.id));
682
+ const dropIds = new Set(droppable.slice(0, state.tombstones.length - MAX_MEMORY_TOMBSTONES).map((row) => row.id));
683
+ if (dropIds.size) state.tombstones = state.tombstones.filter((row) => !dropIds.has(row.id));
684
+ }
685
+ }
686
+ //#endregion
687
+ //#region src/store.ts
688
+ function emptyMemoryState() {
689
+ return {
690
+ settings: defaultSettings(),
691
+ records: [],
692
+ tombstones: [],
693
+ dreams: []
694
+ };
695
+ }
696
+ function cloneState(state) {
697
+ return structuredClone(state);
698
+ }
699
+ //#endregion
700
+ //#region src/service.ts
701
+ const dreamPurpose = {
702
+ id: MEMORY_DREAM_PURPOSE,
703
+ label: "记忆整理",
704
+ defaultTarget: {
705
+ kind: "role",
706
+ role: "strong"
707
+ },
708
+ maxOutputTokens: 1024,
709
+ maxInputChars: 12e3,
710
+ timeoutMs: 6e4
711
+ };
712
+ var MemoryRuntime = class {
713
+ options;
714
+ live;
715
+ generation = 0;
716
+ disposed = false;
717
+ storageFailed = false;
718
+ pending = Promise.resolve();
719
+ ai;
720
+ unregisterPurpose;
721
+ jobs = /* @__PURE__ */ new Map();
722
+ now;
723
+ customId;
724
+ constructor(options) {
725
+ this.options = options;
726
+ this.live = cloneState(options.store.load());
727
+ this.now = options.now ?? Date.now;
728
+ this.customId = options.id;
729
+ this.syncAi();
730
+ }
731
+ get pluginActive() {
732
+ return !this.disposed;
733
+ }
734
+ status(sessionId, projectId) {
735
+ const records = this.live.records.filter((record) => !this.tombstoned(record.id)).filter((record) => !sessionId || this.visibleInSession(record, projectId)).map(cloneRecord);
736
+ const dreams = this.live.dreams.filter((plan) => !sessionId || plan.sessionId === sessionId).map((plan) => structuredClone(plan));
737
+ return {
738
+ settings: structuredClone(this.live.settings),
739
+ records,
740
+ dreams,
741
+ runningDreams: dreams.filter((plan) => this.jobs.has(plan.id)).map((plan) => plan.id),
742
+ projectId,
743
+ storageFailed: this.storageFailed,
744
+ aiAvailable: Boolean(this.ai?.active)
745
+ };
746
+ }
747
+ /** Wait for durable state writes, never for model generation. */
748
+ async readStatus(sessionId, projectId) {
749
+ await this.pending;
750
+ return this.status(sessionId, projectId);
751
+ }
752
+ async dispose() {
753
+ this.disposed = true;
754
+ this.generation += 1;
755
+ this.abortDreams();
756
+ this.detachAi();
757
+ await this.pending;
758
+ }
759
+ async updateSettings(next, expectedRevision) {
760
+ return this.serialize(async () => {
761
+ this.assertOpen();
762
+ if (expectedRevision !== this.live.settings.revision) fail(MEMORY_CONFLICT, "记忆设置已更新,请刷新后重试。");
763
+ const proposed = this.snapshot();
764
+ proposed.settings = {
765
+ ...next,
766
+ revision: expectedRevision + 1
767
+ };
768
+ const disableInject = this.live.settings.injectEnabled && !proposed.settings.injectEnabled;
769
+ const disableDream = this.live.settings.dreamIdleEnabled && !proposed.settings.dreamIdleEnabled;
770
+ await this.persistProposed(proposed);
771
+ this.commit(proposed);
772
+ if (disableInject || disableDream) {
773
+ this.generation += 1;
774
+ this.abortDreams();
775
+ }
776
+ this.syncAi();
777
+ return structuredClone(this.live.settings);
778
+ });
779
+ }
780
+ async list(query) {
781
+ const now = this.now();
782
+ const limit = query.limit ?? 100;
783
+ return this.live.records.filter((record) => !this.tombstoned(record.id)).filter((record) => recordMatchesQuery(record, query, now)).sort((a, b) => b.updatedAt - a.updatedAt || a.id.localeCompare(b.id)).slice(0, Math.min(100, Math.max(1, limit))).map(cloneRecord);
784
+ }
785
+ async recall(query) {
786
+ return boundRecall(await this.list({
787
+ ...query,
788
+ query: void 0,
789
+ kinds: query.kinds && query.kinds.length ? query.kinds : [...INJECT_KINDS],
790
+ statuses: query.statuses ?? ["active"],
791
+ limit: 100
792
+ }), this.now(), {
793
+ query: query.query,
794
+ limit: query.limit ?? 5
795
+ });
796
+ }
797
+ async create(record, options) {
798
+ return this.serialize(async () => {
799
+ this.assertMutationCurrent(options);
800
+ this.assertOpen();
801
+ const parsed = newMemoryRecordSchema.safeParse(record);
802
+ if (!parsed.success) fail(MEMORY_INVALID, "记忆条目格式无效。");
803
+ assertCreatable(parsed.data);
804
+ const proposed = this.snapshot();
805
+ const id = this.mintId(proposed);
806
+ const now = this.now();
807
+ const stored = {
808
+ ...parsed.data,
809
+ id,
810
+ revision: 1,
811
+ createdAt: now,
812
+ updatedAt: now
813
+ };
814
+ proposed.records.push(stored);
815
+ await this.persistProposed(proposed);
816
+ this.commit(proposed);
817
+ return cloneRecord(stored);
818
+ });
819
+ }
820
+ async update(id, patch, expectedRevision, options) {
821
+ return this.serialize(async () => {
822
+ this.assertMutationCurrent(options);
823
+ return this.updateIn(this.snapshot(), id, patch, expectedRevision, true);
824
+ });
825
+ }
826
+ async remove(id, expectedRevision, options) {
827
+ return this.serialize(async () => {
828
+ this.assertMutationCurrent(options);
829
+ this.assertOpen();
830
+ const proposed = this.snapshot();
831
+ const current = this.requireIn(proposed, id);
832
+ if (current.revision !== expectedRevision) fail(MEMORY_CONFLICT, "条目已被其他操作修改,请刷新后重试。");
833
+ proposed.tombstones.push({
834
+ id,
835
+ deletedAt: this.now(),
836
+ lastRevision: current.revision
837
+ });
838
+ proposed.records = proposed.records.filter((record) => record.id !== id);
839
+ this.staleDreamsTouching(proposed, [id]);
840
+ await this.persistProposed(proposed);
841
+ this.commit(proposed);
842
+ });
843
+ }
844
+ async accept(id, expectedRevision) {
845
+ return this.serialize(async () => {
846
+ this.assertOpen();
847
+ const proposed = this.snapshot();
848
+ const current = this.requireIn(proposed, id);
849
+ if (current.revision !== expectedRevision) fail(MEMORY_CONFLICT, "条目已被其他操作修改,请刷新后重试。");
850
+ if (current.status !== "candidate") fail(MEMORY_INVALID, "只能采纳候选条目。");
851
+ const now = this.now();
852
+ assertBasisCurrent(current, recordMap(proposed), tombstoneSet(proposed), now);
853
+ stampInheritedExpiry(current, (current.basis ?? []).map((ref) => proposed.records.find((item) => item.id === ref.id)).filter((item) => Boolean(item)));
854
+ if (isExpired(current, now)) fail(MEMORY_STALE, "候选已过期,不能采纳。");
855
+ current.status = "active";
856
+ current.revision += 1;
857
+ current.updatedAt = now;
858
+ for (const sourceId of current.supersedes ?? []) {
859
+ const source = proposed.records.find((record) => record.id === sourceId);
860
+ if (!source || this.tombstonedIn(proposed, sourceId)) continue;
861
+ const pin = current.basis?.find((ref) => ref.id === sourceId);
862
+ if (current.basis?.length) {
863
+ if (!pin || source.revision !== pin.revision) fail(MEMORY_STALE, "依据记录已修改,须按当前版本重新整理。");
864
+ }
865
+ if (source.status === "deleted") fail(MEMORY_TOMBSTONE, "依据记录已删除,不能采纳。");
866
+ if (source.status === "active") {
867
+ source.status = "superseded";
868
+ source.revision += 1;
869
+ source.updatedAt = now;
870
+ }
871
+ }
872
+ this.staleDreamsTouching(proposed, [id, ...current.supersedes ?? []]);
873
+ await this.persistProposed(proposed);
874
+ this.commit(proposed);
875
+ return cloneRecord(this.requireIn(this.live, id));
876
+ });
877
+ }
878
+ async reject(id, expectedRevision) {
879
+ return this.serialize(async () => {
880
+ const proposed = this.snapshot();
881
+ return this.updateIn(proposed, id, { status: "rejected" }, expectedRevision, true, (record) => {
882
+ if (record.status !== "candidate") fail(MEMORY_INVALID, "只能拒绝候选条目。");
883
+ });
884
+ });
885
+ }
886
+ async revoke(id, expectedRevision) {
887
+ return this.serialize(async () => {
888
+ const proposed = this.snapshot();
889
+ return this.updateIn(proposed, id, { status: "revoked" }, expectedRevision, true, (record) => {
890
+ if (record.status !== "active") fail(MEMORY_INVALID, "只能撤销已生效条目。");
891
+ });
892
+ });
893
+ }
894
+ async promoteToGlobal(id, expectedRevision, options) {
895
+ return this.serialize(async () => {
896
+ this.assertMutationCurrent(options);
897
+ this.assertOpen();
898
+ const proposed = this.snapshot();
899
+ const current = this.requireIn(proposed, id);
900
+ if (current.revision !== expectedRevision) fail(MEMORY_CONFLICT, "条目已被其他操作修改,请刷新后重试。");
901
+ if (current.status !== "candidate" && current.status !== "active") fail(MEMORY_INVALID, "只能把未过期的候选或已生效条目提升为全局。");
902
+ const now = this.now();
903
+ if (isExpired(current, now)) fail(MEMORY_STALE, "条目已过期,不能提升为全局。");
904
+ current.scope = { kind: "global" };
905
+ current.status = "active";
906
+ current.revision += 1;
907
+ current.updatedAt = now;
908
+ this.staleDreamsTouching(proposed, [id]);
909
+ await this.persistProposed(proposed);
910
+ this.commit(proposed);
911
+ return cloneRecord(this.requireIn(this.live, id));
912
+ });
913
+ }
914
+ async handlePreStep(input) {
915
+ const generation = this.generation;
916
+ const decision = await input.next();
917
+ if (!this.canInject(generation, input.signal)) return stripMemoryInjection(decision);
918
+ if (decision.kind !== "enter") return decision;
919
+ const requestText = requestTextFromMessages(decision.messages);
920
+ if (!requestText) return stripMemoryInjection(decision);
921
+ const projectId = projectIdFromCwd(sessionCwd(input.session));
922
+ const scope = projectId ? {
923
+ kind: "project",
924
+ projectId
925
+ } : { kind: "global" };
926
+ const records = await this.recall({
927
+ scope,
928
+ query: requestText,
929
+ kinds: injectKinds(void 0),
930
+ limit: 5
931
+ });
932
+ if (!this.canInject(generation, input.signal)) return stripMemoryInjection(decision);
933
+ return applyMemoryInjection(decision, records, this.options.createInjectMessage ?? ((payload) => payload));
934
+ }
935
+ async previewDream(sessionId, projectId, trigger = "manual") {
936
+ this.requireAi();
937
+ const prepared = await this.serialize(async () => {
938
+ this.assertOpen();
939
+ if (trigger === "idle" && !this.live.settings.dreamIdleEnabled) fail(MEMORY_DISABLED, "闲时整理未开启。");
940
+ const generation = this.generation;
941
+ const now = this.now();
942
+ const scope = projectId ? {
943
+ kind: "project",
944
+ projectId
945
+ } : { kind: "global" };
946
+ const records = (await this.list({
947
+ scope,
948
+ statuses: ["active", "candidate"],
949
+ limit: 64
950
+ })).filter((record) => isDreamSource(record, now));
951
+ const snapshot = snapshotRecords(records);
952
+ const proposed = this.snapshot();
953
+ const plan = {
954
+ id: this.mintId(proposed),
955
+ revision: 1,
956
+ sessionId,
957
+ projectId,
958
+ status: "preview",
959
+ sourceVersion: dreamSourceVersion(snapshot),
960
+ snapshot,
961
+ proposals: [],
962
+ generation,
963
+ createdAt: now,
964
+ updatedAt: now
965
+ };
966
+ proposed.dreams.push(plan);
967
+ await this.persistProposed(proposed);
968
+ this.commit(proposed);
969
+ const abort = new AbortController();
970
+ this.jobs.set(plan.id, {
971
+ abort,
972
+ generation,
973
+ planId: plan.id
974
+ });
975
+ return {
976
+ plan,
977
+ records,
978
+ snapshot,
979
+ scope,
980
+ generation,
981
+ abort
982
+ };
983
+ });
984
+ const ai = this.requireAi();
985
+ try {
986
+ const result = await ai.run({
987
+ purpose: MEMORY_DREAM_PURPOSE,
988
+ sessionId,
989
+ sourceVersion: prepared.plan.sourceVersion,
990
+ system: DREAM_SYSTEM,
991
+ input: JSON.stringify({
992
+ scope: prepared.scope,
993
+ records: prepared.records.map((record) => ({
994
+ id: record.id,
995
+ kind: record.kind,
996
+ title: record.title,
997
+ content: record.content,
998
+ scope: record.scope,
999
+ exceptions: record.exceptions,
1000
+ evidence: record.evidence,
1001
+ status: record.status,
1002
+ revision: record.revision,
1003
+ expiresAt: record.expiresAt ?? null
1004
+ }))
1005
+ }),
1006
+ priority: trigger === "idle" ? "background" : "interactive",
1007
+ signal: prepared.abort.signal,
1008
+ isCurrent: () => this.isDreamCurrent(prepared.plan.id, prepared.generation)
1009
+ });
1010
+ return this.serialize(async () => {
1011
+ if (!this.isDreamCurrent(prepared.plan.id, prepared.generation)) return this.markDream(prepared.plan.id, {
1012
+ status: "stale",
1013
+ error: "已取消或设置已关闭。"
1014
+ });
1015
+ if (result.receipt.status !== "success") {
1016
+ const status = result.receipt.status === "cancelled" ? "cancelled" : "failed";
1017
+ return this.markDream(prepared.plan.id, {
1018
+ status,
1019
+ error: result.receipt.error ?? "整理未完成。"
1020
+ });
1021
+ }
1022
+ const proposals = parseDreamText(result.text, prepared.snapshot, prepared.scope).map((proposal) => ({
1023
+ ...proposal,
1024
+ evidence: inheritEvidence(prepared.records, proposal.sourceIds)
1025
+ }));
1026
+ return this.markDream(prepared.plan.id, {
1027
+ proposals,
1028
+ status: "preview"
1029
+ });
1030
+ });
1031
+ } catch (error) {
1032
+ return this.serialize(async () => {
1033
+ if (isAbortError(error) || !this.isDreamCurrent(prepared.plan.id, prepared.generation)) return this.markDream(prepared.plan.id, {
1034
+ status: "cancelled",
1035
+ error: "已取消。"
1036
+ });
1037
+ return this.markDream(prepared.plan.id, {
1038
+ status: "failed",
1039
+ error: "整理失败。"
1040
+ });
1041
+ });
1042
+ } finally {
1043
+ this.jobs.delete(prepared.plan.id);
1044
+ }
1045
+ }
1046
+ async runIdleDream(sessionId, projectId, trigger = "idle") {
1047
+ try {
1048
+ const plan = await this.previewDream(sessionId, projectId, trigger);
1049
+ if (plan.status !== "preview" || plan.proposals.length === 0) {
1050
+ if (plan.status === "preview") await this.markDreamQuietly(plan.id, { status: "noop" });
1051
+ await this.stampDreamAttempt();
1052
+ return this.currentDream(plan.id) ?? plan;
1053
+ }
1054
+ try {
1055
+ const applied = await this.applyDream(plan.id, plan.revision);
1056
+ await this.stampDreamAttempt();
1057
+ return applied;
1058
+ } catch {
1059
+ await this.markDreamQuietly(plan.id, {
1060
+ status: "failed",
1061
+ error: "自动整理应用失败。"
1062
+ });
1063
+ await this.stampDreamAttempt();
1064
+ return this.currentDream(plan.id) ?? plan;
1065
+ }
1066
+ } catch (error) {
1067
+ await this.recordFailedDreamAttempt(sessionId, projectId, error);
1068
+ throw error;
1069
+ }
1070
+ }
1071
+ get dreamLastAttemptAt() {
1072
+ return this.live.lastAttemptAt;
1073
+ }
1074
+ dreamMaterialCount() {
1075
+ return countDreamMaterial(this.live, this.live.lastAttemptAt);
1076
+ }
1077
+ async applyDream(planId, expectedRevision) {
1078
+ return this.serialize(async () => {
1079
+ this.assertOpen();
1080
+ if (this.jobs.has(planId)) fail(MEMORY_INVALID, "整理尚未完成,请稍候。");
1081
+ const proposed = this.snapshot();
1082
+ const plan = this.requireDreamIn(proposed, planId);
1083
+ if (plan.revision !== expectedRevision) fail(MEMORY_CONFLICT, "梦境预览已更新,请刷新后重试。");
1084
+ const now = this.now();
1085
+ try {
1086
+ assertDreamApply(plan, recordMap(proposed), tombstoneSet(proposed), now);
1087
+ } catch (error) {
1088
+ plan.status = "stale";
1089
+ plan.revision += 1;
1090
+ plan.updatedAt = this.now();
1091
+ plan.error = error instanceof Error ? error.message : "预览已过期。";
1092
+ await this.persistProposed(proposed);
1093
+ this.commit(proposed);
1094
+ throw error instanceof Error ? error : fail(MEMORY_STALE, "预览已过期。");
1095
+ }
1096
+ const touched = /* @__PURE__ */ new Set();
1097
+ for (const proposal of plan.proposals) {
1098
+ const id = this.mintId(proposed);
1099
+ const sources = proposal.sourceIds.map((sourceId) => proposed.records.find((item) => item.id === sourceId)).filter((item) => Boolean(item));
1100
+ const record = {
1101
+ id,
1102
+ revision: 1,
1103
+ scope: proposal.scope,
1104
+ kind: proposal.kind,
1105
+ status: "active",
1106
+ title: proposal.title,
1107
+ content: proposal.content,
1108
+ tags: proposal.tags,
1109
+ evidence: hasEvidence(proposal.evidence) ? proposal.evidence : [{
1110
+ sessionId: plan.sessionId,
1111
+ seq: 0,
1112
+ kind: "manual",
1113
+ excerpt: "dream"
1114
+ }],
1115
+ exceptions: proposal.exceptions,
1116
+ source: "dream",
1117
+ createdAt: now,
1118
+ updatedAt: now,
1119
+ supersedes: proposal.sourceIds,
1120
+ basis: basisFromSnapshot(plan.snapshot, proposal.sourceIds)
1121
+ };
1122
+ stampInheritedExpiry(record, sources);
1123
+ if (isExpired(record, now)) fail(MEMORY_STALE, "依据记录已过期,梦境预览作废。");
1124
+ assertCreatable(record);
1125
+ proposed.records.push(record);
1126
+ touched.add(id);
1127
+ for (const sourceId of proposal.sourceIds) {
1128
+ const source = proposed.records.find((item) => item.id === sourceId);
1129
+ if (!source || this.tombstonedIn(proposed, sourceId)) continue;
1130
+ if (source.status === "active") {
1131
+ source.status = "superseded";
1132
+ source.revision += 1;
1133
+ source.updatedAt = now;
1134
+ }
1135
+ touched.add(sourceId);
1136
+ }
1137
+ }
1138
+ plan.status = "applied";
1139
+ plan.revision += 1;
1140
+ plan.updatedAt = now;
1141
+ this.staleDreamsTouching(proposed, [...touched]);
1142
+ await this.persistProposed(proposed);
1143
+ this.commit(proposed);
1144
+ return structuredClone(this.requireDreamIn(this.live, planId));
1145
+ });
1146
+ }
1147
+ async cancelDream(planId, expectedRevision) {
1148
+ return this.serialize(async () => {
1149
+ const plan = this.live.dreams.find((item) => item.id === planId);
1150
+ if (!plan) fail(MEMORY_NOT_FOUND, "找不到该梦境预览。");
1151
+ if (plan.revision !== expectedRevision) fail(MEMORY_CONFLICT, "梦境预览已更新,请刷新后重试。");
1152
+ this.jobs.get(planId)?.abort.abort();
1153
+ this.jobs.delete(planId);
1154
+ return this.markDream(planId, { status: "cancelled" });
1155
+ });
1156
+ }
1157
+ createManualRecord(input) {
1158
+ assertNoSilentGlobal(input.explicitGlobal, input.projectId);
1159
+ const scope = input.explicitGlobal ? { kind: "global" } : {
1160
+ kind: "project",
1161
+ projectId: input.projectId
1162
+ };
1163
+ return this.create({
1164
+ scope,
1165
+ kind: input.kind,
1166
+ status: "active",
1167
+ title: input.title,
1168
+ content: input.content,
1169
+ tags: input.tags ?? [],
1170
+ exceptions: input.exceptions ?? [],
1171
+ evidence: input.evidence ?? [],
1172
+ source: "user",
1173
+ expiresAt: input.expiresAt
1174
+ });
1175
+ }
1176
+ abortDreams() {
1177
+ for (const job of this.jobs.values()) job.abort.abort();
1178
+ this.jobs.clear();
1179
+ }
1180
+ hasActiveDream(sessionId) {
1181
+ for (const job of this.jobs.values()) {
1182
+ const plan = this.live.dreams.find((item) => item.id === job.planId);
1183
+ if (!plan) continue;
1184
+ if (!sessionId || plan.sessionId === sessionId) return true;
1185
+ }
1186
+ return false;
1187
+ }
1188
+ async updateIn(proposed, id, patch, expectedRevision, persist, guard) {
1189
+ this.assertOpen();
1190
+ const current = this.requireIn(proposed, id);
1191
+ if (current.revision !== expectedRevision) fail(MEMORY_CONFLICT, "条目已被其他操作修改,请刷新后重试。");
1192
+ if (current.status === "deleted") fail(MEMORY_DELETED, "条目已删除,不能恢复。");
1193
+ guard?.(current);
1194
+ Object.assign(current, patch, {
1195
+ id: current.id,
1196
+ revision: current.revision + 1,
1197
+ scope: current.scope,
1198
+ kind: current.kind,
1199
+ source: current.source,
1200
+ createdAt: current.createdAt,
1201
+ updatedAt: this.now()
1202
+ });
1203
+ this.staleDreamsTouching(proposed, [id]);
1204
+ if (persist) {
1205
+ await this.persistProposed(proposed);
1206
+ this.commit(proposed);
1207
+ return cloneRecord(this.requireIn(this.live, id));
1208
+ }
1209
+ return cloneRecord(current);
1210
+ }
1211
+ mintId(proposed) {
1212
+ if (this.customId) {
1213
+ const id = this.customId();
1214
+ if (this.idTaken(proposed, id)) {
1215
+ if (this.tombstonedIn(proposed, id)) fail(MEMORY_TOMBSTONE, "该标识已删除,不能恢复。");
1216
+ fail(MEMORY_CONFLICT, "记忆编号冲突。");
1217
+ }
1218
+ return id;
1219
+ }
1220
+ for (let attempt = 0; attempt < 8; attempt += 1) {
1221
+ const id = randomUUID();
1222
+ if (!this.idTaken(proposed, id)) return id;
1223
+ }
1224
+ fail(MEMORY_CONFLICT, "记忆编号冲突。");
1225
+ }
1226
+ idTaken(state, id) {
1227
+ return this.tombstonedIn(state, id) || state.records.some((record) => record.id === id) || state.dreams.some((plan) => plan.id === id);
1228
+ }
1229
+ tombstoned(id) {
1230
+ return this.tombstonedIn(this.live, id);
1231
+ }
1232
+ tombstonedIn(state, id) {
1233
+ return state.tombstones.some((row) => row.id === id);
1234
+ }
1235
+ requireIn(state, id) {
1236
+ if (this.tombstonedIn(state, id)) fail(MEMORY_TOMBSTONE, "条目已删除,不能恢复。");
1237
+ const record = state.records.find((item) => item.id === id);
1238
+ if (!record) fail(MEMORY_NOT_FOUND, "找不到该记忆条目。");
1239
+ return record;
1240
+ }
1241
+ requireDreamIn(state, id) {
1242
+ const plan = state.dreams.find((item) => item.id === id);
1243
+ if (!plan) fail(MEMORY_NOT_FOUND, "找不到该梦境预览。");
1244
+ return plan;
1245
+ }
1246
+ async markDream(id, patch) {
1247
+ const proposed = this.snapshot();
1248
+ const plan = this.requireDreamIn(proposed, id);
1249
+ Object.assign(plan, patch, {
1250
+ revision: plan.revision + 1,
1251
+ updatedAt: this.now()
1252
+ });
1253
+ await this.persistProposed(proposed);
1254
+ this.commit(proposed);
1255
+ return structuredClone(this.requireDreamIn(this.live, id));
1256
+ }
1257
+ currentDream(id) {
1258
+ const plan = this.live.dreams.find((item) => item.id === id);
1259
+ return plan ? structuredClone(plan) : void 0;
1260
+ }
1261
+ async markDreamQuietly(id, patch) {
1262
+ try {
1263
+ await this.serialize(async () => {
1264
+ const current = this.live.dreams.find((item) => item.id === id);
1265
+ if (!current || current.status !== "preview") return;
1266
+ await this.markDream(id, patch);
1267
+ });
1268
+ } catch {}
1269
+ }
1270
+ async stampDreamAttempt() {
1271
+ try {
1272
+ await this.serialize(async () => {
1273
+ const proposed = this.snapshot();
1274
+ proposed.lastAttemptAt = this.now();
1275
+ await this.persistProposed(proposed);
1276
+ this.commit(proposed);
1277
+ });
1278
+ } catch {}
1279
+ }
1280
+ async recordFailedDreamAttempt(sessionId, projectId, error) {
1281
+ try {
1282
+ await this.serialize(async () => {
1283
+ const proposed = this.snapshot();
1284
+ const now = this.now();
1285
+ proposed.lastAttemptAt = now;
1286
+ proposed.dreams.push({
1287
+ id: this.mintId(proposed),
1288
+ revision: 1,
1289
+ sessionId,
1290
+ projectId,
1291
+ status: "failed",
1292
+ sourceVersion: dreamSourceVersion([]),
1293
+ snapshot: [],
1294
+ proposals: [],
1295
+ generation: this.generation,
1296
+ createdAt: now,
1297
+ updatedAt: now,
1298
+ error: error instanceof Error ? error.message.slice(0, 240) : "整理失败。"
1299
+ });
1300
+ await this.persistProposed(proposed);
1301
+ this.commit(proposed);
1302
+ });
1303
+ } catch {}
1304
+ }
1305
+ staleDreamsTouching(state, ids) {
1306
+ const set = new Set(ids);
1307
+ const now = this.now();
1308
+ for (const plan of state.dreams) {
1309
+ if (plan.status !== "preview") continue;
1310
+ if (plan.snapshot.some((entry) => set.has(entry.id))) {
1311
+ plan.status = "stale";
1312
+ plan.revision += 1;
1313
+ plan.updatedAt = now;
1314
+ plan.error = "相关记录已删除或修正。";
1315
+ }
1316
+ }
1317
+ }
1318
+ visibleInSession(record, projectId) {
1319
+ if (record.scope.kind === "global") return true;
1320
+ return Boolean(projectId) && record.scope.kind === "project" && record.scope.projectId === projectId;
1321
+ }
1322
+ canInject(generation, signal) {
1323
+ return !this.disposed && this.live.settings.injectEnabled && this.generation === generation && !signal.aborted;
1324
+ }
1325
+ isDreamCurrent(planId, generation) {
1326
+ if (this.disposed || this.generation !== generation) return false;
1327
+ const plan = this.live.dreams.find((item) => item.id === planId);
1328
+ return Boolean(plan && plan.status === "preview" && this.ai?.active);
1329
+ }
1330
+ requireAi() {
1331
+ this.syncAi();
1332
+ if (!this.ai?.active) fail(MEMORY_AI_UNAVAILABLE, "需要先加载 @klarkxy/dsh-ai-services,记忆插件不会自动启用它。");
1333
+ return this.ai;
1334
+ }
1335
+ syncAi() {
1336
+ if (this.disposed) {
1337
+ this.detachAi();
1338
+ return;
1339
+ }
1340
+ if (this.ai?.active) return;
1341
+ const ai = this.options.activateAi?.();
1342
+ this.ai = ai;
1343
+ if (ai) try {
1344
+ this.unregisterPurpose = ai.registerPurpose(dreamPurpose);
1345
+ } catch {
1346
+ this.unregisterPurpose = void 0;
1347
+ }
1348
+ }
1349
+ detachAi() {
1350
+ this.unregisterPurpose?.();
1351
+ this.unregisterPurpose = void 0;
1352
+ this.ai?.dispose();
1353
+ this.ai = void 0;
1354
+ }
1355
+ assertOpen() {
1356
+ if (this.disposed) fail(MEMORY_DISABLED, "记忆插件已停止。");
1357
+ }
1358
+ snapshot() {
1359
+ return cloneState(this.live);
1360
+ }
1361
+ commit(proposed) {
1362
+ this.live = cloneState(proposed);
1363
+ }
1364
+ assertMutationCurrent(options) {
1365
+ if (!options) return;
1366
+ if (options.signal?.aborted) fail(MEMORY_CANCELLED, "请求已取消");
1367
+ if (!options.isCurrent) return;
1368
+ let current = false;
1369
+ try {
1370
+ current = options.isCurrent();
1371
+ } catch {
1372
+ fail(MEMORY_CANCELLED, "请求已取消");
1373
+ }
1374
+ if (!current) fail(MEMORY_CANCELLED, "请求已取消");
1375
+ }
1376
+ async persistProposed(proposed) {
1377
+ compactMemoryState(proposed, new Set(this.jobs.keys()));
1378
+ if (!memoryStateSchema.safeParse(proposed).success) {
1379
+ if (memoryStateOverCapacity(proposed)) fail(MEMORY_CAPACITY, "记忆容量已满,已保留原内容。");
1380
+ fail(MEMORY_INVALID, "记忆状态格式无效。");
1381
+ }
1382
+ try {
1383
+ await this.options.store.save(cloneState(proposed));
1384
+ this.storageFailed = false;
1385
+ } catch (error) {
1386
+ if (error instanceof MemoryError && (error.code === "MEMORY_CANCELLED" || error.code === "MEMORY_CAPACITY")) throw error;
1387
+ this.storageFailed = true;
1388
+ if (error instanceof Error && "code" in error) throw error;
1389
+ fail(MEMORY_SAVE_FAILED, "记忆保存失败,已保留原内容。");
1390
+ }
1391
+ }
1392
+ serialize(run) {
1393
+ const task = this.pending.then(run, run);
1394
+ this.pending = task.then(() => {}, () => {});
1395
+ return task;
1396
+ }
1397
+ };
1398
+ //#endregion
1399
+ //#region src/index.ts
1400
+ const name = MEMORY_PLUGIN;
1401
+ const inject = [
1402
+ "storageDomain",
1403
+ "connection",
1404
+ "webServer",
1405
+ "aiServices",
1406
+ "sessions"
1407
+ ];
1408
+ async function apply(ctx) {
1409
+ const host = ctx;
1410
+ const domain = await host.storageDomain.open(memoryDomain);
1411
+ const store = domainStore(domain);
1412
+ const idle = {
1413
+ agentIdle: /* @__PURE__ */ new Map(),
1414
+ lastActivity: /* @__PURE__ */ new Map(),
1415
+ timers: /* @__PURE__ */ new Map()
1416
+ };
1417
+ const runtime = new MemoryRuntime({
1418
+ store,
1419
+ activateAi: () => host.aiServices.activate(MEMORY_ACTIVATE_ID),
1420
+ createInjectMessage: (payload) => createPluginUserMessage(payload)
1421
+ });
1422
+ ctx.provide("aiMemory", runtime);
1423
+ ctx.effect(() => async () => {
1424
+ for (const timer of idle.timers.values()) clearTimeout(timer);
1425
+ idle.timers.clear();
1426
+ await runtime.dispose();
1427
+ await domain.close();
1428
+ }, "dsh-memory.dispose");
1429
+ ctx.effect(() => registerHostRpc(host, MEMORY_RPC_CHANNEL, (endpoint, payload, signal) => handleMemoryRpc(endpoint, payload, signal, runtime, (sessionId) => readSession(ctx, sessionId))), "dsh-memory.rpc");
1430
+ ctx.effect(() => {
1431
+ const offStep = listen(ctx, "agent/pre-step", async (payload, next) => runtime.handlePreStep({
1432
+ sessionId: String(payload.agent.id ?? ""),
1433
+ session: payload.agent.session,
1434
+ signal: payload.signal,
1435
+ next
1436
+ }));
1437
+ const offStatus = listen(ctx, "agent/status", (payload) => {
1438
+ const sessionId = String(payload.agent.id ?? "");
1439
+ if (!sessionId) return;
1440
+ const now = Date.now();
1441
+ if (payload.status === "running") {
1442
+ idle.agentIdle.set(sessionId, false);
1443
+ idle.lastActivity.set(sessionId, now);
1444
+ const timer = idle.timers.get(sessionId);
1445
+ if (timer) {
1446
+ clearTimeout(timer);
1447
+ idle.timers.delete(sessionId);
1448
+ }
1449
+ return;
1450
+ }
1451
+ if (payload.status !== "idle") return;
1452
+ idle.agentIdle.set(sessionId, true);
1453
+ idle.lastActivity.set(sessionId, idle.lastActivity.get(sessionId) ?? now);
1454
+ const timer = idle.timers.get(sessionId);
1455
+ if (timer) clearTimeout(timer);
1456
+ const wait = runtime.status().settings.idleMs;
1457
+ idle.timers.set(sessionId, setTimeout(() => {
1458
+ idle.timers.delete(sessionId);
1459
+ const settings = runtime.status().settings;
1460
+ if (!shouldRunIdleDream({
1461
+ dreamIdleEnabled: settings.dreamIdleEnabled,
1462
+ pluginActive: runtime.pluginActive,
1463
+ agentIdle: idle.agentIdle.get(sessionId) === true,
1464
+ dreamRunning: runtime.hasActiveDream(sessionId),
1465
+ lastActivityAt: idle.lastActivity.get(sessionId) ?? now,
1466
+ now: Date.now(),
1467
+ idleMs: settings.idleMs,
1468
+ lastAttemptAt: runtime.dreamLastAttemptAt,
1469
+ materialCount: runtime.dreamMaterialCount()
1470
+ })) return;
1471
+ runtime.runIdleDream(sessionId, projectIdFromCwd(sessionCwd(readSession(ctx, sessionId))), "idle").catch(() => {});
1472
+ }, wait));
1473
+ });
1474
+ return () => {
1475
+ offStep?.();
1476
+ offStatus?.();
1477
+ };
1478
+ }, "dsh-memory.hooks");
1479
+ }
1480
+ function createPluginUserMessage(payload) {
1481
+ return createUserMessage({
1482
+ source: payload.source,
1483
+ content: payload.content
1484
+ });
1485
+ }
1486
+ function domainStore(domain) {
1487
+ const table = domain.table("state");
1488
+ return {
1489
+ load() {
1490
+ const stored = table.get("current");
1491
+ if (!stored) return emptyMemoryState();
1492
+ return cloneState({
1493
+ settings: storedSettings(stored.settings),
1494
+ records: stored.records ?? [],
1495
+ tombstones: stored.tombstones ?? [],
1496
+ dreams: stored.dreams ?? [],
1497
+ lastAttemptAt: stored.lastAttemptAt
1498
+ });
1499
+ },
1500
+ async save(state) {
1501
+ await table.put("current", cloneState(state));
1502
+ }
1503
+ };
1504
+ }
1505
+ function readSession(ctx, sessionId) {
1506
+ return ctx.sessions.get(sessionId);
1507
+ }
1508
+ function listen(ctx, name, handler) {
1509
+ const off = ctx.on.call(ctx, name, handler);
1510
+ return typeof off === "function" ? off : void 0;
1511
+ }
1512
+ //#endregion
1513
+ export { CHAT_EVENTS_SLOT, MEMORY_RPC_CHANNEL, MemoryRuntime, apply, createPluginUserMessage, defaultSettings, inject, name, projectIdFromCwd };
1514
+
1515
+ //# sourceMappingURL=index.js.map