dsh-plugin-xujie-memory 0.5.0 → 0.7.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.
Files changed (84) hide show
  1. package/ACCEPTANCE.md +37 -0
  2. package/BUILD-PROVENANCE.json +53 -0
  3. package/CHANGELOG.md +40 -1
  4. package/LICENSE +21 -21
  5. package/README.md +67 -17
  6. package/README_EN.md +54 -8
  7. package/cordis.patch.yml +49 -49
  8. package/lib/client.js +17219 -0
  9. package/lib/controller.js +307 -0
  10. package/lib/index.js +200 -14
  11. package/lib/typert.host.d.ts +3 -0
  12. package/lib/typert.host.js +15104 -0
  13. package/lib/typert.remote-client.d.ts +40 -0
  14. package/lib/typert.remote-client.d.ts.map +1 -0
  15. package/lib/typert.remote-client.js +15093 -0
  16. package/lib/types.d.ts +13 -0
  17. package/lib/types.js +1 -0
  18. package/package.json +49 -5
  19. package/python/specs/characters/aloof.json +14 -14
  20. package/python/specs/characters/gentle.json +14 -14
  21. package/python/specs/characters/tsundere.json +14 -14
  22. package/python/specs/debt.v1.json +31 -31
  23. package/python/specs/nickname.v1.json +16 -16
  24. package/python/specs/preference.v1.json +16 -16
  25. package/python/xujie_memory/__init__.py +123 -123
  26. package/python/xujie_memory/admission.py +211 -211
  27. package/python/xujie_memory/audit.py +377 -377
  28. package/python/xujie_memory/benchmark.py +281 -281
  29. package/python/xujie_memory/character.py +199 -199
  30. package/python/xujie_memory/console.py +41 -41
  31. package/python/xujie_memory/consolidate.py +356 -356
  32. package/python/xujie_memory/due.py +121 -121
  33. package/python/xujie_memory/emotion.py +573 -573
  34. package/python/xujie_memory/engine.py +1333 -1312
  35. package/python/xujie_memory/errors.py +98 -98
  36. package/python/xujie_memory/importance.py +104 -104
  37. package/python/xujie_memory/judge.py +350 -350
  38. package/python/xujie_memory/management.py +203 -0
  39. package/python/xujie_memory/memory.py +71 -10
  40. package/python/xujie_memory/orchestrator.py +690 -666
  41. package/python/xujie_memory/phrasing.py +292 -292
  42. package/python/xujie_memory/privacy.py +105 -0
  43. package/python/xujie_memory/schema.sql +492 -480
  44. package/python/xujie_memory/server.py +76 -0
  45. package/python/xujie_memory/specs.py +170 -170
  46. package/python/xujie_memory/summary.py +238 -238
  47. package/scripts/build.mjs +19 -0
  48. package/scripts/prepack.mjs +9 -0
  49. package/scripts/test-all.mjs +9 -0
  50. package/scripts/test-node.mjs +10 -0
  51. package/src/client/MemoryPanel.tsx +536 -0
  52. package/src/client/NeuralField.tsx +88 -0
  53. package/src/client/css.d.ts +4 -0
  54. package/src/client/icons.tsx +28 -0
  55. package/src/client/index.tsx +59 -0
  56. package/src/client/memory-adapter.ts +67 -0
  57. package/src/client/model.ts +79 -0
  58. package/src/client/neural/engine.ts +999 -0
  59. package/src/client/neural/morphology.ts +219 -0
  60. package/src/client/panel.css +337 -0
  61. package/src/client/types.ts +6 -0
  62. package/src/controller.ts +88 -0
  63. package/src/generated/typert.host.js +449 -0
  64. package/src/generated/typert.remote-client.js +438 -0
  65. package/src/index.ts +2 -0
  66. package/src/types.ts +13 -0
  67. package/test/cordis-fixture.mjs +5 -0
  68. package/test/management_regression.py +187 -0
  69. package/test/node-regression.entry.mjs +52 -0
  70. package/vendor/DSH-LICENSE.txt +21 -0
  71. package/vendor/THIRD-PARTY.md +6 -0
  72. package/vendor/dsh-protocol/index.ts +484 -0
  73. package/vendor/dsh-protocol/json-value.ts +53 -0
  74. package/vendor/dsh-protocol/owned-value.ts +39 -0
  75. package/vendor/dsh-protocol/principal.ts +17 -0
  76. package/vendor/dsh-protocol/remote-access.ts +38 -0
  77. package/vendor/dsh-protocol/remote-error.ts +49 -0
  78. package/vendor/dsh-protocol/remote-info.ts +161 -0
  79. package/vendor/dsh-protocol/types.ts +939 -0
  80. package/vendor/licenses/jridgewell-gen-mapping-MIT.txt +19 -0
  81. package/vendor/licenses/jridgewell-resolve-uri-MIT.txt +19 -0
  82. package/vendor/licenses/jridgewell-sourcemap-codec-MIT.txt +19 -0
  83. package/vendor/licenses/jridgewell-trace-mapping-MIT.txt +19 -0
  84. package/vendor/licenses/zod-MIT.txt +21 -0
@@ -0,0 +1,307 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name);
5
+ var __typeError = (msg) => {
6
+ throw TypeError(msg);
7
+ };
8
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
9
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
10
+ var __decoratorStart = (base) => [, , , __create(base?.[__knownSymbol("metadata")] ?? null)];
11
+ var __decoratorStrings = ["class", "method", "getter", "setter", "accessor", "field", "value", "get", "set"];
12
+ var __expectFn = (fn) => fn !== void 0 && typeof fn !== "function" ? __typeError("Function expected") : fn;
13
+ var __decoratorContext = (kind, name, done, metadata, fns) => ({ kind: __decoratorStrings[kind], name, metadata, addInitializer: (fn) => done._ ? __typeError("Already initialized") : fns.push(__expectFn(fn || null)) });
14
+ var __decoratorMetadata = (array, target) => __defNormalProp(target, __knownSymbol("metadata"), array[3]);
15
+ var __runInitializers = (array, flags, self, value) => {
16
+ for (var i = 0, fns = array[flags >> 1], n = fns && fns.length; i < n; i++) flags & 1 ? fns[i].call(self) : value = fns[i].call(self, value);
17
+ return value;
18
+ };
19
+ var __decorateElement = (array, flags, name, decorators, target, extra) => {
20
+ var fn, it, done, ctx, access, k = flags & 7, s = !!(flags & 8), p = !!(flags & 16);
21
+ var j = k > 3 ? array.length + 1 : k ? s ? 1 : 2 : 0, key = __decoratorStrings[k + 5];
22
+ var initializers = k > 3 && (array[j - 1] = []), extraInitializers = array[j] || (array[j] = []);
23
+ var desc = k && (!p && !s && (target = target.prototype), k < 5 && (k > 3 || !p) && __getOwnPropDesc(k < 4 ? target : { get [name]() {
24
+ return __privateGet(this, extra);
25
+ }, set [name](x) {
26
+ return __privateSet(this, extra, x);
27
+ } }, name));
28
+ k ? p && k < 4 && __name(extra, (k > 2 ? "set " : k > 1 ? "get " : "") + name) : __name(target, name);
29
+ for (var i = decorators.length - 1; i >= 0; i--) {
30
+ ctx = __decoratorContext(k, name, done = {}, array[3], extraInitializers);
31
+ if (k) {
32
+ ctx.static = s, ctx.private = p, access = ctx.access = { has: p ? (x) => __privateIn(target, x) : (x) => name in x };
33
+ if (k ^ 3) access.get = p ? (x) => (k ^ 1 ? __privateGet : __privateMethod)(x, target, k ^ 4 ? extra : desc.get) : (x) => x[name];
34
+ if (k > 2) access.set = p ? (x, y) => __privateSet(x, target, y, k ^ 4 ? extra : desc.set) : (x, y) => x[name] = y;
35
+ }
36
+ it = (0, decorators[i])(k ? k < 4 ? p ? extra : desc[key] : k > 4 ? void 0 : { get: desc.get, set: desc.set } : target, ctx), done._ = 1;
37
+ if (k ^ 4 || it === void 0) __expectFn(it) && (k > 4 ? initializers.unshift(it) : k ? p ? extra = it : desc[key] = it : target = it);
38
+ else if (typeof it !== "object" || it === null) __typeError("Object expected");
39
+ else __expectFn(fn = it.get) && (desc.get = fn), __expectFn(fn = it.set) && (desc.set = fn), __expectFn(fn = it.init) && initializers.unshift(fn);
40
+ }
41
+ return k || __decoratorMetadata(array, target), desc && __defProp(target, name, desc), p ? k ^ 4 ? extra : desc : target;
42
+ };
43
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
44
+ var __privateIn = (member, obj) => Object(obj) !== obj ? __typeError('Cannot use the "in" operator on this value') : member.has(obj);
45
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
46
+ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
47
+ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
48
+
49
+ // vendor/dsh-protocol/index.ts
50
+ import { Context, Service } from "@deepseek-ai/cordis";
51
+
52
+ // vendor/dsh-protocol/remote-error.ts
53
+ var RemoteError = class extends Error {
54
+ /**
55
+ * @param code - stable failure code declared in {@link RemoteErrorDetailsMap}.
56
+ * @param message - human diagnostic carried across the wire.
57
+ * @param details - structured payload typed by the code.
58
+ * @param options - standard Error options (`cause` survives in-process only).
59
+ */
60
+ constructor(code, message, details, options) {
61
+ super(message, options);
62
+ this.code = code;
63
+ this.details = details;
64
+ this.name = "RemoteError";
65
+ }
66
+ /** Structural marker: cross-realm/bundle identification never uses instanceof. */
67
+ isDSHRemoteError = true;
68
+ };
69
+
70
+ // vendor/dsh-protocol/owned-value.ts
71
+ var TYPERT_OWNED_VALUE = Symbol.for("dsh.typert.owned-value");
72
+
73
+ // vendor/dsh-protocol/index.ts
74
+ var TYPERT_REMOTE_SEGMENT_PATTERN = /^[A-Za-z0-9_$.-]+$/;
75
+ function isTypertRemoteSegment(value) {
76
+ return value !== "." && value !== ".." && TYPERT_REMOTE_SEGMENT_PATTERN.test(value);
77
+ }
78
+ var REMOTE_METHOD_DESCRIPTOR = "@deepseek-ai/dsh-typert-protocol/remote-methods";
79
+ function bindTypertRemote(service, serviceKey, options = {}) {
80
+ validateName("service key", serviceKey);
81
+ const namespace = options.namespace ?? serviceKey;
82
+ validateName("namespace", namespace);
83
+ const ctx = Reflect.get(service, "ctx");
84
+ if (Context.is(ctx)) provideInvocationAccessor(ctx);
85
+ return Object.freeze({ service, serviceKey, namespace });
86
+ }
87
+ var TypertRemoteService = class extends Service {
88
+ /** Visible binding consumed by the Gateway's source-mode discovery. */
89
+ typertRemote;
90
+ /**
91
+ * Register the Service and bind the same key to Typert Gateway.
92
+ * @param ctx - owning Cordis Context.
93
+ * @param serviceKey - exact Cordis service key and default wire namespace.
94
+ * @param options - optional wire namespace.
95
+ */
96
+ constructor(ctx, serviceKey, options = {}) {
97
+ super(ctx, serviceKey);
98
+ this.typertRemote = bindTypertRemote(this, this.name, options);
99
+ }
100
+ };
101
+ function provideInvocationAccessor(ctx) {
102
+ if (Object.hasOwn(ctx.root.reflect.props, "invocation")) return;
103
+ ctx.root.accessor("invocation", { get: () => void 0 });
104
+ }
105
+ function Remote(methodExportOrOptions, context) {
106
+ if (typeof methodExportOrOptions === "string") {
107
+ validateName("Remote export name", methodExportOrOptions);
108
+ return remoteDecorator({ kind: "direct" }, { exportName: methodExportOrOptions });
109
+ }
110
+ if (typeof methodExportOrOptions === "object") {
111
+ return remoteDecorator({ kind: "direct" }, markerOptions(
112
+ methodExportOrOptions,
113
+ REMOTE_OPTION_KEYS,
114
+ 'Remote options require name, mode: "stream", or resources'
115
+ ));
116
+ }
117
+ if (context === void 0) throw new TypeError("typert-protocol: Remote decorator context is missing");
118
+ addMarkerInitializer(context, { kind: "direct" });
119
+ }
120
+ var REMOTE_OPTION_KEYS = /* @__PURE__ */ new Set(["name", "mode", "resources"]);
121
+ function markerOptions(options, accepted, requirement) {
122
+ const keys = Reflect.ownKeys(options);
123
+ if (keys.length === 0 || keys.some((key) => typeof key !== "string" || !accepted.has(key)) || Object.hasOwn(options, "mode") && options.mode !== "stream") {
124
+ throw new TypeError(`typert-protocol: ${requirement}`);
125
+ }
126
+ const { mode, name, resources } = options;
127
+ if (name !== void 0) validateName("Remote export name", name);
128
+ return {
129
+ ...mode === void 0 ? {} : { mode },
130
+ ...name === void 0 ? {} : { exportName: name },
131
+ ...resources === void 0 ? {} : { resources: resourceReaders(resources) }
132
+ };
133
+ }
134
+ function resourceReaders(resources) {
135
+ const readers = {};
136
+ for (const [kind, reader] of Object.entries(resources)) {
137
+ if (typeof reader !== "function") {
138
+ throw new TypeError(`typert-protocol: Remote resource reader "${kind}" must be a function`);
139
+ }
140
+ readers[kind] = reader;
141
+ }
142
+ if (Object.keys(readers).length === 0) throw new TypeError("typert-protocol: Remote resources require at least one reader");
143
+ return Object.freeze(readers);
144
+ }
145
+ function remoteDecorator(invocation, options = {}) {
146
+ return function(_method, context) {
147
+ addMarkerInitializer(context, invocation, options);
148
+ };
149
+ }
150
+ function readRemoteMethodDescriptor(prototype) {
151
+ const property = Object.getOwnPropertyDescriptor(prototype, REMOTE_METHOD_DESCRIPTOR);
152
+ if (property === void 0) return void 0;
153
+ const descriptor = property.value;
154
+ if (descriptor === null || typeof descriptor !== "object") {
155
+ throw new TypeError("typert-protocol: Remote method descriptor must be an object");
156
+ }
157
+ const version = Reflect.get(descriptor, "version");
158
+ if (version !== 1) {
159
+ throw new TypeError(`typert-protocol: unsupported Remote method descriptor version ${String(version)}`);
160
+ }
161
+ const methods = Reflect.get(descriptor, "methods");
162
+ if (!Array.isArray(methods)) {
163
+ throw new TypeError("typert-protocol: Remote method descriptor methods must be an array");
164
+ }
165
+ return descriptor;
166
+ }
167
+ function addMarkerInitializer(context, invocation, options = {}) {
168
+ if (context.private || context.static || typeof context.name !== "string") {
169
+ throw new TypeError("typert-protocol: Remote decorators require a public instance method with a string name");
170
+ }
171
+ const method = context.name;
172
+ context.addInitializer(function() {
173
+ const prototype = Object.getPrototypeOf(this);
174
+ if (prototype === null) {
175
+ throw new TypeError(`typert-protocol: cannot mark Remote method "${method}" on an object without a prototype`);
176
+ }
177
+ mark(prototype, method, invocation, options);
178
+ });
179
+ }
180
+ function mark(prototype, method, invocation, { mode, exportName, resources }) {
181
+ const descriptor = readRemoteMethodDescriptor(prototype);
182
+ const marker = Object.freeze({
183
+ method,
184
+ ...exportName === void 0 || exportName === method ? {} : { exportName },
185
+ ...mode === void 0 ? {} : { mode },
186
+ invocation: Object.freeze(invocation),
187
+ ...resources === void 0 ? {} : { resources }
188
+ });
189
+ const current = descriptor?.methods.find((candidate) => candidate.method === method);
190
+ if (current !== void 0) {
191
+ if (current.exportName === marker.exportName && current.mode === marker.mode && current.resources === marker.resources && sameInvocation(current.invocation, invocation)) return;
192
+ throw new Error(`typert-protocol: Remote method "${method}" has conflicting invocation markers`);
193
+ }
194
+ Object.defineProperty(prototype, REMOTE_METHOD_DESCRIPTOR, {
195
+ configurable: true,
196
+ value: Object.freeze({
197
+ version: 1,
198
+ methods: Object.freeze([...descriptor?.methods ?? [], marker])
199
+ })
200
+ });
201
+ }
202
+ function sameInvocation(left, right) {
203
+ if (left.kind === "direct") return right.kind === "direct";
204
+ if (right.kind === "direct") return false;
205
+ return left.context === right.context;
206
+ }
207
+ function validateName(subject, value) {
208
+ if (!isTypertRemoteSegment(value)) {
209
+ throw new TypeError(`typert-protocol: ${subject} must contain only RPC endpoint segment characters`);
210
+ }
211
+ }
212
+
213
+ // src/controller.ts
214
+ var _forget_dec, _edit_dec, _search_dec, _detail_dec, _snapshot_dec, _catalog_dec, _a, _init;
215
+ var MemoryController = class extends (_a = TypertRemoteService, _catalog_dec = [Remote], _snapshot_dec = [Remote], _detail_dec = [Remote], _search_dec = [Remote], _edit_dec = [Remote], _forget_dec = [Remote], _a) {
216
+ constructor(ctx, bridge) {
217
+ super(ctx, "xujieMemory");
218
+ this.bridge = bridge;
219
+ __runInitializers(_init, 5, this);
220
+ }
221
+ owner() {
222
+ if (this.ctx.invocation?.peer.principal.kind !== "owner") {
223
+ throw new RemoteError("xujie-memory/owner-required", "\u53EA\u6709 DSH \u6240\u6709\u8005\u53EF\u4EE5\u7BA1\u7406\u8BB0\u5FC6\u5E93", { action: "memory/manage" });
224
+ }
225
+ }
226
+ async call(path, body) {
227
+ this.owner();
228
+ try {
229
+ return await this.bridge.request(path, body);
230
+ } catch (error) {
231
+ throw new RemoteError("xujie-memory/rejected", "\u8BB0\u5FC6\u5E93\u64CD\u4F5C\u672A\u5B8C\u6210", {
232
+ reason: error instanceof Error ? error.message : "kernel_unavailable"
233
+ });
234
+ }
235
+ }
236
+ async catalog() {
237
+ const r = await this.call("/manage/catalog", {});
238
+ return { scope_mode: r.scope_mode, scopes: r.scopes };
239
+ }
240
+ async snapshot(scopeId) {
241
+ const r = await this.call("/manage/snapshot", { scope_id: scopeId });
242
+ const cards = r.cards.map((c) => ({
243
+ account_id: c.profile.account_id,
244
+ display_name: c.profile.display_name ?? c.profile.account_id,
245
+ status: c.profile.status,
246
+ tags: c.tags.map((t) => ({
247
+ tag_id: t.tag_id,
248
+ kind: t.kind,
249
+ value: t.value,
250
+ source: t.source,
251
+ confidence: t.confidence
252
+ }))
253
+ }));
254
+ const { scope_id, scope_mode, nodes, edges, truncated, limit } = r;
255
+ const { traces, traces_erased, events, states, anchors, clarifications_open, summaries } = r.stats;
256
+ return {
257
+ scope_id,
258
+ scope_mode,
259
+ nodes,
260
+ edges,
261
+ cards,
262
+ truncated,
263
+ limit,
264
+ stats: { traces, traces_erased, events, states, anchors, clarifications_open, summaries }
265
+ };
266
+ }
267
+ async detail(scopeId, traceId) {
268
+ return (await this.call("/manage/detail", { scope_id: scopeId, trace_id: traceId })).trace;
269
+ }
270
+ async search(scopeId, query) {
271
+ const r = await this.call("/manage/search", { scope_id: scopeId, query });
272
+ return r.hits.map((h) => ({
273
+ trace_id: h.trace_id,
274
+ content: h.content,
275
+ content_hash: h.content_hash,
276
+ score: h.score,
277
+ priority: Number(h.priority ?? 0),
278
+ importance: Number(h.importance ?? 1),
279
+ anchor: Boolean(h.anchor),
280
+ recorded_at: h.recorded_at ?? "",
281
+ layer: h.layer
282
+ }));
283
+ }
284
+ async edit(scopeId, traceId, content, expectedHash) {
285
+ const r = await this.call("/manage/edit", { scope_id: scopeId, trace_id: traceId, content, expected_hash: expectedHash });
286
+ return {
287
+ changed: r.changed,
288
+ trace: r.trace,
289
+ ...r.replaced_trace_id ? { replaced_trace_id: r.replaced_trace_id, pending: r.pending } : {}
290
+ };
291
+ }
292
+ async forget(scopeId, traceId, expectedHash) {
293
+ const r = await this.call("/manage/forget", { scope_id: scopeId, trace_id: traceId, expected_hash: expectedHash });
294
+ return { trace_id: r.trace_id, erased: r.erased, already_erased: r.already_erased, state_keys: r.state_keys };
295
+ }
296
+ };
297
+ _init = __decoratorStart(_a);
298
+ __decorateElement(_init, 1, "catalog", _catalog_dec, MemoryController);
299
+ __decorateElement(_init, 1, "snapshot", _snapshot_dec, MemoryController);
300
+ __decorateElement(_init, 1, "detail", _detail_dec, MemoryController);
301
+ __decorateElement(_init, 1, "search", _search_dec, MemoryController);
302
+ __decorateElement(_init, 1, "edit", _edit_dec, MemoryController);
303
+ __decorateElement(_init, 1, "forget", _forget_dec, MemoryController);
304
+ __decoratorMetadata(_init, MemoryController);
305
+ export {
306
+ MemoryController
307
+ };
package/lib/index.js CHANGED
@@ -21,6 +21,8 @@
21
21
  * 其余类型、以及 source.kind 不是 user 的 user/message → 跳过
22
22
  */
23
23
 
24
+ import { MemoryController } from './controller.js'
25
+ export { MemoryController } from './controller.js'
24
26
  import { spawn } from 'node:child_process'
25
27
  import { appendFileSync, existsSync, mkdirSync, openSync, closeSync, readFileSync, statSync, renameSync } from 'node:fs'
26
28
  import { homedir } from 'node:os'
@@ -34,6 +36,7 @@ const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
34
36
  const BUNDLED_KERNEL = join(PACKAGE_ROOT, 'python')
35
37
  const DEFAULT_ENDPOINT = 'http://127.0.0.1:8765'
36
38
  const TURN_TIMEOUT_MS = 4000
39
+ const TOOL_TIMEOUT_MS = 5000
37
40
  const HEALTH_TIMEOUT_MS = 800
38
41
  const READY_WAIT_MS = 10000
39
42
  const WATCHDOG_MS = 5000
@@ -42,14 +45,13 @@ const MAX_TEXT_CHARS = 2000
42
45
  const MAX_SESSIONS = 32
43
46
  const LOG_ROTATE_BYTES = 2 * 1024 * 1024
44
47
 
45
- /** 每个会话最近一轮的材料。 */
46
- const contexts = new Map()
47
- /** 每个会话正在进行的 /turn(串行队列的尾巴)。 */
48
- const pending = new Map()
49
- let lastContext = ''
50
- let shapeLogged = false
51
-
52
48
  export function apply(ctx, config = {}) {
49
+ // Per-plugin-instance caches: two profiles/databases never share material.
50
+ const contexts = new Map()
51
+ const pending = new Map()
52
+ const latestByUser = new Map()
53
+ const cacheState = { contexts, latestByUser, epoch: 0, disposed: false }
54
+ let shapeLogged = false
53
55
  const configured = new URL((config.endpoint || DEFAULT_ENDPOINT).replace(/\/+$/, ''))
54
56
  const host = configured.hostname || '127.0.0.1'
55
57
  const basePort = Number(configured.port || 8765)
@@ -73,6 +75,33 @@ export function apply(ctx, config = {}) {
73
75
  let endpoint = `http://${host}:${basePort}`
74
76
  let token = readToken(tokenFile)
75
77
 
78
+ // A single queue owns kernel operations and prompt-cache publication. UI edits,
79
+ // model deletions and new turns cannot race to republish stale material.
80
+ let operationTail = Promise.resolve()
81
+ const enqueue = (run) => {
82
+ const job = operationTail.catch(() => {}).then(run)
83
+ operationTail = job.catch(() => {})
84
+ return job
85
+ }
86
+ const clearMemoryCache = () => { cacheState.epoch++; contexts.clear(); latestByUser.clear() }
87
+ ctx.effect(() => () => { cacheState.disposed = true; clearMemoryCache() }, 'xujie-memory: instance caches')
88
+ ctx.inject(['typert'], (remoteCtx) => {
89
+ new MemoryController(remoteCtx, {
90
+ request: (path, body) => enqueue(async () => {
91
+ if (cacheState.disposed) throw new Error('memory_plugin_unloaded')
92
+ if (kernelReady) await kernelReady
93
+ const mutation = path === '/manage/edit' || path === '/manage/forget'
94
+ if (mutation) clearMemoryCache()
95
+ const resp = await fetchJson(`${endpoint}${path}`, body, 15000, token)
96
+ if (!resp || resp.ok !== true) {
97
+ throw new Error(resp?.detail ?? resp?.error ?? 'memory_kernel_unavailable')
98
+ }
99
+ if (mutation) clearMemoryCache()
100
+ return resp
101
+ }),
102
+ })
103
+ })
104
+
76
105
  const log = (level, message) => {
77
106
  const logger = ctx.logger
78
107
  if (logger && typeof logger[level] === 'function') logger[level](`xujie-memory: ${message}`)
@@ -236,7 +265,7 @@ export function apply(ctx, config = {}) {
236
265
  // 没有这个会话的材料(新会话第一句之前 / 宿主内部组装):
237
266
  // user 模式下记忆本来就跨会话共享,用最近一次的材料;
238
267
  // session 模式下绝不串会话,返回空。
239
- return scope === 'user' ? lastContext : ''
268
+ return scope === 'user' && raw ? (latestByUser.get(String(raw.userId ?? raw.account ?? 'user')) ?? '') : ''
240
269
  }
241
270
  service.section({
242
271
  name: 'xujie-memory',
@@ -254,6 +283,159 @@ export function apply(ctx, config = {}) {
254
283
  })
255
284
  })
256
285
 
286
+ // ------------------------------------------------------------ 工具
287
+ // 注入全自动,但"主动回忆"和"记错了删掉"必须给模型一只手:
288
+ // 只会被动接收召回结果 = 只许它看,不许它查、不许它改。
289
+ // 三个都是可解释的低危操作:搜(只读)、忘(软删,可审计)、看账(只读)。
290
+ const toolDisposers = []
291
+ ctx.inject(['tools'], (toolCtx) => {
292
+ // 宿主没给 tools 服务(老版本 / 测试替身)时静默跳过:
293
+ // 工具是增强,不能因为它让整块记忆插件挂不上。
294
+ if (typeof toolCtx?.tools?.register !== 'function') {
295
+ log('warn', '宿主没有 tools 服务,检索/删除工具不会注册(注入照常)')
296
+ return
297
+ }
298
+ const sessionOf = (exec) => exec?.agent?.session ?? exec?.session
299
+ const call = async (path, payload, exec) => {
300
+ const resp = await enqueue(async () => {
301
+ if (path === '/memory/forget' || path === '/memory/edit') clearMemoryCache()
302
+ return await fetchJson(
303
+ `${endpoint}${path}`,
304
+ { session: sessionKey(sessionOf(exec)), user: String(sessionOf(exec)?.userId ?? sessionOf(exec)?.account ?? 'user'), ...payload },
305
+ TOOL_TIMEOUT_MS,
306
+ token,
307
+ )
308
+ })
309
+ if (!resp || resp.ok !== true) {
310
+ throw new Error(`xujie-memory: ${resp?.detail ?? resp?.error ?? 'memory kernel did not respond'}`)
311
+ }
312
+ return resp
313
+ }
314
+ const text = (value) => [{ type: 'text', text: value }]
315
+
316
+ toolDisposers.push(toolCtx.tools.register({
317
+ name: 'memory_search',
318
+ description: '在用户的长期记忆里检索他说过的话(支持短词与错字模糊匹配)。只读。返回原话、trace_id 与权重点。要删除某条记忆前,先用它拿到 trace_id。',
319
+ parameters: {
320
+ type: 'object',
321
+ properties: {
322
+ query: { type: 'string', description: '要检索的内容;两个字也能搜,错一个字也能命中' },
323
+ limit: { type: 'number', description: '最多返回几条,默认 5(1-20)' },
324
+ },
325
+ required: ['query'],
326
+ },
327
+ output: {
328
+ schema: { type: 'object' },
329
+ render: (_args, value) => {
330
+ const hits = value?.hits ?? []
331
+ if (hits.length === 0) return text(`没有搜到与「${value?.query ?? ''}」相关的记忆。`)
332
+ const lines = hits.map((h) => {
333
+ const tags = [h.anchor ? '锚点' : '', h.importance ? `重要度 ${h.importance}` : ''].filter(Boolean)
334
+ return `- ${h.content}(trace_id: ${h.trace_id}${tags.length ? ',' + tags.join(',') : ''})`
335
+ })
336
+ return text(`搜到 ${hits.length} 条:\n${lines.join('\n')}`)
337
+ },
338
+ },
339
+ execute: async (args, exec) => {
340
+ const query = String(args?.query ?? '').trim().slice(0, MAX_TEXT_CHARS)
341
+ if (!query) throw new Error('memory_search: query 不能为空')
342
+ const limit = Math.max(1, Math.min(20, Math.round(Number(args?.limit) || 5)))
343
+ const resp = await call('/memory/search', { query, limit }, exec)
344
+ return { ok: true, query: resp.query ?? query, hits: resp.hits ?? [] }
345
+ },
346
+ }))
347
+
348
+ toolDisposers.push(toolCtx.tools.register({
349
+ name: 'memory_forget',
350
+ description: '删掉一条记忆(软删:内容清空、FTS 移除、由它推出的事实一起失效并重算;库里保留 erased_at 供审计,不可恢复)。trace_id 用 memory_search 取。用户说「忘掉这条」时用。',
351
+ parameters: {
352
+ type: 'object',
353
+ properties: {
354
+ trace_id: { type: 'string', description: '要删的记忆条目 id(memory_search 返回的 trace_id)' },
355
+ },
356
+ required: ['trace_id'],
357
+ },
358
+ output: {
359
+ schema: { type: 'object' },
360
+ render: (_args, value) => {
361
+ if (!value?.erased) return text('删除失败:内核没有确认。')
362
+ const tail = value.already_erased ? '(这条之前已经删过)' : ''
363
+ return text(`已删除 ${value.trace_id}${tail},连带失效 ${value.state_keys?.length ?? 0} 组状态。`)
364
+ },
365
+ },
366
+ execute: async (args, exec) => {
367
+ const traceId = String(args?.trace_id ?? '').trim()
368
+ if (!traceId) throw new Error('memory_forget: trace_id 不能为空')
369
+ const result = await call('/memory/forget', { trace_id: traceId }, exec)
370
+ contexts.clear()
371
+ latestByUser.clear()
372
+ return result
373
+ },
374
+ }))
375
+
376
+ toolDisposers.push(toolCtx.tools.register({
377
+ name: 'memory_edit',
378
+ description: '仅在用户明确要求纠正记忆时,替换一条原话并重算事实与画像。先用 memory_search 取得 trace_id 和 content_hash;保存成功后返回新的 trace_id。不能用来偷偷改写用户历史。',
379
+ parameters: {
380
+ type: 'object',
381
+ properties: {
382
+ trace_id: { type: 'string', description: '要纠正的原话 id' },
383
+ content: { type: 'string', description: '用户明确确认的新内容,最多 2000 字' },
384
+ expected_hash: { type: 'string', description: 'memory_search 返回的 content_hash,防止覆盖其他修改' },
385
+ },
386
+ required: ['trace_id', 'content', 'expected_hash'],
387
+ },
388
+ output: {
389
+ schema: { type: 'object' },
390
+ render: (_args, value) => text(value?.ok
391
+ ? `已纠正记忆,新 trace_id: ${value.trace?.trace_id}。`
392
+ : '纠正未完成,请重新检索后再试。'),
393
+ },
394
+ execute: async (args, exec) => {
395
+ const trace_id = String(args?.trace_id ?? '').trim()
396
+ const content = String(args?.content ?? '').trim()
397
+ const expected_hash = String(args?.expected_hash ?? '').trim()
398
+ if (!trace_id || !content || content.length > MAX_TEXT_CHARS || !expected_hash) {
399
+ throw new Error('memory_edit: 缺少记录、有效内容或版本 hash')
400
+ }
401
+ const result = await call('/memory/edit', { trace_id, content, expected_hash }, exec)
402
+ clearMemoryCache()
403
+ return result
404
+ },
405
+ }))
406
+
407
+ toolDisposers.push(toolCtx.tools.register({
408
+ name: 'memory_status',
409
+ description: '看记忆库现状:原话条数、已删条数、锚点数、未确认项,以及内核健康信息。用户问「你记得我什么」「记忆正常吗」时用。只读。',
410
+ parameters: { type: 'object', properties: {} },
411
+ output: {
412
+ schema: { type: 'object' },
413
+ render: (_args, value) => {
414
+ const kernel = value?.kernel ?? {}
415
+ const head = `记忆库:原话 ${value?.traces ?? 0} 条(已删 ${value?.traces_erased ?? 0} 条),`
416
+ + `状态 ${value?.states ?? 0} 条,锚点 ${value?.anchors ?? 0} 个,`
417
+ + `未确认 ${value?.clarifications_open ?? 0} 个。`
418
+ const tail = kernel.ok
419
+ ? `内核:pid ${kernel.pid},版本 ${kernel.version},scope=${kernel.scope_mode},库 ${kernel.db}`
420
+ : '内核:当前连不上(注入会降级为「这一轮没有记忆」)。'
421
+ return text(`${head}\n${tail}`)
422
+ },
423
+ },
424
+ execute: async (_args, exec) => {
425
+ const [stats, kernel] = await Promise.all([
426
+ call('/memory/stats', {}, exec),
427
+ health(endpoint, token),
428
+ ])
429
+ return { ...stats, kernel }
430
+ },
431
+ }))
432
+ })
433
+ ctx.effect(() => () => {
434
+ for (const dispose of toolDisposers.splice(0)) {
435
+ try { dispose?.() } catch { /* 已释放 */ }
436
+ }
437
+ }, 'xujie-memory: tools')
438
+
257
439
  // ------------------------------------------------------------ 写记忆
258
440
  ctx.on('session/event', (session, event) => {
259
441
  const type = event?.type
@@ -281,10 +463,7 @@ export function apply(ctx, config = {}) {
281
463
 
282
464
  const key = sessionKey(session)
283
465
  // 同一会话串行:上一句没写完,下一句排在后面,保证顺序。
284
- const prev = pending.get(key) || Promise.resolve()
285
- const job = prev
286
- .catch(() => {})
287
- .then(() => remember(() => endpoint, () => token, session, text, maxAskedTotal, log))
466
+ const job = enqueue(() => remember(() => endpoint, () => token, session, text, maxAskedTotal, log, cacheState))
288
467
  .finally(() => { if (pending.get(key) === job) pending.delete(key) })
289
468
  pending.set(key, job)
290
469
  })
@@ -412,7 +591,10 @@ function sessionKey(session) {
412
591
 
413
592
  // ---------------------------------------------------------------- 请求
414
593
 
415
- async function remember(getEndpoint, getToken, session, text, maxAskedTotal, log) {
594
+ async function remember(getEndpoint, getToken, session, text, maxAskedTotal, log, cache) {
595
+ if (cache.disposed) return
596
+ const { contexts, latestByUser } = cache
597
+ const epoch = cache.epoch
416
598
  const key = sessionKey(session)
417
599
  const body = {
418
600
  session: key,
@@ -430,6 +612,7 @@ async function remember(getEndpoint, getToken, session, text, maxAskedTotal, log
430
612
  try {
431
613
  const resp = await fetchJson(`${getEndpoint()}/turn`, body, TURN_TIMEOUT_MS, getToken())
432
614
  if (resp?.ok && typeof resp.context === 'string') {
615
+ if (cache.disposed || cache.epoch !== epoch) return
433
616
  contexts.delete(key)
434
617
  contexts.set(key, resp.context) // 重新插入 = 移到最新
435
618
  while (contexts.size > MAX_SESSIONS) {
@@ -437,7 +620,10 @@ async function remember(getEndpoint, getToken, session, text, maxAskedTotal, log
437
620
  if (oldest === undefined) break
438
621
  contexts.delete(oldest)
439
622
  }
440
- lastContext = resp.context
623
+ const user = String(session?.userId ?? session?.account ?? 'user')
624
+ latestByUser.delete(user)
625
+ latestByUser.set(user, resp.context)
626
+ while (latestByUser.size > MAX_SESSIONS) latestByUser.delete(latestByUser.keys().next().value)
441
627
  log('debug',
442
628
  `记下 ${resp.outcomes?.length ?? 0} 条判定,材料 ${resp.context.length} 字,会话 ${key}`)
443
629
  return
@@ -0,0 +1,3 @@
1
+ /* Generated by @deepseek-ai/dsh-typert-generator from FaceModel — do not edit. */
2
+
3
+ export declare const TYPERT: unknown