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.
- package/ACCEPTANCE.md +37 -0
- package/BUILD-PROVENANCE.json +53 -0
- package/CHANGELOG.md +40 -1
- package/LICENSE +21 -21
- package/README.md +67 -17
- package/README_EN.md +54 -8
- package/cordis.patch.yml +49 -49
- package/lib/client.js +17219 -0
- package/lib/controller.js +307 -0
- package/lib/index.js +200 -14
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +15104 -0
- package/lib/typert.remote-client.d.ts +40 -0
- package/lib/typert.remote-client.d.ts.map +1 -0
- package/lib/typert.remote-client.js +15093 -0
- package/lib/types.d.ts +13 -0
- package/lib/types.js +1 -0
- package/package.json +49 -5
- package/python/specs/characters/aloof.json +14 -14
- package/python/specs/characters/gentle.json +14 -14
- package/python/specs/characters/tsundere.json +14 -14
- package/python/specs/debt.v1.json +31 -31
- package/python/specs/nickname.v1.json +16 -16
- package/python/specs/preference.v1.json +16 -16
- package/python/xujie_memory/__init__.py +123 -123
- package/python/xujie_memory/admission.py +211 -211
- package/python/xujie_memory/audit.py +377 -377
- package/python/xujie_memory/benchmark.py +281 -281
- package/python/xujie_memory/character.py +199 -199
- package/python/xujie_memory/console.py +41 -41
- package/python/xujie_memory/consolidate.py +356 -356
- package/python/xujie_memory/due.py +121 -121
- package/python/xujie_memory/emotion.py +573 -573
- package/python/xujie_memory/engine.py +1333 -1312
- package/python/xujie_memory/errors.py +98 -98
- package/python/xujie_memory/importance.py +104 -104
- package/python/xujie_memory/judge.py +350 -350
- package/python/xujie_memory/management.py +203 -0
- package/python/xujie_memory/memory.py +71 -10
- package/python/xujie_memory/orchestrator.py +690 -666
- package/python/xujie_memory/phrasing.py +292 -292
- package/python/xujie_memory/privacy.py +105 -0
- package/python/xujie_memory/schema.sql +492 -480
- package/python/xujie_memory/server.py +76 -0
- package/python/xujie_memory/specs.py +170 -170
- package/python/xujie_memory/summary.py +238 -238
- package/scripts/build.mjs +19 -0
- package/scripts/prepack.mjs +9 -0
- package/scripts/test-all.mjs +9 -0
- package/scripts/test-node.mjs +10 -0
- package/src/client/MemoryPanel.tsx +536 -0
- package/src/client/NeuralField.tsx +88 -0
- package/src/client/css.d.ts +4 -0
- package/src/client/icons.tsx +28 -0
- package/src/client/index.tsx +59 -0
- package/src/client/memory-adapter.ts +67 -0
- package/src/client/model.ts +79 -0
- package/src/client/neural/engine.ts +999 -0
- package/src/client/neural/morphology.ts +219 -0
- package/src/client/panel.css +337 -0
- package/src/client/types.ts +6 -0
- package/src/controller.ts +88 -0
- package/src/generated/typert.host.js +449 -0
- package/src/generated/typert.remote-client.js +438 -0
- package/src/index.ts +2 -0
- package/src/types.ts +13 -0
- package/test/cordis-fixture.mjs +5 -0
- package/test/management_regression.py +187 -0
- package/test/node-regression.entry.mjs +52 -0
- package/vendor/DSH-LICENSE.txt +21 -0
- package/vendor/THIRD-PARTY.md +6 -0
- package/vendor/dsh-protocol/index.ts +484 -0
- package/vendor/dsh-protocol/json-value.ts +53 -0
- package/vendor/dsh-protocol/owned-value.ts +39 -0
- package/vendor/dsh-protocol/principal.ts +17 -0
- package/vendor/dsh-protocol/remote-access.ts +38 -0
- package/vendor/dsh-protocol/remote-error.ts +49 -0
- package/vendor/dsh-protocol/remote-info.ts +161 -0
- package/vendor/dsh-protocol/types.ts +939 -0
- package/vendor/licenses/jridgewell-gen-mapping-MIT.txt +19 -0
- package/vendor/licenses/jridgewell-resolve-uri-MIT.txt +19 -0
- package/vendor/licenses/jridgewell-sourcemap-codec-MIT.txt +19 -0
- package/vendor/licenses/jridgewell-trace-mapping-MIT.txt +19 -0
- 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' ?
|
|
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
|
|
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
|
-
|
|
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
|