dsh-plugin-xujie-memory 0.6.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 (61) hide show
  1. package/ACCEPTANCE.md +37 -0
  2. package/BUILD-PROVENANCE.json +53 -0
  3. package/CHANGELOG.md +12 -0
  4. package/README.md +48 -23
  5. package/README_EN.md +32 -9
  6. package/lib/client.js +17219 -0
  7. package/lib/controller.js +307 -0
  8. package/lib/index.js +87 -22
  9. package/lib/typert.host.d.ts +3 -0
  10. package/lib/typert.host.js +15104 -0
  11. package/lib/typert.remote-client.d.ts +40 -0
  12. package/lib/typert.remote-client.d.ts.map +1 -0
  13. package/lib/typert.remote-client.js +15093 -0
  14. package/lib/types.d.ts +13 -0
  15. package/lib/types.js +1 -0
  16. package/package.json +49 -5
  17. package/python/xujie_memory/__init__.py +123 -123
  18. package/python/xujie_memory/engine.py +1333 -1312
  19. package/python/xujie_memory/management.py +203 -0
  20. package/python/xujie_memory/memory.py +27 -21
  21. package/python/xujie_memory/privacy.py +105 -0
  22. package/python/xujie_memory/schema.sql +492 -480
  23. package/python/xujie_memory/server.py +52 -0
  24. package/scripts/build.mjs +19 -0
  25. package/scripts/prepack.mjs +9 -0
  26. package/scripts/test-all.mjs +9 -0
  27. package/scripts/test-node.mjs +10 -0
  28. package/src/client/MemoryPanel.tsx +536 -0
  29. package/src/client/NeuralField.tsx +88 -0
  30. package/src/client/css.d.ts +4 -0
  31. package/src/client/icons.tsx +28 -0
  32. package/src/client/index.tsx +59 -0
  33. package/src/client/memory-adapter.ts +67 -0
  34. package/src/client/model.ts +79 -0
  35. package/src/client/neural/engine.ts +999 -0
  36. package/src/client/neural/morphology.ts +219 -0
  37. package/src/client/panel.css +337 -0
  38. package/src/client/types.ts +6 -0
  39. package/src/controller.ts +88 -0
  40. package/src/generated/typert.host.js +449 -0
  41. package/src/generated/typert.remote-client.js +438 -0
  42. package/src/index.ts +2 -0
  43. package/src/types.ts +13 -0
  44. package/test/cordis-fixture.mjs +5 -0
  45. package/test/management_regression.py +187 -0
  46. package/test/node-regression.entry.mjs +52 -0
  47. package/vendor/DSH-LICENSE.txt +21 -0
  48. package/vendor/THIRD-PARTY.md +6 -0
  49. package/vendor/dsh-protocol/index.ts +484 -0
  50. package/vendor/dsh-protocol/json-value.ts +53 -0
  51. package/vendor/dsh-protocol/owned-value.ts +39 -0
  52. package/vendor/dsh-protocol/principal.ts +17 -0
  53. package/vendor/dsh-protocol/remote-access.ts +38 -0
  54. package/vendor/dsh-protocol/remote-error.ts +49 -0
  55. package/vendor/dsh-protocol/remote-info.ts +161 -0
  56. package/vendor/dsh-protocol/types.ts +939 -0
  57. package/vendor/licenses/jridgewell-gen-mapping-MIT.txt +19 -0
  58. package/vendor/licenses/jridgewell-resolve-uri-MIT.txt +19 -0
  59. package/vendor/licenses/jridgewell-sourcemap-codec-MIT.txt +19 -0
  60. package/vendor/licenses/jridgewell-trace-mapping-MIT.txt +19 -0
  61. 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'
@@ -43,14 +45,13 @@ const MAX_TEXT_CHARS = 2000
43
45
  const MAX_SESSIONS = 32
44
46
  const LOG_ROTATE_BYTES = 2 * 1024 * 1024
45
47
 
46
- /** 每个会话最近一轮的材料。 */
47
- const contexts = new Map()
48
- /** 每个会话正在进行的 /turn(串行队列的尾巴)。 */
49
- const pending = new Map()
50
- let lastContext = ''
51
- let shapeLogged = false
52
-
53
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
54
55
  const configured = new URL((config.endpoint || DEFAULT_ENDPOINT).replace(/\/+$/, ''))
55
56
  const host = configured.hostname || '127.0.0.1'
56
57
  const basePort = Number(configured.port || 8765)
@@ -74,6 +75,33 @@ export function apply(ctx, config = {}) {
74
75
  let endpoint = `http://${host}:${basePort}`
75
76
  let token = readToken(tokenFile)
76
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
+
77
105
  const log = (level, message) => {
78
106
  const logger = ctx.logger
79
107
  if (logger && typeof logger[level] === 'function') logger[level](`xujie-memory: ${message}`)
@@ -237,7 +265,7 @@ export function apply(ctx, config = {}) {
237
265
  // 没有这个会话的材料(新会话第一句之前 / 宿主内部组装):
238
266
  // user 模式下记忆本来就跨会话共享,用最近一次的材料;
239
267
  // session 模式下绝不串会话,返回空。
240
- return scope === 'user' ? lastContext : ''
268
+ return scope === 'user' && raw ? (latestByUser.get(String(raw.userId ?? raw.account ?? 'user')) ?? '') : ''
241
269
  }
242
270
  service.section({
243
271
  name: 'xujie-memory',
@@ -269,12 +297,15 @@ export function apply(ctx, config = {}) {
269
297
  }
270
298
  const sessionOf = (exec) => exec?.agent?.session ?? exec?.session
271
299
  const call = async (path, payload, exec) => {
272
- const resp = await fetchJson(
273
- `${endpoint}${path}`,
274
- { session: sessionKey(sessionOf(exec)), user: String(sessionOf(exec)?.userId ?? sessionOf(exec)?.account ?? 'user'), ...payload },
275
- TOOL_TIMEOUT_MS,
276
- token,
277
- )
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
+ })
278
309
  if (!resp || resp.ok !== true) {
279
310
  throw new Error(`xujie-memory: ${resp?.detail ?? resp?.error ?? 'memory kernel did not respond'}`)
280
311
  }
@@ -335,10 +366,40 @@ export function apply(ctx, config = {}) {
335
366
  execute: async (args, exec) => {
336
367
  const traceId = String(args?.trace_id ?? '').trim()
337
368
  if (!traceId) throw new Error('memory_forget: trace_id 不能为空')
338
- await Promise.allSettled([...pending.values()])
339
369
  const result = await call('/memory/forget', { trace_id: traceId }, exec)
340
370
  contexts.clear()
341
- lastContext = ''
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()
342
403
  return result
343
404
  },
344
405
  }))
@@ -402,10 +463,7 @@ export function apply(ctx, config = {}) {
402
463
 
403
464
  const key = sessionKey(session)
404
465
  // 同一会话串行:上一句没写完,下一句排在后面,保证顺序。
405
- const prev = pending.get(key) || Promise.resolve()
406
- const job = prev
407
- .catch(() => {})
408
- .then(() => remember(() => endpoint, () => token, session, text, maxAskedTotal, log))
466
+ const job = enqueue(() => remember(() => endpoint, () => token, session, text, maxAskedTotal, log, cacheState))
409
467
  .finally(() => { if (pending.get(key) === job) pending.delete(key) })
410
468
  pending.set(key, job)
411
469
  })
@@ -533,7 +591,10 @@ function sessionKey(session) {
533
591
 
534
592
  // ---------------------------------------------------------------- 请求
535
593
 
536
- 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
537
598
  const key = sessionKey(session)
538
599
  const body = {
539
600
  session: key,
@@ -551,6 +612,7 @@ async function remember(getEndpoint, getToken, session, text, maxAskedTotal, log
551
612
  try {
552
613
  const resp = await fetchJson(`${getEndpoint()}/turn`, body, TURN_TIMEOUT_MS, getToken())
553
614
  if (resp?.ok && typeof resp.context === 'string') {
615
+ if (cache.disposed || cache.epoch !== epoch) return
554
616
  contexts.delete(key)
555
617
  contexts.set(key, resp.context) // 重新插入 = 移到最新
556
618
  while (contexts.size > MAX_SESSIONS) {
@@ -558,7 +620,10 @@ async function remember(getEndpoint, getToken, session, text, maxAskedTotal, log
558
620
  if (oldest === undefined) break
559
621
  contexts.delete(oldest)
560
622
  }
561
- 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)
562
627
  log('debug',
563
628
  `记下 ${resp.outcomes?.length ?? 0} 条判定,材料 ${resp.context.length} 字,会话 ${key}`)
564
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