oh-memos-mcp 3.1.8 → 3.1.9
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/CHANGELOG.md +18 -3
- package/README.md +31 -4
- package/dist/api-client.d.ts +13 -0
- package/dist/api-client.js +295 -12
- package/dist/cube-manager.js +6 -6
- package/dist/handlers/admin.js +8 -8
- package/dist/handlers/calendar.js +4 -4
- package/dist/handlers/graph.js +6 -6
- package/dist/handlers/index.d.ts +1 -0
- package/dist/handlers/index.js +10 -3
- package/dist/handlers/memory.js +18 -10
- package/dist/handlers/search.js +6 -6
- package/dist/handlers/think.js +3 -3
- package/dist/handlers/utils.js +3 -2
- package/dist/handlers/wiki-export.js +3 -3
- package/dist/handlers/wiki-import-relations.js +3 -3
- package/dist/handlers/wiki-import.js +10 -4
- package/dist/memory-write-response.d.ts +8 -1
- package/dist/memory-write-response.js +14 -1
- package/dist/models.js +3 -0
- package/dist/server.js +11 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,9 +4,24 @@ All notable changes to `oh-memos-mcp` are documented here.
|
|
|
4
4
|
|
|
5
5
|
This project follows [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## [3.1.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## [3.1.9] - 2026-09-16
|
|
10
|
+
|
|
11
|
+
配套 API / Docker 镜像版本为 3.1.9。保存先确认原文与向量已持久化,再返回记忆 ID;
|
|
12
|
+
LLM 解析由 API 在后台自动完成,即使模型调用耗时数分钟,也不阻塞 MCP 保存回执。
|
|
13
|
+
|
|
14
|
+
- 识别 `vector_saved` 与 `enrichment_status`,明确显示向量已保存和后台解析状态,兼容旧 API。
|
|
15
|
+
- 未确认向量保存或缺少实际 ID 时返回 MCP `isError: true`,且不写入去重缓存。
|
|
16
|
+
- 请求和响应正文超时归类为 `API_TIMEOUT`,不再误报 API 离线;结果未知的 POST 不自动重放。
|
|
17
|
+
- 显式传递 `memory_type`;Wiki 导入同样拒绝未确认的向量写入。
|
|
18
|
+
- 完整详情使用原始创建时间,避免将后台解析的更新时间误标为创建时间。
|
|
19
|
+
|
|
20
|
+
验证:582 项 Node 测试、旧版及 v2 MCP 协议检查通过。配套 API 的 118 项 Python 回归通过。
|
|
21
|
+
真实 MCP 首次保存 596ms 返回,LLM 约 225 秒后补齐同一 ID 的元数据;重复保存 362ms,
|
|
22
|
+
原文、ID 和已有解析结果均保留。
|
|
23
|
+
|
|
24
|
+
## [3.1.8] - 2026-08-27
|
|
10
25
|
|
|
11
26
|
仅 MCP server(npm `oh-memos-mcp`)。Python 包与容器镜像无改动,仍为 3.1.5。
|
|
12
27
|
|
package/README.md
CHANGED
|
@@ -240,9 +240,36 @@ be deleted without a prompt; add it only if you have decided you want that.
|
|
|
240
240
|
| `MEMOS_PROVIDER` | No | `local` when `MEMOS_MODE=lite`, else `api` | Storage backend. Set explicitly only to override the mode-derived default |
|
|
241
241
|
| `MEMOS_LOG_LEVEL` | No | `info` | `debug` / `info` / `warning` / `error`. Logs go to stderr so they never corrupt the stdio JSON-RPC stream |
|
|
242
242
|
|
|
243
|
-
`MEMOS_URL` is required for Full mode and unused in Lite.
|
|
244
|
-
|
|
245
|
-
###
|
|
243
|
+
`MEMOS_URL` is required for Full mode and unused in Lite.
|
|
244
|
+
|
|
245
|
+
### Save acknowledgement and background parsing
|
|
246
|
+
|
|
247
|
+
In Full mode with the `tree_text` backend, `memos_save` returns after the
|
|
248
|
+
original text and its vector have been confirmed in storage. LLM extraction
|
|
249
|
+
continues in the background, even when a model call takes several minutes.
|
|
250
|
+
The response contains the stored ID; the API adds `vector_saved: true`,
|
|
251
|
+
`enrichment_status: "pending"`, and `queued: false`. Here `queued` describes
|
|
252
|
+
the memory write, which has already completed.
|
|
253
|
+
|
|
254
|
+
Parsing adds the key, tags, background, and extracted facts to that same record.
|
|
255
|
+
It preserves the original text, vector, ID, creation time, and explicit source
|
|
256
|
+
metadata. Graph conflict handling links these originals instead of merging or
|
|
257
|
+
deleting them. Identical retries target the same ID.
|
|
258
|
+
|
|
259
|
+
Two background consumers process persisted tasks. A failed parse leaves the
|
|
260
|
+
saved memory available and retries after 30 and 60 seconds; three completed
|
|
261
|
+
failed attempts stop automatic retries. Interrupted attempts can resume after
|
|
262
|
+
restart, including local projects restored from the cube registry. Parse status
|
|
263
|
+
and the exception type are stored as `enrichment_*` metadata.
|
|
264
|
+
|
|
265
|
+
This API path does not depend on `MOS_TYPED_SAVE_FAST` or the memory scheduler.
|
|
266
|
+
Existing library, chat/document ingestion, and Lite behavior remain available.
|
|
267
|
+
Upgrade/restart the API and reconnect MCP clients after rebuilding the Node
|
|
268
|
+
server. Older APIs without the additive fields remain compatible. An explicit
|
|
269
|
+
unconfirmed vector result is rejected; response timeouts report `API_TIMEOUT`
|
|
270
|
+
and do not automatically replay writes.
|
|
271
|
+
|
|
272
|
+
### Retrieval behaviour (3.1.x)
|
|
246
273
|
|
|
247
274
|
These change what search returns. All are off or conservative by default, so
|
|
248
275
|
upgrading never silently changes ranking.
|
|
@@ -250,7 +277,7 @@ upgrading never silently changes ranking.
|
|
|
250
277
|
| Variable | Required | Default | Description |
|
|
251
278
|
|----------|----------|---------|-------------|
|
|
252
279
|
| `MEMOS_SPREAD_ACTIVATION` | No | `false` | One-hop graph spreading activation. When on, a search also returns memories reachable from its direct hits over `CAUSE` / `CONDITION` / `RELATE` edges, annotated ` · via CAUSE from <first 8 chars of the source id>`. Full mode only — it queries Neo4j directly, so `NEO4J_HTTP_URL`, `NEO4J_USER`, and `NEO4J_PASSWORD` must all resolve |
|
|
253
|
-
| `MEMOS_SHOW_WORKING_MEMORY` | No | `false` | Show the scheduler's `WorkingMemory` tier.
|
|
280
|
+
| `MEMOS_SHOW_WORKING_MEMORY` | No | `false` | Show the scheduler's `WorkingMemory` tier. Chat/document and legacy ingestion can create short-term copies alongside long-term memories; hiding this tier avoids displaying those pairs. Vector-first content saves create one original record. Debugging only |
|
|
254
281
|
| `MEMOS_AUTO_CAPTURE` | No | `false` | Accept auto-captured memories. Auto-captured records are also ranked below explicit saves |
|
|
255
282
|
|
|
256
283
|
> **Spreading activation degrades silently without Neo4j credentials.** Retrieval
|
package/dist/api-client.d.ts
CHANGED
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
*
|
|
4
4
|
* HTTP fetch wrapper with timeout, retry, and health check.
|
|
5
5
|
*/
|
|
6
|
+
/** A response deadline does not prove the API is offline or a write failed. */
|
|
7
|
+
export declare function isApiTimeoutError(error: unknown): boolean;
|
|
8
|
+
/** Return true when an exception means the HTTP peer could not be reached. */
|
|
9
|
+
export declare function isApiUnreachableError(error: unknown): boolean;
|
|
10
|
+
/** Return true only when no request body could have reached the peer. */
|
|
11
|
+
export declare function isSafeApiAliasFallbackError(error: unknown): boolean;
|
|
12
|
+
/** Add the IPv4 loopback alias for clients whose resolver prefers IPv6. */
|
|
13
|
+
export declare function apiUrlCandidates(url: string): string[];
|
|
14
|
+
/** Join an API path to the configured base without losing its prefix/query. */
|
|
15
|
+
export declare function apiUrl(path?: string, base?: string): string;
|
|
16
|
+
export declare function apiHealthUrl(base?: string, detail?: boolean): string;
|
|
17
|
+
/** Remove credentials and query values before an endpoint is shown to a client. */
|
|
18
|
+
export declare function apiUrlForDisplay(url?: string): string;
|
|
6
19
|
export declare function fetchWithTimeout(url: string, options?: RequestInit & {
|
|
7
20
|
timeoutMs?: number;
|
|
8
21
|
}): Promise<Response>;
|
package/dist/api-client.js
CHANGED
|
@@ -3,25 +3,296 @@
|
|
|
3
3
|
*
|
|
4
4
|
* HTTP fetch wrapper with timeout, retry, and health check.
|
|
5
5
|
*/
|
|
6
|
+
import { execFileSync } from "node:child_process";
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { release as osRelease } from "node:os";
|
|
6
9
|
import { MEMOS_URL, MEMOS_API_WAIT_MAX, MEMOS_TIMEOUT_HEALTH, MEMOS_TIMEOUT_TOOL, logger, registeredCubes, } from "./config.js";
|
|
7
10
|
// ============================================================================
|
|
8
11
|
// Fetch with Timeout
|
|
9
12
|
// ============================================================================
|
|
13
|
+
const API_NETWORK_ERROR_CODES = [
|
|
14
|
+
"ECONNREFUSED",
|
|
15
|
+
"ECONNABORTED",
|
|
16
|
+
"ECONNRESET",
|
|
17
|
+
"ETIMEDOUT",
|
|
18
|
+
"ENETDOWN",
|
|
19
|
+
"ENETUNREACH",
|
|
20
|
+
"EHOSTDOWN",
|
|
21
|
+
"EHOSTUNREACH",
|
|
22
|
+
"ENOTFOUND",
|
|
23
|
+
"EAI_AGAIN",
|
|
24
|
+
"EADDRNOTAVAIL",
|
|
25
|
+
"UND_ERR_SOCKET",
|
|
26
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
27
|
+
];
|
|
28
|
+
// These failures happen before an HTTP request can be delivered. They are the
|
|
29
|
+
// only network errors safe to replay for a non-idempotent method.
|
|
30
|
+
const API_ALIAS_SAFE_ERROR_CODES = [
|
|
31
|
+
"ECONNREFUSED",
|
|
32
|
+
"ENETUNREACH",
|
|
33
|
+
"EHOSTUNREACH",
|
|
34
|
+
"ENOTFOUND",
|
|
35
|
+
"EAI_AGAIN",
|
|
36
|
+
"EADDRNOTAVAIL",
|
|
37
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
38
|
+
];
|
|
39
|
+
let discoveredWslHosts;
|
|
40
|
+
function isWslRuntime() {
|
|
41
|
+
if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP)
|
|
42
|
+
return true;
|
|
43
|
+
if (process.platform !== "linux")
|
|
44
|
+
return false;
|
|
45
|
+
const kernel = osRelease().toLowerCase();
|
|
46
|
+
return kernel.includes("microsoft") || kernel.includes("wsl");
|
|
47
|
+
}
|
|
48
|
+
function normalizeDiscoveredHost(value) {
|
|
49
|
+
const host = value.trim().replace(/^\[|\]$/g, "").replace(/%[^/]+$/, "");
|
|
50
|
+
if (!host ||
|
|
51
|
+
host === "localhost" ||
|
|
52
|
+
host === "::1" ||
|
|
53
|
+
host === "0.0.0.0" ||
|
|
54
|
+
/^127(?:\.[0-9]+){3}$/.test(host)) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
return host.includes(":") ? `[${host}]` : host;
|
|
58
|
+
}
|
|
59
|
+
function discoverWslHosts() {
|
|
60
|
+
if (discoveredWslHosts)
|
|
61
|
+
return discoveredWslHosts;
|
|
62
|
+
const hosts = new Set(["host.docker.internal"]);
|
|
63
|
+
try {
|
|
64
|
+
const resolvConf = readFileSync("/etc/resolv.conf", "utf8");
|
|
65
|
+
for (const line of resolvConf.split(/\r?\n/)) {
|
|
66
|
+
const match = /^\s*nameserver\s+(\S+)/.exec(line);
|
|
67
|
+
const host = match ? normalizeDiscoveredHost(match[1]) : undefined;
|
|
68
|
+
if (host)
|
|
69
|
+
hosts.add(host);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// WSL integrations may not expose resolv.conf; keep the other candidates.
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const routeOutput = String(execFileSync("ip", ["route", "show", "default"], {
|
|
77
|
+
encoding: "utf8",
|
|
78
|
+
timeout: 500,
|
|
79
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
80
|
+
}));
|
|
81
|
+
const match = /(?:^|\s)default\s+via\s+(\S+)/.exec(routeOutput);
|
|
82
|
+
const host = match ? normalizeDiscoveredHost(match[1]) : undefined;
|
|
83
|
+
if (host)
|
|
84
|
+
hosts.add(host);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// The ip utility is optional; resolv.conf and host.docker.internal suffice.
|
|
88
|
+
}
|
|
89
|
+
discoveredWslHosts = [...hosts];
|
|
90
|
+
return discoveredWslHosts;
|
|
91
|
+
}
|
|
92
|
+
function replaceUrlHost(url, host) {
|
|
93
|
+
try {
|
|
94
|
+
const parsed = new URL(url);
|
|
95
|
+
parsed.hostname = host;
|
|
96
|
+
return parsed.toString();
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** A response deadline does not prove the API is offline or a write failed. */
|
|
103
|
+
export function isApiTimeoutError(error) {
|
|
104
|
+
if (typeof error !== "object" || error === null)
|
|
105
|
+
return false;
|
|
106
|
+
const details = error;
|
|
107
|
+
if (details.name === "AbortError" || details.name === "TimeoutError")
|
|
108
|
+
return true;
|
|
109
|
+
if (details.code === "UND_ERR_HEADERS_TIMEOUT" || details.code === "UND_ERR_BODY_TIMEOUT")
|
|
110
|
+
return true;
|
|
111
|
+
return details.cause !== undefined && details.cause !== error && isApiTimeoutError(details.cause);
|
|
112
|
+
}
|
|
113
|
+
/** Return true when an exception means the HTTP peer could not be reached. */
|
|
114
|
+
export function isApiUnreachableError(error) {
|
|
115
|
+
if (isApiTimeoutError(error))
|
|
116
|
+
return false;
|
|
117
|
+
if (typeof error === "object" && error !== null) {
|
|
118
|
+
const details = error;
|
|
119
|
+
if (typeof details.code === "string" &&
|
|
120
|
+
API_NETWORK_ERROR_CODES.includes(details.code)) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
if (details.cause !== undefined && isApiUnreachableError(details.cause)) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const message = String(error).toLowerCase();
|
|
128
|
+
return (message.includes("fetch failed") ||
|
|
129
|
+
message.includes("network error") ||
|
|
130
|
+
message.includes("socket hang up") ||
|
|
131
|
+
message.includes("connect timeout") ||
|
|
132
|
+
API_NETWORK_ERROR_CODES.some((code) => message.includes(code.toLowerCase())));
|
|
133
|
+
}
|
|
134
|
+
/** Return true only when no request body could have reached the peer. */
|
|
135
|
+
export function isSafeApiAliasFallbackError(error) {
|
|
136
|
+
if (typeof error === "object" && error !== null) {
|
|
137
|
+
const details = error;
|
|
138
|
+
if (typeof details.code === "string" &&
|
|
139
|
+
API_ALIAS_SAFE_ERROR_CODES.includes(details.code)) {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
if (details.cause !== undefined && isSafeApiAliasFallbackError(details.cause)) {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const message = String(error).toLowerCase();
|
|
147
|
+
return API_ALIAS_SAFE_ERROR_CODES.some((code) => message.includes(code.toLowerCase()));
|
|
148
|
+
}
|
|
149
|
+
/** Add the IPv4 loopback alias for clients whose resolver prefers IPv6. */
|
|
150
|
+
export function apiUrlCandidates(url) {
|
|
151
|
+
let parsed;
|
|
152
|
+
try {
|
|
153
|
+
parsed = new URL(url);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return [url];
|
|
157
|
+
}
|
|
158
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
159
|
+
return [url];
|
|
160
|
+
const host = parsed.hostname.toLowerCase();
|
|
161
|
+
if (host !== "localhost" && host !== "127.0.0.1")
|
|
162
|
+
return [url];
|
|
163
|
+
const alias = new URL(url);
|
|
164
|
+
alias.hostname = host === "localhost" ? "127.0.0.1" : "localhost";
|
|
165
|
+
const candidates = [url, alias.toString()];
|
|
166
|
+
if (isWslRuntime()) {
|
|
167
|
+
for (const discoveredHost of discoverWslHosts()) {
|
|
168
|
+
const candidate = replaceUrlHost(url, discoveredHost);
|
|
169
|
+
if (candidate && !candidates.includes(candidate))
|
|
170
|
+
candidates.push(candidate);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return candidates;
|
|
174
|
+
}
|
|
175
|
+
const preferredLoopbackHosts = new Map();
|
|
176
|
+
function orderedApiUrlCandidates(url) {
|
|
177
|
+
const candidates = apiUrlCandidates(url);
|
|
178
|
+
if (candidates.length < 2)
|
|
179
|
+
return candidates;
|
|
180
|
+
try {
|
|
181
|
+
const parsed = new URL(url);
|
|
182
|
+
const preferredHost = preferredLoopbackHosts.get(parsed.origin);
|
|
183
|
+
if (!preferredHost)
|
|
184
|
+
return candidates;
|
|
185
|
+
const preferred = new URL(url);
|
|
186
|
+
preferred.hostname = preferredHost;
|
|
187
|
+
const preferredUrl = preferred.toString();
|
|
188
|
+
return [preferredUrl, ...candidates.filter((candidate) => candidate !== preferredUrl)];
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return candidates;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/** Join an API path to the configured base without losing its prefix/query. */
|
|
195
|
+
export function apiUrl(path = "", base = MEMOS_URL) {
|
|
196
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
197
|
+
try {
|
|
198
|
+
const parsedBase = new URL(base);
|
|
199
|
+
const parsedPath = new URL(normalizedPath, "http://memos.local");
|
|
200
|
+
const basePath = parsedBase.pathname.replace(/\/+$/, "").replace(/^\/+/, "");
|
|
201
|
+
const childPath = parsedPath.pathname.replace(/^\/+/, "");
|
|
202
|
+
const joinedPath = [basePath, childPath].filter(Boolean).join("/");
|
|
203
|
+
parsedBase.pathname = joinedPath ? `/${joinedPath}` : "/";
|
|
204
|
+
// Base query values (for example a gateway token) apply to every endpoint;
|
|
205
|
+
// a path-specific value wins when both use the same key.
|
|
206
|
+
const query = new URLSearchParams(parsedBase.search);
|
|
207
|
+
for (const [key, value] of parsedPath.searchParams)
|
|
208
|
+
query.set(key, value);
|
|
209
|
+
parsedBase.search = query.toString();
|
|
210
|
+
parsedBase.hash = "";
|
|
211
|
+
return parsedBase.toString();
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// Keep the old string behavior for a malformed base; the first fetch then
|
|
215
|
+
// reports the useful URL error to the caller.
|
|
216
|
+
const cleanBase = base.replace(/[?#].*$/, "").replace(/\/+$/, "");
|
|
217
|
+
const cleanPath = normalizedPath.replace(/^\/+/, "");
|
|
218
|
+
return `${cleanBase}/${cleanPath}`;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
export function apiHealthUrl(base = MEMOS_URL, detail = false) {
|
|
222
|
+
const suffix = detail ? "/health/detail" : "/health";
|
|
223
|
+
return apiUrl(suffix, base);
|
|
224
|
+
}
|
|
225
|
+
/** Remove credentials and query values before an endpoint is shown to a client. */
|
|
226
|
+
export function apiUrlForDisplay(url = MEMOS_URL) {
|
|
227
|
+
try {
|
|
228
|
+
const parsed = new URL(url);
|
|
229
|
+
parsed.username = "";
|
|
230
|
+
parsed.password = "";
|
|
231
|
+
parsed.search = "";
|
|
232
|
+
parsed.hash = "";
|
|
233
|
+
const displayed = parsed.toString();
|
|
234
|
+
return parsed.pathname === "/" && displayed.endsWith("/")
|
|
235
|
+
? displayed.slice(0, -1)
|
|
236
|
+
: displayed;
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return url
|
|
240
|
+
.replace(/[?#].*$/, "")
|
|
241
|
+
.replace(/:\/\/[^/@\s]+@/, "://");
|
|
242
|
+
}
|
|
243
|
+
}
|
|
10
244
|
export async function fetchWithTimeout(url, options = {}) {
|
|
11
245
|
const timeoutMs = (options.timeoutMs ?? MEMOS_TIMEOUT_TOOL) * 1000;
|
|
12
246
|
const { timeoutMs: _, ...fetchOptions } = options;
|
|
13
|
-
const
|
|
14
|
-
const
|
|
247
|
+
const method = String(fetchOptions.method ?? "GET").toUpperCase();
|
|
248
|
+
const candidates = orderedApiUrlCandidates(url);
|
|
249
|
+
const isIdempotent = method === "GET" || method === "HEAD";
|
|
250
|
+
let originalOrigin;
|
|
15
251
|
try {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return response;
|
|
252
|
+
originalOrigin = new URL(url).origin;
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
// The first fetch will report the malformed URL.
|
|
21
256
|
}
|
|
22
|
-
|
|
23
|
-
|
|
257
|
+
let lastError;
|
|
258
|
+
for (const candidate of candidates) {
|
|
259
|
+
const controller = new AbortController();
|
|
260
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
261
|
+
try {
|
|
262
|
+
const response = await fetch(candidate, {
|
|
263
|
+
...fetchOptions,
|
|
264
|
+
signal: controller.signal,
|
|
265
|
+
});
|
|
266
|
+
if (originalOrigin) {
|
|
267
|
+
const selectedOrigin = new URL(candidate).origin;
|
|
268
|
+
if (selectedOrigin === originalOrigin) {
|
|
269
|
+
preferredLoopbackHosts.delete(originalOrigin);
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
preferredLoopbackHosts.set(originalOrigin, new URL(candidate).hostname);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return response;
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
lastError = error;
|
|
279
|
+
const canTryAlias = candidate !== candidates.at(-1) &&
|
|
280
|
+
(isIdempotent
|
|
281
|
+
? isApiUnreachableError(error)
|
|
282
|
+
: isSafeApiAliasFallbackError(error));
|
|
283
|
+
if (!canTryAlias) {
|
|
284
|
+
if (originalOrigin) {
|
|
285
|
+
preferredLoopbackHosts.delete(originalOrigin);
|
|
286
|
+
}
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
logger.debug("MemOS API endpoint failed, trying loopback alias");
|
|
290
|
+
}
|
|
291
|
+
finally {
|
|
292
|
+
clearTimeout(timer);
|
|
293
|
+
}
|
|
24
294
|
}
|
|
295
|
+
throw lastError;
|
|
25
296
|
}
|
|
26
297
|
// ============================================================================
|
|
27
298
|
// API Readiness Check
|
|
@@ -32,7 +303,7 @@ export async function waitForApiReady(maxWait, interval = 2.0) {
|
|
|
32
303
|
const start = Date.now();
|
|
33
304
|
while (Date.now() - start < maxWaitMs) {
|
|
34
305
|
try {
|
|
35
|
-
const response = await fetchWithTimeout(
|
|
306
|
+
const response = await fetchWithTimeout(apiUrl("/users"), {
|
|
36
307
|
timeoutMs: MEMOS_TIMEOUT_HEALTH,
|
|
37
308
|
});
|
|
38
309
|
if (response.ok) {
|
|
@@ -57,7 +328,15 @@ function buildUrl(base, params) {
|
|
|
57
328
|
urlParams.append(k, String(v));
|
|
58
329
|
}
|
|
59
330
|
const qs = urlParams.toString();
|
|
60
|
-
|
|
331
|
+
if (!qs)
|
|
332
|
+
return base;
|
|
333
|
+
const hashIndex = base.indexOf("#");
|
|
334
|
+
const beforeHash = hashIndex === -1 ? base : base.slice(0, hashIndex);
|
|
335
|
+
const hash = hashIndex === -1 ? "" : base.slice(hashIndex);
|
|
336
|
+
const separator = beforeHash.includes("?")
|
|
337
|
+
? (beforeHash.endsWith("?") || beforeHash.endsWith("&") ? "" : "&")
|
|
338
|
+
: "?";
|
|
339
|
+
return `${beforeHash}${separator}${qs}${hash}`;
|
|
61
340
|
}
|
|
62
341
|
async function doFetch(method, url, options) {
|
|
63
342
|
const finalUrl = buildUrl(url, options.params);
|
|
@@ -80,7 +359,11 @@ async function doFetch(method, url, options) {
|
|
|
80
359
|
const data = (await response.json());
|
|
81
360
|
return { status: response.status, data };
|
|
82
361
|
}
|
|
83
|
-
catch {
|
|
362
|
+
catch (error) {
|
|
363
|
+
// Body reads can fail after response headers arrive. Preserve deadlines
|
|
364
|
+
// as exceptions so an ambiguous POST is not replayed or reported offline.
|
|
365
|
+
if (isApiTimeoutError(error))
|
|
366
|
+
throw error;
|
|
84
367
|
return { status: response.status, data: null };
|
|
85
368
|
}
|
|
86
369
|
}
|
package/dist/cube-manager.js
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import * as fs from "fs";
|
|
7
7
|
import * as path from "path";
|
|
8
|
-
import {
|
|
8
|
+
import { MEMOS_USER, MEMOS_DEFAULT_CUBE, MEMOS_CUBES_DIR, MEMOS_TIMEOUT_STARTUP, REGISTRATION_RETRY_INTERVAL, registeredCubes, lastRegistrationAttempt, logger, isDefaultCubeFromEnv, } from "./config.js";
|
|
9
9
|
import { detectCubeFromPath } from "./keyword-enhancer.js";
|
|
10
|
-
import { fetchWithTimeout } from "./api-client.js";
|
|
10
|
+
import { apiUrl, apiUrlForDisplay, fetchWithTimeout, isApiUnreachableError, } from "./api-client.js";
|
|
11
11
|
// ============================================================================
|
|
12
12
|
// Windows/WSL Path Conversion
|
|
13
13
|
// ============================================================================
|
|
@@ -368,7 +368,7 @@ export function ensureCubeDirectory(cubeId) {
|
|
|
368
368
|
// ============================================================================
|
|
369
369
|
export async function verifyCubeLoaded(cubeId) {
|
|
370
370
|
try {
|
|
371
|
-
const response = await fetchWithTimeout(
|
|
371
|
+
const response = await fetchWithTimeout(apiUrl(`/memories?user_id=${encodeURIComponent(MEMOS_USER)}&mem_cube_id=${encodeURIComponent(cubeId)}&limit=1`), { method: "GET", timeoutMs: 5 });
|
|
372
372
|
if (response.ok) {
|
|
373
373
|
const data = await response.json();
|
|
374
374
|
return data.code === 200;
|
|
@@ -412,7 +412,7 @@ export async function ensureCubeRegistered(cubeId, force = false) {
|
|
|
412
412
|
}
|
|
413
413
|
}
|
|
414
414
|
// Register with API
|
|
415
|
-
const response = await fetchWithTimeout(
|
|
415
|
+
const response = await fetchWithTimeout(apiUrl("/mem_cubes"), {
|
|
416
416
|
method: "POST",
|
|
417
417
|
headers: { "Content-Type": "application/json" },
|
|
418
418
|
body: JSON.stringify({
|
|
@@ -441,8 +441,8 @@ export async function ensureCubeRegistered(cubeId, force = false) {
|
|
|
441
441
|
}
|
|
442
442
|
catch (err) {
|
|
443
443
|
const msg = String(err);
|
|
444
|
-
if (
|
|
445
|
-
return [false, `Cannot connect to MemOS API at ${
|
|
444
|
+
if (isApiUnreachableError(err)) {
|
|
445
|
+
return [false, `Cannot connect to MemOS API at ${apiUrlForDisplay()}. Is the server running?`];
|
|
446
446
|
}
|
|
447
447
|
return [false, `Failed to register cube '${cubeId}': ${err}`];
|
|
448
448
|
}
|
package/dist/handlers/admin.js
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from "fs";
|
|
8
8
|
import * as path from "path";
|
|
9
|
-
import {
|
|
10
|
-
import { fetchWithTimeout } from "../api-client.js";
|
|
9
|
+
import { MEMOS_USER, MEMOS_DEFAULT_CUBE, MEMOS_CUBES_DIR, MEMOS_ENABLE_DELETE, MEMOS_TIMEOUT_TOOL, registeredCubes, } from "../config.js";
|
|
10
|
+
import { apiUrl, apiUrlForDisplay, fetchWithTimeout } from "../api-client.js";
|
|
11
11
|
import { ensureCubeDirectory, ensureCubeRegistered, getCubePath, getCubesBaseDir, getLocalCubesBaseDir, listAvailableCubes, validateAndFixCubeConfig, verifyCubeLoaded, } from "../cube-manager.js";
|
|
12
12
|
import { ERR_CUBE_NOT_FOUND, ERR_DELETE_DISABLED, ERR_PARAM_MISSING, apiErrorResponse, cubeRegistrationError, errorResponse, getCubeIdFromArgs, } from "./utils.js";
|
|
13
13
|
// ============================================================================
|
|
@@ -43,7 +43,7 @@ export async function handleMemosListCubes(arguments_) {
|
|
|
43
43
|
"",
|
|
44
44
|
`Cubes directory: \`${cubesDir}\``,
|
|
45
45
|
"",
|
|
46
|
-
`To create a new cube, use the MemOS web interface at ${
|
|
46
|
+
`To create a new cube, use the MemOS web interface at ${apiUrlForDisplay(apiUrl("/docs"))}`,
|
|
47
47
|
"or create a cube directory with a config.json file.",
|
|
48
48
|
].join("\n") }];
|
|
49
49
|
}
|
|
@@ -107,7 +107,7 @@ export async function handleMemosRegisterCube(arguments_) {
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
try {
|
|
110
|
-
const response = await fetchWithTimeout(
|
|
110
|
+
const response = await fetchWithTimeout(apiUrl("/mem_cubes"), {
|
|
111
111
|
method: "POST",
|
|
112
112
|
headers: { "Content-Type": "application/json" },
|
|
113
113
|
body: JSON.stringify({
|
|
@@ -157,7 +157,7 @@ export async function handleMemosCreateUser(arguments_) {
|
|
|
157
157
|
const userId = String(arguments_.user_id ?? MEMOS_USER);
|
|
158
158
|
const userName = String(arguments_.user_name ?? userId);
|
|
159
159
|
try {
|
|
160
|
-
const response = await fetchWithTimeout(
|
|
160
|
+
const response = await fetchWithTimeout(apiUrl("/users"), {
|
|
161
161
|
method: "POST",
|
|
162
162
|
headers: { "Content-Type": "application/json" },
|
|
163
163
|
body: JSON.stringify({
|
|
@@ -292,7 +292,7 @@ export async function handleMemosDelete(arguments_) {
|
|
|
292
292
|
if (!regSuccess)
|
|
293
293
|
return cubeRegistrationError(cubeId, regError);
|
|
294
294
|
if (deleteAll) {
|
|
295
|
-
const response = await fetchWithTimeout(
|
|
295
|
+
const response = await fetchWithTimeout(apiUrl(`/memories/${cubeId}?user_id=${encodeURIComponent(MEMOS_USER)}`), { method: "DELETE" });
|
|
296
296
|
if (response.ok) {
|
|
297
297
|
const data = await response.json();
|
|
298
298
|
if (data.code === 200) {
|
|
@@ -321,7 +321,7 @@ export async function handleMemosDelete(arguments_) {
|
|
|
321
321
|
for (const mid of idsToDelete) {
|
|
322
322
|
let memContent = "*(Unknown content)*";
|
|
323
323
|
try {
|
|
324
|
-
const getResp = await fetchWithTimeout(
|
|
324
|
+
const getResp = await fetchWithTimeout(apiUrl(`/memories/${cubeId}/${mid}?user_id=${encodeURIComponent(MEMOS_USER)}`), { method: "GET" });
|
|
325
325
|
if (getResp.ok) {
|
|
326
326
|
const gData = await getResp.json();
|
|
327
327
|
if (gData.code === 200 && gData.data) {
|
|
@@ -335,7 +335,7 @@ export async function handleMemosDelete(arguments_) {
|
|
|
335
335
|
catch {
|
|
336
336
|
memContent = "*(Fetch failed)*";
|
|
337
337
|
}
|
|
338
|
-
const deleteResp = await fetchWithTimeout(
|
|
338
|
+
const deleteResp = await fetchWithTimeout(apiUrl(`/memories/${cubeId}/${mid}?user_id=${encodeURIComponent(MEMOS_USER)}`), { method: "DELETE" });
|
|
339
339
|
if (deleteResp.ok) {
|
|
340
340
|
const data = await deleteResp.json();
|
|
341
341
|
if (data.code === 200) {
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* memos_admin(action="calendar") - project mode and student mode
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
7
|
-
import { apiCallWithRetry } from "../api-client.js";
|
|
6
|
+
import { MEMOS_USER } from "../config.js";
|
|
7
|
+
import { apiCallWithRetry, apiUrl } from "../api-client.js";
|
|
8
8
|
import { ensureCubeRegistered } from "../cube-manager.js";
|
|
9
9
|
import { apiErrorResponse, cubeRegistrationError, getCubeIdFromArgs } from "./utils.js";
|
|
10
10
|
// ============================================================================
|
|
@@ -201,7 +201,7 @@ export async function handleMemosCalendar(arguments_) {
|
|
|
201
201
|
if (!regSuccess)
|
|
202
202
|
return cubeRegistrationError(cubeId, regError);
|
|
203
203
|
if (mode === "project") {
|
|
204
|
-
const result = await apiCallWithRetry("GET",
|
|
204
|
+
const result = await apiCallWithRetry("GET", apiUrl("/memories"), cubeId, { params: { user_id: MEMOS_USER, mem_cube_id: cubeId, limit: 200 } }, ensureCubeRegistered);
|
|
205
205
|
if (result.success && result.data) {
|
|
206
206
|
const resultData = result.data.data ?? {};
|
|
207
207
|
const memories = [];
|
|
@@ -229,7 +229,7 @@ export async function handleMemosCalendar(arguments_) {
|
|
|
229
229
|
if (week !== undefined) {
|
|
230
230
|
[startDate, endDate] = getWeekDates(startDate, week);
|
|
231
231
|
}
|
|
232
|
-
const result = await apiCallWithRetry("GET",
|
|
232
|
+
const result = await apiCallWithRetry("GET", apiUrl("/memories"), cubeId, { params: { user_id: MEMOS_USER, mem_cube_id: cubeId, limit: 100 } }, ensureCubeRegistered);
|
|
233
233
|
if (result.success && result.data) {
|
|
234
234
|
const resultData = result.data.data ?? {};
|
|
235
235
|
let memories = [];
|
package/dist/handlers/graph.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* memos_graph(mode="path"), memos_graph(mode="related"), memos_graph(mode="schema"), memos_graph(mode="impact")
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
7
|
-
import { fetchWithTimeout } from "../api-client.js";
|
|
6
|
+
import { MEMOS_USER, NEO4J_HTTP_URL, NEO4J_USER, NEO4J_PASSWORD, logger, registeredCubes, } from "../config.js";
|
|
7
|
+
import { apiUrl, fetchWithTimeout } from "../api-client.js";
|
|
8
8
|
import { ensureCubeRegistered } from "../cube-manager.js";
|
|
9
9
|
import { formatProvenance } from "../graph-provenance.js";
|
|
10
10
|
import { buildGraphImportPlan, renderGraphImportPlan, } from "../graphify-import.js";
|
|
@@ -99,7 +99,7 @@ export async function handleMemosTracePath(arguments_) {
|
|
|
99
99
|
if (!regSuccess)
|
|
100
100
|
return cubeRegistrationError(cubeId, regError);
|
|
101
101
|
try {
|
|
102
|
-
const response = await fetchWithTimeout(
|
|
102
|
+
const response = await fetchWithTimeout(apiUrl("/product/graph/trace_path"), {
|
|
103
103
|
method: "POST",
|
|
104
104
|
headers: { "Content-Type": "application/json" },
|
|
105
105
|
body: JSON.stringify({
|
|
@@ -243,7 +243,7 @@ export async function handleMemosGetGraph(arguments_) {
|
|
|
243
243
|
// Search for relevant memories first
|
|
244
244
|
let memories = [];
|
|
245
245
|
try {
|
|
246
|
-
const searchResponse = await fetchWithTimeout(
|
|
246
|
+
const searchResponse = await fetchWithTimeout(apiUrl("/search"), {
|
|
247
247
|
method: "POST",
|
|
248
248
|
headers: { "Content-Type": "application/json" },
|
|
249
249
|
body: JSON.stringify({
|
|
@@ -262,7 +262,7 @@ export async function handleMemosGetGraph(arguments_) {
|
|
|
262
262
|
registeredCubes.delete(cubeId);
|
|
263
263
|
const [retrySuccess] = await ensureCubeRegistered(cubeId, true);
|
|
264
264
|
if (retrySuccess) {
|
|
265
|
-
const retrySearch = await fetchWithTimeout(
|
|
265
|
+
const retrySearch = await fetchWithTimeout(apiUrl("/search"), {
|
|
266
266
|
method: "POST",
|
|
267
267
|
headers: { "Content-Type": "application/json" },
|
|
268
268
|
body: JSON.stringify({
|
|
@@ -499,7 +499,7 @@ export async function handleMemosExportSchema(arguments_) {
|
|
|
499
499
|
if (!regSuccess)
|
|
500
500
|
return cubeRegistrationError(cubeId, regError);
|
|
501
501
|
try {
|
|
502
|
-
const response = await fetchWithTimeout(
|
|
502
|
+
const response = await fetchWithTimeout(apiUrl("/graph/schema"), {
|
|
503
503
|
method: "POST",
|
|
504
504
|
headers: { "Content-Type": "application/json" },
|
|
505
505
|
body: JSON.stringify({
|
package/dist/handlers/index.d.ts
CHANGED
|
@@ -7,4 +7,5 @@
|
|
|
7
7
|
import type { TextContent } from "../types.js";
|
|
8
8
|
export declare function dispatchTool(name: string, arguments_: Record<string, unknown>): Promise<TextContent[]>;
|
|
9
9
|
export declare function handleApiUnreachable(): Promise<TextContent[]>;
|
|
10
|
+
export declare function handleApiTimeout(): Promise<TextContent[]>;
|
|
10
11
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/handlers/index.js
CHANGED
|
@@ -14,7 +14,8 @@ import { handleMemosImportWiki } from "./wiki-import.js";
|
|
|
14
14
|
import { handleMemosCanvas } from "./canvas.js";
|
|
15
15
|
import { handleMemosDistillSkill, handleMemosListSkillCandidates, handleMemosReviewSkillCandidate, handleMemosInstallSkillCandidate } from "./skill.js";
|
|
16
16
|
import { handleMemosListCubes, handleMemosRegisterCube, handleMemosCreateUser, handleMemosValidateCubes, handleMemosDelete, handleMemosCapabilities, } from "./admin.js";
|
|
17
|
-
import {
|
|
17
|
+
import { MEMOS_PROVIDER } from "../config.js";
|
|
18
|
+
import { apiHealthUrl, apiUrlForDisplay } from "../api-client.js";
|
|
18
19
|
import { errorResponse } from "./utils.js";
|
|
19
20
|
export async function dispatchTool(name, arguments_) {
|
|
20
21
|
if (MEMOS_PROVIDER === "local" && ["memos_think", "memos_graph", "memos_export_wiki", "memos_import_wiki", "memos_admin", "memos_delete"].includes(name)) {
|
|
@@ -99,13 +100,19 @@ export async function handleApiUnreachable() {
|
|
|
99
100
|
return [{
|
|
100
101
|
type: "text",
|
|
101
102
|
text: [
|
|
102
|
-
`❌ [API_UNREACHABLE] Cannot connect to MemOS API at ${
|
|
103
|
+
`❌ [API_UNREACHABLE] Cannot connect to MemOS API at ${apiUrlForDisplay()}`,
|
|
103
104
|
"",
|
|
104
105
|
"💡 Suggestions:",
|
|
105
|
-
|
|
106
|
+
`- Check if MemOS API is running: \`curl ${apiUrlForDisplay(apiHealthUrl())}\``,
|
|
106
107
|
"- Start with: `scripts/local/start.bat`",
|
|
107
108
|
"- Check port availability",
|
|
108
109
|
].join("\n"),
|
|
109
110
|
}];
|
|
110
111
|
}
|
|
112
|
+
export async function handleApiTimeout() {
|
|
113
|
+
return errorResponse(`MemOS API at ${apiUrlForDisplay()} did not respond before the request deadline.`, "API_TIMEOUT", [
|
|
114
|
+
"A write may still have completed. Check memos_search or memos_list_v2 before retrying.",
|
|
115
|
+
"Check API logs for storage delays; LLM parsing runs in background after vector confirmation.",
|
|
116
|
+
]);
|
|
117
|
+
}
|
|
111
118
|
//# sourceMappingURL=index.js.map
|
package/dist/handlers/memory.js
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
* memos_save, memos_list_v2, memos_get, memos_admin(action="stats")
|
|
5
5
|
*/
|
|
6
6
|
import * as crypto from "crypto";
|
|
7
|
-
import {
|
|
7
|
+
import { MEMOS_USER, MEMOS_CUBES_DIR, logger } from "../config.js";
|
|
8
8
|
import { getMemoryProvider } from "../providers/provider-factory.js";
|
|
9
|
-
import { apiCallWithRetry } from "../api-client.js";
|
|
9
|
+
import { apiCallWithRetry, apiUrl } from "../api-client.js";
|
|
10
10
|
import { ensureCubeRegistered } from "../cube-manager.js";
|
|
11
|
-
import { parseMemoryWriteResponse } from "../memory-write-response.js";
|
|
11
|
+
import { parseMemoryWriteResponse, UnconfirmedMemoryWriteError } from "../memory-write-response.js";
|
|
12
12
|
import { recordAccess } from "../access-tracker.js";
|
|
13
13
|
import { filterEphemeralTier } from "../memory-tier.js";
|
|
14
14
|
import { findSiblings, renderVerbatimSections, verbatimOf, } from "../verbatim-source.js";
|
|
@@ -95,24 +95,32 @@ export async function handleMemosSave(arguments_) {
|
|
|
95
95
|
const [regSuccess, regError] = await ensureCubeRegistered(cubeId);
|
|
96
96
|
if (!regSuccess)
|
|
97
97
|
return cubeRegistrationError(cubeId, regError);
|
|
98
|
-
const result = await apiCallWithRetry("POST",
|
|
98
|
+
const result = await apiCallWithRetry("POST", apiUrl("/memories"), cubeId, {
|
|
99
99
|
body: {
|
|
100
100
|
user_id: MEMOS_USER,
|
|
101
101
|
mem_cube_id: cubeId,
|
|
102
102
|
memory_content: content,
|
|
103
|
+
memory_type: memoryType,
|
|
103
104
|
},
|
|
104
105
|
}, ensureCubeRegistered);
|
|
105
106
|
if (result.success) {
|
|
106
|
-
markSaved(content, cubeId);
|
|
107
107
|
const write = parseMemoryWriteResponse(result.data);
|
|
108
|
+
if (write.vectorSaved === false || (write.vectorSaved === true && write.memoryIds.length === 0)) {
|
|
109
|
+
throw new UnconfirmedMemoryWriteError(errorResponse("Vector storage was not confirmed; the save cannot be acknowledged.", "VECTOR_SAVE_UNCONFIRMED", ["Check API storage logs and search for the memory before retrying."]));
|
|
110
|
+
}
|
|
111
|
+
markSaved(content, cubeId);
|
|
108
112
|
const idText = write.memoryIds.length > 0 ? ` · IDs: ${write.memoryIds.join(", ")}` : "";
|
|
109
113
|
const warningText = write.warnings.length > 0
|
|
110
114
|
? ` · warnings: ${write.warnings.join("; ")}`
|
|
111
115
|
: "";
|
|
116
|
+
const vectorText = write.vectorSaved === true ? " · vector saved" : "";
|
|
117
|
+
const enrichmentText = write.enrichmentStatus === "pending"
|
|
118
|
+
? " · LLM parsing continues in background"
|
|
119
|
+
: write.enrichmentStatus === "failed" ? " · background parsing failed; saved memory is retained" : "";
|
|
112
120
|
return [
|
|
113
121
|
{
|
|
114
122
|
type: "text",
|
|
115
|
-
text: `Memory saved as [${memoryType}] in cube '${cubeId}'${idText}${warningText}`,
|
|
123
|
+
text: `Memory saved as [${memoryType}] in cube '${cubeId}'${idText}${vectorText}${enrichmentText}${warningText}`,
|
|
116
124
|
},
|
|
117
125
|
];
|
|
118
126
|
}
|
|
@@ -177,7 +185,7 @@ export async function handleMemosList(arguments_) {
|
|
|
177
185
|
// (the API filters by internal type, not MCP type), but never the whole cube —
|
|
178
186
|
// pulling everything blew a limit=5 request up to ~1.8M tokens on large cubes.
|
|
179
187
|
params.limit = memoryType ? Math.max(limit * 20, 200) : limit;
|
|
180
|
-
const result = await apiCallWithRetry("GET",
|
|
188
|
+
const result = await apiCallWithRetry("GET", apiUrl("/memories"), cubeId, { params }, ensureCubeRegistered);
|
|
181
189
|
if (result.success && result.data) {
|
|
182
190
|
const resultData = result.data.data ?? {};
|
|
183
191
|
const allMemories = prepareListMemories(resultData, memoryType, limit);
|
|
@@ -233,7 +241,7 @@ export async function handleMemosList(arguments_) {
|
|
|
233
241
|
*/
|
|
234
242
|
async function fetchSiblings(cubeId, target) {
|
|
235
243
|
try {
|
|
236
|
-
const result = await apiCallWithRetry("GET",
|
|
244
|
+
const result = await apiCallWithRetry("GET", apiUrl("/memories"), cubeId, { params: { user_id: MEMOS_USER, mem_cube_id: cubeId, limit: 200 } }, ensureCubeRegistered);
|
|
237
245
|
if (!result.success || !result.data)
|
|
238
246
|
return [];
|
|
239
247
|
const data = result.data.data;
|
|
@@ -286,7 +294,7 @@ export async function handleMemosGet(arguments_) {
|
|
|
286
294
|
const [regSuccess, regError] = await ensureCubeRegistered(cubeId);
|
|
287
295
|
if (!regSuccess)
|
|
288
296
|
return cubeRegistrationError(cubeId, regError);
|
|
289
|
-
const result = await apiCallWithRetry("GET",
|
|
297
|
+
const result = await apiCallWithRetry("GET", apiUrl(`/memories/${cubeId}/${memoryId}`), cubeId,
|
|
290
298
|
// user_id 必须显式传:后端 `get_memory` 的 user_id 是可选参数,缺失时回退到
|
|
291
299
|
// MOS 实例自己的 user_id(root)。而 ensureCubeRegistered 注册的是 MEMOS_USER
|
|
292
300
|
// (dev_user),于是 root 对该 cube 无授权,校验抛 ValueError → 400
|
|
@@ -385,7 +393,7 @@ export async function handleMemosGetStats(arguments_) {
|
|
|
385
393
|
const [regSuccess, regError] = await ensureCubeRegistered(cubeId);
|
|
386
394
|
if (!regSuccess)
|
|
387
395
|
return cubeRegistrationError(cubeId, regError);
|
|
388
|
-
const result = await apiCallWithRetry("GET",
|
|
396
|
+
const result = await apiCallWithRetry("GET", apiUrl("/memories"), cubeId, { params: { user_id: MEMOS_USER, mem_cube_id: cubeId } }, ensureCubeRegistered);
|
|
389
397
|
if (result.success && result.data) {
|
|
390
398
|
const [stats, total] = computeMemoryStats(result.data.data ?? {});
|
|
391
399
|
if (total === 0) {
|
package/dist/handlers/search.js
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* memos_search, memos_search, memos_suggest, memos_context_resume
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { MEMOS_USER, NEO4J_HTTP_URL, NEO4J_USER, NEO4J_PASSWORD, MEMOS_MODE, MEMOS_CUBES_DIR, logger, } from "../config.js";
|
|
7
7
|
import { getMemoryProvider } from "../providers/provider-factory.js";
|
|
8
|
-
import { apiCallWithRetry, fetchWithTimeout } from "../api-client.js";
|
|
8
|
+
import { apiCallWithRetry, apiUrl, fetchWithTimeout } from "../api-client.js";
|
|
9
9
|
import { ensureCubeRegistered } from "../cube-manager.js";
|
|
10
10
|
import { formatMemoriesForDisplay } from "../formatters.js";
|
|
11
11
|
import { PREVIEW_COUNT, shouldCompact, compactedResultToText, toMinimal, } from "../models.js";
|
|
@@ -307,7 +307,7 @@ export async function handleMemosSearch(arguments_) {
|
|
|
307
307
|
const [regSuccess, regError] = await ensureCubeRegistered(cubeId);
|
|
308
308
|
if (!regSuccess)
|
|
309
309
|
return cubeRegistrationError(cubeId, regError);
|
|
310
|
-
const apiResult = await apiCallWithRetry("POST",
|
|
310
|
+
const apiResult = await apiCallWithRetry("POST", apiUrl("/search"), cubeId, {
|
|
311
311
|
body: {
|
|
312
312
|
user_id: MEMOS_USER,
|
|
313
313
|
query,
|
|
@@ -387,7 +387,7 @@ export async function handleMemosSearchContext(arguments_) {
|
|
|
387
387
|
if (!regSuccess)
|
|
388
388
|
return cubeRegistrationError(cubeId, regError);
|
|
389
389
|
try {
|
|
390
|
-
const response = await fetchWithTimeout(
|
|
390
|
+
const response = await fetchWithTimeout(apiUrl("/search"), {
|
|
391
391
|
method: "POST",
|
|
392
392
|
headers: { "Content-Type": "application/json" },
|
|
393
393
|
body: JSON.stringify({
|
|
@@ -424,7 +424,7 @@ export async function handleMemosSearchContext(arguments_) {
|
|
|
424
424
|
}
|
|
425
425
|
else {
|
|
426
426
|
// Fallback to standard search
|
|
427
|
-
const fallbackResult = await apiCallWithRetry("POST",
|
|
427
|
+
const fallbackResult = await apiCallWithRetry("POST", apiUrl("/search"), cubeId, { body: { user_id: MEMOS_USER, query, install_cube_ids: [cubeId] } }, ensureCubeRegistered);
|
|
428
428
|
if (fallbackResult.success && fallbackResult.data) {
|
|
429
429
|
let resultData = fallbackResult.data
|
|
430
430
|
.data ?? {};
|
|
@@ -564,7 +564,7 @@ export async function handleMemosContextResume(arguments_) {
|
|
|
564
564
|
let recentMemories = await getTemporalMemories(cubeId, RESUME_LIMIT, 24);
|
|
565
565
|
// Fallback to API list
|
|
566
566
|
if (recentMemories.length === 0) {
|
|
567
|
-
const result = await apiCallWithRetry("GET",
|
|
567
|
+
const result = await apiCallWithRetry("GET", apiUrl("/memories"), cubeId, {
|
|
568
568
|
params: {
|
|
569
569
|
user_id: MEMOS_USER,
|
|
570
570
|
mem_cube_id: cubeId,
|
package/dist/handlers/think.js
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* of CONFLICT edges: those edges are only created by the reorganizer pipeline,
|
|
15
15
|
* which is disabled by default, so in practice they rarely exist.
|
|
16
16
|
*/
|
|
17
|
-
import {
|
|
18
|
-
import { apiCallWithRetry } from "../api-client.js";
|
|
17
|
+
import { MEMOS_USER } from "../config.js";
|
|
18
|
+
import { apiCallWithRetry, apiUrl } from "../api-client.js";
|
|
19
19
|
import { ensureCubeRegistered } from "../cube-manager.js";
|
|
20
20
|
import { extractKeywords, extractMcpType } from "../query-processing.js";
|
|
21
21
|
import { levenshteinDistance } from "../keyword-enhancer.js";
|
|
@@ -127,7 +127,7 @@ export async function handleMemosThink(arguments_) {
|
|
|
127
127
|
return cubeRegistrationError(cubeId, regError);
|
|
128
128
|
// Two recall paths in parallel: semantic search + recent temporal slice.
|
|
129
129
|
const [apiResult, temporal] = await Promise.all([
|
|
130
|
-
apiCallWithRetry("POST",
|
|
130
|
+
apiCallWithRetry("POST", apiUrl("/search"), cubeId, { body: { user_id: MEMOS_USER, query, install_cube_ids: [cubeId], top_k: topK } }, ensureCubeRegistered),
|
|
131
131
|
getTemporalMemories(cubeId, 5, 72).catch(() => []),
|
|
132
132
|
]);
|
|
133
133
|
if (!apiResult.success) {
|
package/dist/handlers/utils.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { detectCubeFromPath } from "../keyword-enhancer.js";
|
|
7
7
|
import { getDefaultCubeId } from "../cube-manager.js";
|
|
8
|
+
import { apiHealthUrl, apiUrlForDisplay } from "../api-client.js";
|
|
8
9
|
// ============================================================================
|
|
9
10
|
// Error Codes
|
|
10
11
|
// ============================================================================
|
|
@@ -43,7 +44,7 @@ export function successResponse(message) {
|
|
|
43
44
|
}
|
|
44
45
|
export function cubeRegistrationError(cubeId, detail) {
|
|
45
46
|
return errorResponse(`Cube '${cubeId}' registration failed: ${detail ?? "unknown error"}`, ERR_CUBE_REGISTRATION, [
|
|
46
|
-
`Check if MemOS API is running: \`curl
|
|
47
|
+
`Check if MemOS API is running: \`curl ${apiUrlForDisplay(apiHealthUrl())}\``,
|
|
47
48
|
`Verify cube exists: \`memos_admin(action="list_cubes", include_status=True)\``,
|
|
48
49
|
`Try manual registration: \`memos_admin(action="register_cube", cube_id="...")\``,
|
|
49
50
|
]);
|
|
@@ -68,7 +69,7 @@ export function apiErrorResponse(operation, statusOrMsg) {
|
|
|
68
69
|
(msg.includes("cube") && msg.includes("not found"))) {
|
|
69
70
|
suggestions.push('Cube not registered — run `memos_admin(action="register_cube", cube_id="...")` (find ids via `memos_admin(action="list_cubes")`), then retry');
|
|
70
71
|
}
|
|
71
|
-
suggestions.push(
|
|
72
|
+
suggestions.push(`Check API health: \`curl ${apiUrlForDisplay(apiHealthUrl(undefined, true))}\``, "Check API logs for details");
|
|
72
73
|
return errorResponse(`${operation} failed: ${statusOrMsg}`, ERR_API_ERROR, suggestions);
|
|
73
74
|
}
|
|
74
75
|
// ============================================================================
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import * as fs from "fs";
|
|
17
17
|
import * as path from "path";
|
|
18
|
-
import {
|
|
19
|
-
import { fetchWithTimeout } from "../api-client.js";
|
|
18
|
+
import { logger } from "../config.js";
|
|
19
|
+
import { apiUrl, fetchWithTimeout } from "../api-client.js";
|
|
20
20
|
import { ensureCubeRegistered } from "../cube-manager.js";
|
|
21
21
|
import { extractMcpType } from "../query-processing.js";
|
|
22
22
|
import { EDGE_LABELS } from "../wiki-relations.js";
|
|
@@ -33,7 +33,7 @@ async function fetchAllGraphData(cubeId) {
|
|
|
33
33
|
let totalNodes = 0;
|
|
34
34
|
let totalEdges = 0;
|
|
35
35
|
for (let page = 1; page <= MAX_PAGES; page++) {
|
|
36
|
-
const response = await fetchWithTimeout(
|
|
36
|
+
const response = await fetchWithTimeout(apiUrl("/product/graph/data"), {
|
|
37
37
|
method: "POST",
|
|
38
38
|
headers: { "Content-Type": "application/json" },
|
|
39
39
|
// user_id doubles as the Neo4j user_name filter server-side, and cube
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { apiCallWithRetry } from "../api-client.js";
|
|
1
|
+
import { MEMOS_USER } from "../config.js";
|
|
2
|
+
import { apiCallWithRetry, apiUrl } from "../api-client.js";
|
|
3
3
|
import { ensureCubeRegistered } from "../cube-manager.js";
|
|
4
4
|
import { relationEdges } from "../wiki-relations.js";
|
|
5
5
|
async function writeRelationsForPage(cubeId, effectiveId, page, fileBaseIndex) {
|
|
@@ -7,7 +7,7 @@ async function writeRelationsForPage(cubeId, effectiveId, page, fileBaseIndex) {
|
|
|
7
7
|
const written = [];
|
|
8
8
|
const failed = [];
|
|
9
9
|
for (const edge of result.resolved) {
|
|
10
|
-
const relResult = await apiCallWithRetry("POST",
|
|
10
|
+
const relResult = await apiCallWithRetry("POST", apiUrl("/product/graph/relation"), cubeId, {
|
|
11
11
|
body: {
|
|
12
12
|
user_id: MEMOS_USER,
|
|
13
13
|
mem_cube_id: cubeId,
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
import * as crypto from "crypto";
|
|
18
18
|
import * as fs from "fs";
|
|
19
19
|
import * as path from "path";
|
|
20
|
-
import {
|
|
21
|
-
import { apiCallWithRetry } from "../api-client.js";
|
|
20
|
+
import { MEMOS_USER, logger } from "../config.js";
|
|
21
|
+
import { apiCallWithRetry, apiUrl } from "../api-client.js";
|
|
22
22
|
import { ensureCubeRegistered } from "../cube-manager.js";
|
|
23
23
|
import { parseMemoryWriteResponse } from "../memory-write-response.js";
|
|
24
24
|
import { inspectWikiPages, normalizeLedger, } from "../wiki-import-safety.js";
|
|
@@ -135,7 +135,7 @@ function releaseImportLock(wikiDir) {
|
|
|
135
135
|
}
|
|
136
136
|
/** Returns the stored node, null when the memory does not exist, or an error string. */
|
|
137
137
|
async function getStoredMemory(cubeId, memoryId) {
|
|
138
|
-
const result = await apiCallWithRetry("GET",
|
|
138
|
+
const result = await apiCallWithRetry("GET", apiUrl(`/memories/${cubeId}/${memoryId}`), cubeId,
|
|
139
139
|
// 同 handlers/memory.ts 的 handleMemosGet:缺 user_id 会让后端回退到 root,
|
|
140
140
|
// 对 MEMOS_USER 名下的 cube 报「does not have access」。这里更隐蔽 —— 该错误
|
|
141
141
|
// 既不含 "not found" 也不是 404,会被当成真实故障返回,把整页 import 判成失败。
|
|
@@ -164,7 +164,7 @@ async function savePageAsMemory(cubeId, page) {
|
|
|
164
164
|
body.created_at = page.created;
|
|
165
165
|
if (page.updated)
|
|
166
166
|
body.updated_at = page.updated;
|
|
167
|
-
const result = await apiCallWithRetry("POST",
|
|
167
|
+
const result = await apiCallWithRetry("POST", apiUrl("/memories"), cubeId, { body }, ensureCubeRegistered);
|
|
168
168
|
if (!result.success) {
|
|
169
169
|
return {
|
|
170
170
|
error: String(result.data?.message ??
|
|
@@ -175,6 +175,12 @@ async function savePageAsMemory(cubeId, page) {
|
|
|
175
175
|
};
|
|
176
176
|
}
|
|
177
177
|
const parsed = parseMemoryWriteResponse(result.data);
|
|
178
|
+
if (parsed.vectorSaved === false || (parsed.vectorSaved === true && parsed.memoryIds.length === 0)) {
|
|
179
|
+
return {
|
|
180
|
+
error: "Vector storage was not confirmed", memoryIds: [],
|
|
181
|
+
warnings: parsed.warnings, uncertain: true,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
178
184
|
return {
|
|
179
185
|
error: null,
|
|
180
186
|
memoryIds: parsed.memoryIds,
|
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
import type { ApiResponse } from "./types.js";
|
|
1
|
+
import type { ApiResponse, TextContent } from "./types.js";
|
|
2
|
+
/** A rejected storage acknowledgement must be an MCP tool error, not success text. */
|
|
3
|
+
export declare class UnconfirmedMemoryWriteError extends Error {
|
|
4
|
+
readonly content: TextContent[];
|
|
5
|
+
constructor(content: TextContent[]);
|
|
6
|
+
}
|
|
2
7
|
export interface MemoryWriteResult {
|
|
3
8
|
memoryIds: string[];
|
|
4
9
|
warnings: string[];
|
|
10
|
+
vectorSaved?: boolean;
|
|
11
|
+
enrichmentStatus?: string;
|
|
5
12
|
}
|
|
6
13
|
/** Normalize the additive POST /memories response while accepting legacy APIs. */
|
|
7
14
|
export declare function parseMemoryWriteResponse(response: ApiResponse<unknown>): MemoryWriteResult;
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/** A rejected storage acknowledgement must be an MCP tool error, not success text. */
|
|
2
|
+
export class UnconfirmedMemoryWriteError extends Error {
|
|
3
|
+
content;
|
|
4
|
+
constructor(content) {
|
|
5
|
+
super(content.map((item) => item.text).join("\n"));
|
|
6
|
+
this.content = content;
|
|
7
|
+
this.name = "UnconfirmedMemoryWriteError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
1
10
|
/** Normalize the additive POST /memories response while accepting legacy APIs. */
|
|
2
11
|
export function parseMemoryWriteResponse(response) {
|
|
3
12
|
const data = response.data;
|
|
@@ -10,6 +19,10 @@ export function parseMemoryWriteResponse(response) {
|
|
|
10
19
|
const warnings = Array.isArray(payload.warnings)
|
|
11
20
|
? payload.warnings.filter((warning) => typeof warning === "string" && warning.length > 0)
|
|
12
21
|
: [];
|
|
13
|
-
return {
|
|
22
|
+
return {
|
|
23
|
+
memoryIds, warnings,
|
|
24
|
+
...(typeof payload.vector_saved === "boolean" ? { vectorSaved: payload.vector_saved } : {}),
|
|
25
|
+
...(typeof payload.enrichment_status === "string" ? { enrichmentStatus: payload.enrichment_status } : {}),
|
|
26
|
+
};
|
|
14
27
|
}
|
|
15
28
|
//# sourceMappingURL=memory-write-response.js.map
|
package/dist/models.js
CHANGED
|
@@ -76,6 +76,9 @@ export function toFull(memory, cubeId = "", userId = "") {
|
|
|
76
76
|
const cleanContent = content.replace(/^\[[A-Z_]+\]\s*/, "");
|
|
77
77
|
return {
|
|
78
78
|
...brief,
|
|
79
|
+
createdAt: memory.created_at ??
|
|
80
|
+
meta.created_at ??
|
|
81
|
+
brief.createdAt,
|
|
79
82
|
content: cleanContent,
|
|
80
83
|
background: memory.background ?? meta.background,
|
|
81
84
|
cubeId,
|
package/dist/server.js
CHANGED
|
@@ -7,10 +7,11 @@ import { readFileSync } from "node:fs";
|
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { dirname, join } from "node:path";
|
|
9
9
|
import { MEMOS_DEFAULT_CUBE, MEMOS_ENABLE_DELETE, MEMOS_PROVIDER, logger } from "./config.js";
|
|
10
|
-
import { waitForApiReady } from "./api-client.js";
|
|
10
|
+
import { apiHealthUrl, apiUrlForDisplay, isApiTimeoutError, isApiUnreachableError, waitForApiReady, } from "./api-client.js";
|
|
11
11
|
import { ensureCubeRegistered } from "./cube-manager.js";
|
|
12
12
|
import { toolSchemas, toolAnnotations } from "./tools-registry.js";
|
|
13
|
-
import { dispatchTool, handleApiUnreachable } from "./handlers/index.js";
|
|
13
|
+
import { dispatchTool, handleApiTimeout, handleApiUnreachable } from "./handlers/index.js";
|
|
14
|
+
import { UnconfirmedMemoryWriteError } from "./memory-write-response.js";
|
|
14
15
|
import { createOnceFireAndForget } from "./server-lifecycle.js";
|
|
15
16
|
import { NormalizingStdioTransport } from "./stdio-compat-transport.js";
|
|
16
17
|
// ============================================================================
|
|
@@ -41,8 +42,14 @@ function registerTools(server) {
|
|
|
41
42
|
};
|
|
42
43
|
}
|
|
43
44
|
catch (err) {
|
|
45
|
+
if (err instanceof UnconfirmedMemoryWriteError) {
|
|
46
|
+
return { content: err.content, isError: true };
|
|
47
|
+
}
|
|
44
48
|
const errStr = String(err);
|
|
45
|
-
if (
|
|
49
|
+
if (isApiTimeoutError(err)) {
|
|
50
|
+
return { content: await handleApiTimeout(), isError: true };
|
|
51
|
+
}
|
|
52
|
+
if (isApiUnreachableError(err)) {
|
|
46
53
|
const unreachable = await handleApiUnreachable();
|
|
47
54
|
return {
|
|
48
55
|
content: unreachable.map((r) => ({ type: "text", text: r.text })),
|
|
@@ -58,7 +65,7 @@ function registerTools(server) {
|
|
|
58
65
|
"",
|
|
59
66
|
"💡 Suggestions:",
|
|
60
67
|
"- Check MCP server logs for details",
|
|
61
|
-
|
|
68
|
+
`- Verify MemOS API is healthy: \`curl ${apiUrlForDisplay(apiHealthUrl(undefined, true))}\``,
|
|
62
69
|
].join("\n"),
|
|
63
70
|
},
|
|
64
71
|
],
|