@marsnme/mcp-gateway 0.2.0 → 0.3.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/deploy/phase2/build_release_artifact.sh +5 -5
- package/deploy/phase2/pre_deploy_schema_gate.sh +1 -1
- package/deploy/phase3/smoke_gate.sh +11 -5
- package/deploy/systemd/memory-mcp-gateway@.service +6 -6
- package/package.json +2 -2
- package/scripts/tests/test_dream_runner_modes.py +2 -2
- package/server.mjs +374 -89
|
@@ -31,15 +31,15 @@ ARTIFACT_PATH="${OUTPUT_DIR}/${ARTIFACT_BASENAME}.tar.gz"
|
|
|
31
31
|
MANIFEST_PATH="${OUTPUT_DIR}/${ARTIFACT_BASENAME}.manifest.json"
|
|
32
32
|
|
|
33
33
|
tar -C "${REPO_ROOT}" -czf "${ARTIFACT_PATH}" \
|
|
34
|
-
--exclude='
|
|
35
|
-
--exclude='
|
|
34
|
+
--exclude='marsnme-supabase/deploy/artifacts' \
|
|
35
|
+
--exclude='marsnme-supabase/deploy/phase0/ct101-baseline-*' \
|
|
36
36
|
README.md \
|
|
37
|
-
|
|
37
|
+
marsnme-supabase \
|
|
38
38
|
supabase/config.toml \
|
|
39
39
|
supabase/migrations
|
|
40
40
|
|
|
41
41
|
ARTIFACT_SHA256="$(shasum -a 256 "${ARTIFACT_PATH}" | awk '{print $1}')"
|
|
42
|
-
SERVER_SHA256="$(shasum -a 256 "${REPO_ROOT}/
|
|
42
|
+
SERVER_SHA256="$(shasum -a 256 "${REPO_ROOT}/marsnme-supabase/server.mjs" | awk '{print $1}')"
|
|
43
43
|
|
|
44
44
|
cat > "${MANIFEST_PATH}" <<EOF
|
|
45
45
|
{
|
|
@@ -47,7 +47,7 @@ cat > "${MANIFEST_PATH}" <<EOF
|
|
|
47
47
|
"commit_hash": "${COMMIT_HASH}",
|
|
48
48
|
"artifact_path": "${ARTIFACT_PATH}",
|
|
49
49
|
"artifact_sha256": "${ARTIFACT_SHA256}",
|
|
50
|
-
"server_path": "
|
|
50
|
+
"server_path": "marsnme-supabase/server.mjs",
|
|
51
51
|
"server_sha256": "${SERVER_SHA256}"
|
|
52
52
|
}
|
|
53
53
|
EOF
|
|
@@ -151,7 +151,7 @@ for profile in "${PROFILE_LIST[@]}"; do
|
|
|
151
151
|
profile_ident="$(quote_ident "${profile}")"
|
|
152
152
|
|
|
153
153
|
check_sql "${profile}.memories required columns" \
|
|
154
|
-
"SELECT id,body,source,session_id,tags,agent_body,environment,promoted,promoted_at,created_at,expires_at FROM ${profile_ident}.memories LIMIT 0;"
|
|
154
|
+
"SELECT id,body,source,session_id,tags,agent_body,environment,promoted,promoted_at,created_at,expires_at,recipient_body,note,read_at FROM ${profile_ident}.memories LIMIT 0;"
|
|
155
155
|
|
|
156
156
|
check_sql "${profile}.marsvault_chunks required columns" \
|
|
157
157
|
"SELECT id,content,source_file,section,body,visibility,tags,type,date,origin,source_memory_id,source_session_id,source_tool,source_user_note,agent_body,environment,created_at,updated_at FROM ${profile_ident}.marsvault_chunks LIMIT 0;"
|
|
@@ -112,9 +112,9 @@ mcp_jsonrpc() {
|
|
|
112
112
|
if [[ "${SPAWN_LOCAL}" == "true" ]]; then
|
|
113
113
|
COCO_LOG="/tmp/mars-memory-coco-smoke.log"
|
|
114
114
|
TOTO_LOG="/tmp/mars-memory-toto-smoke.log"
|
|
115
|
-
MCP_PROFILE=coco PORT=18790 SUPABASE_BASE_URL="${SUPABASE_BASE_URL_FOR_LOCAL}" MCP_REQUIRE_BEARER=false node "${REPO_ROOT}/
|
|
115
|
+
MCP_PROFILE=coco PORT=18790 SUPABASE_BASE_URL="${SUPABASE_BASE_URL_FOR_LOCAL}" MCP_REQUIRE_BEARER=false node "${REPO_ROOT}/marsnme-supabase/server.mjs" >"${COCO_LOG}" 2>&1 &
|
|
116
116
|
COCO_PID="$!"
|
|
117
|
-
MCP_PROFILE=toto PORT=18791 SUPABASE_BASE_URL="${SUPABASE_BASE_URL_FOR_LOCAL}" MCP_REQUIRE_BEARER=false node "${REPO_ROOT}/
|
|
117
|
+
MCP_PROFILE=toto PORT=18791 SUPABASE_BASE_URL="${SUPABASE_BASE_URL_FOR_LOCAL}" MCP_REQUIRE_BEARER=false node "${REPO_ROOT}/marsnme-supabase/server.mjs" >"${TOTO_LOG}" 2>&1 &
|
|
118
118
|
TOTO_PID="$!"
|
|
119
119
|
sleep 2
|
|
120
120
|
fi
|
|
@@ -131,7 +131,8 @@ COCO_TOOLS_JSON="$(mcp_jsonrpc "${COCO_URL}" '{"jsonrpc":"2.0","id":"coco-tools-
|
|
|
131
131
|
TOTO_TOOLS_JSON="$(mcp_jsonrpc "${TOTO_URL}" '{"jsonrpc":"2.0","id":"toto-tools-list","method":"tools/list","params":{}}')"
|
|
132
132
|
|
|
133
133
|
COCO_TOOLS_CHECK="$(TOOLS_PAYLOAD="${COCO_TOOLS_JSON}" node -e '
|
|
134
|
-
const expected = ["insert_memory", "list_memories", "search_memories", "recall", "health_check", "session_boot", "session_close", "dream_ingest", "memory_ingest"];
|
|
134
|
+
const expected = ["insert_memory", "list_memories", "search_memories", "reload_source_registry", "recall", "get_summary", "get_full", "health_check", "session_boot", "session_close", "dream_ingest", "memory_ingest", "demote_memory", "soft_forget", "explain_memory", "batch_promote"];
|
|
135
|
+
const prdTools = ["save_prd", "get_prd", "list_prds", "score_prd", "spawn_to_linear"];
|
|
135
136
|
const payload = JSON.parse(process.env.TOOLS_PAYLOAD || "{}");
|
|
136
137
|
if (payload.error) throw new Error("tools/list error: " + JSON.stringify(payload.error));
|
|
137
138
|
const tools = payload.result?.tools;
|
|
@@ -139,11 +140,14 @@ if (!Array.isArray(tools)) throw new Error("tools/list missing result.tools");
|
|
|
139
140
|
const names = tools.map((item) => item?.name).filter(Boolean);
|
|
140
141
|
const missing = expected.filter((name) => !names.includes(name));
|
|
141
142
|
if (missing.length > 0) throw new Error("missing tools: " + missing.join(","));
|
|
143
|
+
const leaked = names.filter((name) => prdTools.includes(name));
|
|
144
|
+
if (leaked.length > 0) throw new Error("PRD tools must not be exposed: " + leaked.join(","));
|
|
142
145
|
process.stdout.write(JSON.stringify({ tool_count: names.length, tools: names }));
|
|
143
146
|
')"
|
|
144
147
|
|
|
145
148
|
TOTO_TOOLS_CHECK="$(TOOLS_PAYLOAD="${TOTO_TOOLS_JSON}" node -e '
|
|
146
|
-
const expected = ["insert_memory", "list_memories", "search_memories", "recall", "health_check", "session_boot", "session_close", "dream_ingest", "memory_ingest"];
|
|
149
|
+
const expected = ["insert_memory", "list_memories", "search_memories", "reload_source_registry", "recall", "get_summary", "get_full", "health_check", "session_boot", "session_close", "dream_ingest", "memory_ingest", "demote_memory", "soft_forget", "explain_memory", "batch_promote"];
|
|
150
|
+
const prdTools = ["save_prd", "get_prd", "list_prds", "score_prd", "spawn_to_linear"];
|
|
147
151
|
const payload = JSON.parse(process.env.TOOLS_PAYLOAD || "{}");
|
|
148
152
|
if (payload.error) throw new Error("tools/list error: " + JSON.stringify(payload.error));
|
|
149
153
|
const tools = payload.result?.tools;
|
|
@@ -151,6 +155,8 @@ if (!Array.isArray(tools)) throw new Error("tools/list missing result.tools");
|
|
|
151
155
|
const names = tools.map((item) => item?.name).filter(Boolean);
|
|
152
156
|
const missing = expected.filter((name) => !names.includes(name));
|
|
153
157
|
if (missing.length > 0) throw new Error("missing tools: " + missing.join(","));
|
|
158
|
+
const leaked = names.filter((name) => prdTools.includes(name));
|
|
159
|
+
if (leaked.length > 0) throw new Error("PRD tools must not be exposed: " + leaked.join(","));
|
|
154
160
|
process.stdout.write(JSON.stringify({ tool_count: names.length, tools: names }));
|
|
155
161
|
')"
|
|
156
162
|
|
|
@@ -261,7 +267,7 @@ if [[ -z "${OUTPUT_PATH}" ]]; then
|
|
|
261
267
|
fi
|
|
262
268
|
|
|
263
269
|
COMMIT_HASH="$(git -C "${REPO_ROOT}" rev-parse HEAD)"
|
|
264
|
-
LOCAL_SERVER_SHA256="$(shasum -a 256 "${REPO_ROOT}/
|
|
270
|
+
LOCAL_SERVER_SHA256="$(shasum -a 256 "${REPO_ROOT}/marsnme-supabase/server.mjs" | awk '{print $1}')"
|
|
265
271
|
|
|
266
272
|
if [[ -n "${REMOTE_SERVER_SHA256}" ]]; then
|
|
267
273
|
REMOTE_SERVER_SHA256_VALUE="\"${REMOTE_SERVER_SHA256}\""
|
|
@@ -6,19 +6,19 @@ Requires=docker.service
|
|
|
6
6
|
|
|
7
7
|
[Service]
|
|
8
8
|
Type=simple
|
|
9
|
-
WorkingDirectory=/opt/mars-memory-mcp/current-%i/
|
|
9
|
+
WorkingDirectory=/opt/mars-memory-mcp/current-%i/marsnme-supabase
|
|
10
10
|
EnvironmentFile=-/opt/mars-memory-mcp/shared/.env
|
|
11
11
|
EnvironmentFile=-/opt/mars-memory-mcp/shared/.env.%i
|
|
12
|
-
EnvironmentFile=-/opt/mars-memory-mcp/current-%i/
|
|
13
|
-
EnvironmentFile=-/opt/mars-memory-mcp/current-%i/
|
|
14
|
-
EnvironmentFile=-/opt/mars-memory-mcp/current/
|
|
15
|
-
EnvironmentFile=-/opt/mars-memory-mcp/current/
|
|
12
|
+
EnvironmentFile=-/opt/mars-memory-mcp/current-%i/marsnme-supabase/.env
|
|
13
|
+
EnvironmentFile=-/opt/mars-memory-mcp/current-%i/marsnme-supabase/.env.%i
|
|
14
|
+
EnvironmentFile=-/opt/mars-memory-mcp/current/marsnme-supabase/.env
|
|
15
|
+
EnvironmentFile=-/opt/mars-memory-mcp/current/marsnme-supabase/.env.%i
|
|
16
16
|
Environment=MCP_PROFILE=%i
|
|
17
17
|
Environment=MCP_REQUIRE_BEARER=true
|
|
18
18
|
Environment=JINA_EMBEDDING_API_URL=https://api.jina.ai/v1/embeddings
|
|
19
19
|
Environment=JINA_EMBEDDING_MODEL=jina-embeddings-v3
|
|
20
20
|
Environment=JINA_EMBEDDING_DIMENSIONS=1024
|
|
21
|
-
ExecStart=/usr/bin/node /opt/mars-memory-mcp/current-%i/
|
|
21
|
+
ExecStart=/usr/bin/node /opt/mars-memory-mcp/current-%i/marsnme-supabase/server.mjs
|
|
22
22
|
Restart=always
|
|
23
23
|
RestartSec=3
|
|
24
24
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marsnme/mcp-gateway",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"mcpName": "io.github.Marsmanleo/marsnme",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "Agent-agnostic, LLM-agnostic memory backend for MCP-compatible tools",
|
|
@@ -49,4 +49,4 @@
|
|
|
49
49
|
"engines": {
|
|
50
50
|
"node": ">=20"
|
|
51
51
|
}
|
|
52
|
-
}
|
|
52
|
+
}
|
|
@@ -8,8 +8,8 @@ from typing import Optional
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
ROOT = Path(__file__).resolve().parents[3]
|
|
11
|
-
DREAM_RUNNER = ROOT / "
|
|
12
|
-
HERMES_WRAPPER = ROOT / "
|
|
11
|
+
DREAM_RUNNER = ROOT / "marsnme-supabase" / "scripts" / "dream_runner.py"
|
|
12
|
+
HERMES_WRAPPER = ROOT / "marsnme-supabase" / "scripts" / "hermes_digest_runner.py"
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class DreamRunnerModeTests(unittest.TestCase):
|
package/server.mjs
CHANGED
|
@@ -13,7 +13,7 @@ const PROFILE_CONFIGS = {
|
|
|
13
13
|
defaultPort: 18790,
|
|
14
14
|
gatewayDir: 'coco-mcp-gateway',
|
|
15
15
|
publicHostSuffix: 'coco-mcp.marsgroup.asia',
|
|
16
|
-
sourceWhitelist: ['perplexity', 'cursor', 'warp', 'openclaw', 'hermes'],
|
|
16
|
+
sourceWhitelist: ['perplexity', 'cursor', 'warp', 'openclaw', 'hermes', 'draft', 'grok'],
|
|
17
17
|
recallBodyEnum: ['coco', 'toto', 'system'],
|
|
18
18
|
digestDefaultOrigin: 'hermes-coco-digest',
|
|
19
19
|
memoryIngestToolName: 'memory_ingest',
|
|
@@ -35,7 +35,7 @@ const PROFILE_CONFIGS = {
|
|
|
35
35
|
defaultPort: 18791,
|
|
36
36
|
gatewayDir: 'toto-mcp-gateway',
|
|
37
37
|
publicHostSuffix: 'toto-mcp.marsgroup.asia',
|
|
38
|
-
sourceWhitelist: ['perplexity', 'cursor', 'warp', 'openclaw'],
|
|
38
|
+
sourceWhitelist: ['perplexity', 'cursor', 'warp', 'openclaw', 'grok'],
|
|
39
39
|
recallBodyEnum: ['toto', 'system'],
|
|
40
40
|
digestDefaultOrigin: 'hermes-toto-digest',
|
|
41
41
|
memoryIngestToolName: 'memory_ingest',
|
|
@@ -190,9 +190,13 @@ const ANON_ALLOWED_TOOL_NAMES = new Set([
|
|
|
190
190
|
'search_memories',
|
|
191
191
|
'reload_source_registry',
|
|
192
192
|
'recall',
|
|
193
|
+
'get_summary',
|
|
194
|
+
'get_full',
|
|
193
195
|
'health_check',
|
|
194
196
|
'explain_memory'
|
|
195
197
|
]);
|
|
198
|
+
const RECALL_PREVIEW_MAX_CHARS = 80;
|
|
199
|
+
const CHUNK_SUMMARY_MAX_CHARS = 300;
|
|
196
200
|
const SUPABASE_REQUEST_CONTEXT = new AsyncLocalStorage();
|
|
197
201
|
const OAUTH_ENABLED = process.env.MCP_OAUTH_ENABLED !== 'false';
|
|
198
202
|
const REQUIRE_BEARER = process.env.MCP_REQUIRE_BEARER === 'true';
|
|
@@ -256,7 +260,8 @@ const SOURCE_TO_AGENT_BODY_MAP = new Map([
|
|
|
256
260
|
['cursor', 'cursor'],
|
|
257
261
|
['warp', 'warp'],
|
|
258
262
|
['openclaw', 'desktop'],
|
|
259
|
-
['hermes', 'desktop']
|
|
263
|
+
['hermes', 'desktop'],
|
|
264
|
+
['draft', 'desktop']
|
|
260
265
|
]);
|
|
261
266
|
const DEFAULT_AGENT_BODY = String(process.env.MCP_AGENT_BODY || '')
|
|
262
267
|
.trim()
|
|
@@ -307,29 +312,29 @@ function buildTools() {
|
|
|
307
312
|
return [
|
|
308
313
|
{
|
|
309
314
|
name: 'insert_memory',
|
|
310
|
-
description: `
|
|
315
|
+
description: `Store a short-term ${PROFILE.displayName} memory into ${DB_PROFILE}.memories (ephemeral context that auto-expires). Use this to capture observations, decisions, session notes, or any context worth remembering temporarily. Memories last 7 days by default unless expires_at is set. For permanent knowledge, use memory_ingest instead.`,
|
|
311
316
|
inputSchema: {
|
|
312
317
|
type: 'object',
|
|
313
318
|
properties: {
|
|
314
|
-
body: { type: 'string', description: '
|
|
319
|
+
body: { type: 'string', description: 'The memory content to store. Be specific and include relevant context — this text is used for semantic search later.' },
|
|
315
320
|
source: buildMemorySourceSchema(),
|
|
316
|
-
session_id: { type: 'string', description: '
|
|
321
|
+
session_id: { type: 'string', description: 'Unique identifier for the current session/conversation. Used to group related memories together.' },
|
|
317
322
|
tags: {
|
|
318
323
|
type: 'array',
|
|
319
324
|
items: { type: 'string' },
|
|
320
|
-
description: '
|
|
325
|
+
description: 'Categorization labels for filtering (e.g. ["decision", "architecture", "bugfix"])'
|
|
321
326
|
},
|
|
322
327
|
expires_at: {
|
|
323
328
|
type: 'string',
|
|
324
|
-
description: '
|
|
329
|
+
description: 'Custom expiration time in ISO 8601 format (e.g. "2026-06-17T00:00:00Z"). Defaults to 7 days from now if omitted.'
|
|
325
330
|
},
|
|
326
331
|
agent_body: {
|
|
327
332
|
type: 'string',
|
|
328
|
-
description: '
|
|
333
|
+
description: 'Which persona/agent body this memory belongs to (e.g. "coco", "toto"). Defaults to the profile of this server.'
|
|
329
334
|
},
|
|
330
335
|
environment: {
|
|
331
336
|
type: 'string',
|
|
332
|
-
description: '
|
|
337
|
+
description: 'Deployment environment label (e.g. "production", "staging"). Defaults to MCP_ENVIRONMENT if set.'
|
|
333
338
|
}
|
|
334
339
|
},
|
|
335
340
|
required: ['body', 'source', 'session_id'],
|
|
@@ -338,36 +343,36 @@ function buildTools() {
|
|
|
338
343
|
},
|
|
339
344
|
{
|
|
340
345
|
name: 'list_memories',
|
|
341
|
-
description: `List recent memories from ${DB_PROFILE}.memories
|
|
346
|
+
description: `List recent short-term memories from ${DB_PROFILE}.memories in reverse chronological order — like a daily log or activity feed. Use this to review what was recently captured, check for duplicate memories before inserting, or browse recent activity. This is a time-ordered listing tool, not a search tool. For semantic search by meaning, use search_memories instead. For long-term knowledge retrieval, use recall. Returns memory body, source, creation time, and expiration status.`,
|
|
342
347
|
inputSchema: {
|
|
343
348
|
type: 'object',
|
|
344
349
|
properties: {
|
|
345
|
-
limit: { type: 'number', minimum: 1, maximum: 100, default: 20 },
|
|
350
|
+
limit: { type: 'number', minimum: 1, maximum: 100, default: 20, description: 'Maximum number of memories to return (default 20)' },
|
|
346
351
|
source: buildMemorySourceSchema(),
|
|
347
|
-
unexpired_only: { type: 'boolean', default: true }
|
|
352
|
+
unexpired_only: { type: 'boolean', default: true, description: 'When true (default), only return memories that have not yet expired. Set false to include expired entries.' }
|
|
348
353
|
},
|
|
349
354
|
additionalProperties: false
|
|
350
355
|
}
|
|
351
356
|
},
|
|
352
357
|
{
|
|
353
358
|
name: 'search_memories',
|
|
354
|
-
description: `Semantic search memories
|
|
359
|
+
description: `Semantic search across short-term memories in ${DB_PROFILE}.memories using Jina embeddings. Finds memories by meaning, not just keywords — ask a natural language question and get the most relevant matches. Use this to recall past decisions, find related context, or check if something was already discussed recently.`,
|
|
355
360
|
inputSchema: {
|
|
356
361
|
type: 'object',
|
|
357
362
|
properties: {
|
|
358
|
-
query: { type: 'string', description: '
|
|
359
|
-
limit: { type: 'number', minimum: 1, maximum: 100, default: 20 },
|
|
363
|
+
query: { type: 'string', description: 'Natural language search query. Describe what you are looking for — semantic matching finds relevant results even without exact keywords.' },
|
|
364
|
+
limit: { type: 'number', minimum: 1, maximum: 100, default: 20, description: 'Maximum number of results to return' },
|
|
360
365
|
source: buildMemorySourceSchema(),
|
|
361
|
-
unexpired_only: { type: 'boolean', default: true },
|
|
362
|
-
min_similarity: { type: 'number', minimum: -1, maximum: 1 },
|
|
363
|
-
scope: { type: 'string', enum: ['this_body', 'all_bodies'], default: 'this_body' },
|
|
366
|
+
unexpired_only: { type: 'boolean', default: true, description: 'When true (default), exclude expired memories from results' },
|
|
367
|
+
min_similarity: { type: 'number', minimum: -1, maximum: 1, description: 'Minimum cosine similarity threshold for results (range -1 to 1). Higher values return fewer but more relevant matches.' },
|
|
368
|
+
scope: { type: 'string', enum: ['this_body', 'all_bodies'], default: 'this_body', description: 'Search scope: "this_body" searches only the current profile, "all_bodies" searches across all personas.' },
|
|
364
369
|
agent_body: {
|
|
365
370
|
type: 'string',
|
|
366
|
-
description: '
|
|
371
|
+
description: 'Filter results to a specific persona/body (e.g. "coco", "toto")'
|
|
367
372
|
},
|
|
368
373
|
environment: {
|
|
369
374
|
type: 'string',
|
|
370
|
-
description: '
|
|
375
|
+
description: 'Filter results to a specific environment (e.g. "production", "staging")'
|
|
371
376
|
}
|
|
372
377
|
},
|
|
373
378
|
required: ['query'],
|
|
@@ -377,7 +382,7 @@ function buildTools() {
|
|
|
377
382
|
{
|
|
378
383
|
name: 'reload_source_registry',
|
|
379
384
|
description:
|
|
380
|
-
`Reload
|
|
385
|
+
`Reload the source registry cache from ${DB_PROFILE}.${SOURCE_REGISTRY_TABLE}. Use this after adding or removing entries to refresh which sources are enabled for insert/search filtering. Only effective when running in registry mode (MCP_SOURCE_MODE=registry).`,
|
|
381
386
|
inputSchema: {
|
|
382
387
|
type: 'object',
|
|
383
388
|
properties: {},
|
|
@@ -386,40 +391,64 @@ function buildTools() {
|
|
|
386
391
|
},
|
|
387
392
|
{
|
|
388
393
|
name: 'recall',
|
|
389
|
-
description: `Semantic recall from ${DB_PROFILE}.marsvault_chunks using Jina embeddings
|
|
394
|
+
description: `Semantic recall from long-term memory (${DB_PROFILE}.marsvault_chunks) using Jina embeddings. Returns preview snippets (~${RECALL_PREVIEW_MAX_CHARS} chars) per match by default — use get_summary for a longer excerpt or get_full for complete chunk text. Searches promoted insights, digests, and archived knowledge using vector similarity.`,
|
|
390
395
|
inputSchema: {
|
|
391
396
|
type: 'object',
|
|
392
397
|
properties: {
|
|
393
|
-
query: { type: 'string', description: '
|
|
394
|
-
limit: { type: 'number', minimum: 1, maximum: 50, default: 5 },
|
|
395
|
-
body: { type: 'string', enum: PROFILE.recallBodyEnum, default: DB_PROFILE },
|
|
396
|
-
include_global: { type: 'boolean', default: true },
|
|
397
|
-
include_shared: { type: 'boolean', default: true },
|
|
398
|
-
include_private: { type: 'boolean', default: true },
|
|
399
|
-
type: { type: 'string', description: '
|
|
400
|
-
min_similarity: { type: 'number', minimum: -1, maximum: 1 },
|
|
401
|
-
scope: { type: 'string', enum: ['this_body', 'all_bodies'], default: 'this_body' },
|
|
398
|
+
query: { type: 'string', description: 'Natural language query describing what knowledge you need. The system finds semantically similar chunks — describe the concept, not just keywords.' },
|
|
399
|
+
limit: { type: 'number', minimum: 1, maximum: 50, default: 5, description: 'Maximum number of chunks to return (default 5)' },
|
|
400
|
+
body: { type: 'string', enum: PROFILE.recallBodyEnum, default: DB_PROFILE, description: 'Which persona profile to search in (e.g. "coco", "toto", "system")' },
|
|
401
|
+
include_global: { type: 'boolean', default: true, description: 'Include globally visible chunks in results' },
|
|
402
|
+
include_shared: { type: 'boolean', default: true, description: 'Include shared-visibility chunks in results' },
|
|
403
|
+
include_private: { type: 'boolean', default: true, description: 'Include private chunks in results' },
|
|
404
|
+
type: { type: 'string', description: 'Filter by chunk type (e.g. "insight", "digest", "observation")' },
|
|
405
|
+
min_similarity: { type: 'number', minimum: -1, maximum: 1, description: 'Minimum cosine similarity threshold. Raise for higher precision, lower for broader recall.' },
|
|
406
|
+
scope: { type: 'string', enum: ['this_body', 'all_bodies'], default: 'this_body', description: 'Search scope: "this_body" for current profile only, "all_bodies" for cross-persona search' },
|
|
402
407
|
agent_body: {
|
|
403
408
|
type: 'string',
|
|
404
|
-
description: '
|
|
409
|
+
description: 'Filter to a specific persona/body scope'
|
|
405
410
|
},
|
|
406
411
|
environment: {
|
|
407
412
|
type: 'string',
|
|
408
|
-
description: '
|
|
413
|
+
description: 'Filter to a specific environment label'
|
|
409
414
|
},
|
|
410
415
|
debug_explain: {
|
|
411
416
|
type: 'boolean',
|
|
412
417
|
default: false,
|
|
413
|
-
description: '
|
|
418
|
+
description: 'When true, include token overlap details in each result for debugging relevance'
|
|
414
419
|
}
|
|
415
420
|
},
|
|
416
421
|
required: ['query'],
|
|
417
422
|
additionalProperties: false
|
|
418
423
|
}
|
|
419
424
|
},
|
|
425
|
+
{
|
|
426
|
+
name: 'get_summary',
|
|
427
|
+
description: `Fetch a medium-length excerpt (~${CHUNK_SUMMARY_MAX_CHARS} chars) of a long-term memory chunk by ID. Use after recall when a preview match looks relevant. For complete text, call get_full.`,
|
|
428
|
+
inputSchema: {
|
|
429
|
+
type: 'object',
|
|
430
|
+
properties: {
|
|
431
|
+
id: { type: 'string', description: 'UUID of the marsvault_chunks row' }
|
|
432
|
+
},
|
|
433
|
+
required: ['id'],
|
|
434
|
+
additionalProperties: false
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
name: 'get_full',
|
|
439
|
+
description: `Fetch the complete text of a long-term memory chunk by ID. Use sparingly after recall/get_summary when you need the full institutional memory entry.`,
|
|
440
|
+
inputSchema: {
|
|
441
|
+
type: 'object',
|
|
442
|
+
properties: {
|
|
443
|
+
id: { type: 'string', description: 'UUID of the marsvault_chunks row' }
|
|
444
|
+
},
|
|
445
|
+
required: ['id'],
|
|
446
|
+
additionalProperties: false
|
|
447
|
+
}
|
|
448
|
+
},
|
|
420
449
|
{
|
|
421
450
|
name: 'health_check',
|
|
422
|
-
description: `Run ${PROFILE.displayName} memory
|
|
451
|
+
description: `Run comprehensive ${PROFILE.displayName} memory system diagnostics. Returns chunk counts by type and visibility, identifies expiring short-term memories that need promotion, detects timeline coverage gaps, and finds conflicting or redundant long-term chunks. Use this regularly to maintain memory hygiene and before batch promotion.`,
|
|
423
452
|
inputSchema: {
|
|
424
453
|
type: 'object',
|
|
425
454
|
properties: {
|
|
@@ -514,11 +543,11 @@ function buildTools() {
|
|
|
514
543
|
{
|
|
515
544
|
name: 'session_boot',
|
|
516
545
|
description:
|
|
517
|
-
|
|
546
|
+
`Initialize a new ${PROFILE.displayName} session by loading identity, workflow context, and recent status from long-term memory. Also runs an expiry-focused health snapshot and records a heartbeat sign-in. Call this once at the start of every new conversation to restore continuity and awareness of pending work.`,
|
|
518
547
|
inputSchema: {
|
|
519
548
|
type: 'object',
|
|
520
549
|
properties: {
|
|
521
|
-
source: { type: 'string', enum: PROFILE.sourceWhitelist },
|
|
550
|
+
source: { type: 'string', enum: PROFILE.sourceWhitelist, description: 'Source tool identifier (e.g. "warp", "cursor", "perplexity") for provenance tracking' },
|
|
522
551
|
body_name: {
|
|
523
552
|
type: 'string',
|
|
524
553
|
description: 'Optional body/persona name (例如:大家姐、三哥、五妹、Toto)'
|
|
@@ -551,7 +580,12 @@ function buildTools() {
|
|
|
551
580
|
type: 'number',
|
|
552
581
|
minimum: 1,
|
|
553
582
|
maximum: 10,
|
|
554
|
-
default: 5
|
|
583
|
+
default: 5,
|
|
584
|
+
description: 'Maximum recall results per category (identity/workflow/status, default 5)'
|
|
585
|
+
},
|
|
586
|
+
body: {
|
|
587
|
+
type: 'string',
|
|
588
|
+
description: 'Recipient body name — check for 便條 (handoff notes) addressed to this body and surface them at the top of boot output. Defaults to current profile (e.g. "coco") so any CoCo body receives CoCo-addressed notes without passing an explicit body.'
|
|
555
589
|
},
|
|
556
590
|
alert_window_hours: {
|
|
557
591
|
type: 'number',
|
|
@@ -579,7 +613,7 @@ function buildTools() {
|
|
|
579
613
|
{
|
|
580
614
|
name: 'session_close',
|
|
581
615
|
description:
|
|
582
|
-
|
|
616
|
+
`Save a ${PROFILE.displayName} session close summary before ending a conversation. The summary is stored as a short-term memory with 7-day retention, providing context for the next session boot. After saving, automatically promotes up to 5 soon-expiring short-term memories to long-term storage (alert window 48h); promote failures never fail the close. Include what was accomplished, what is still pending, and any important context for continuity.`,
|
|
583
617
|
inputSchema: {
|
|
584
618
|
type: 'object',
|
|
585
619
|
properties: {
|
|
@@ -596,6 +630,14 @@ function buildTools() {
|
|
|
596
630
|
mood: {
|
|
597
631
|
type: 'string',
|
|
598
632
|
description: 'Optional mood marker'
|
|
633
|
+
},
|
|
634
|
+
to: {
|
|
635
|
+
type: 'string',
|
|
636
|
+
description: 'Recipient body name — leave a 便條 (handoff note) for another body, surfaced on that body next session_boot'
|
|
637
|
+
},
|
|
638
|
+
note: {
|
|
639
|
+
type: 'string',
|
|
640
|
+
description: 'Short context handoff for the recipient body (used with `to`)'
|
|
599
641
|
}
|
|
600
642
|
},
|
|
601
643
|
required: ['source', 'summary'],
|
|
@@ -604,24 +646,24 @@ function buildTools() {
|
|
|
604
646
|
},
|
|
605
647
|
{
|
|
606
648
|
name: 'dream_ingest',
|
|
607
|
-
description: `
|
|
649
|
+
description: `Ingest a Hermes digest (periodic summary report) into ${DB_PROFILE}.marsvault_chunks long-term memory. The content is automatically chunked into semantically meaningful segments and stored with vector embeddings for future recall. Use this for scheduled reports, daily digests, or any structured summary content that should be permanently available.`,
|
|
608
650
|
inputSchema: {
|
|
609
651
|
type: 'object',
|
|
610
652
|
properties: {
|
|
611
|
-
content: { type: 'string', description: '
|
|
612
|
-
source_file: { type: 'string', description: 'Logical
|
|
613
|
-
section: { type: 'string', description: '
|
|
653
|
+
content: { type: 'string', description: 'The full text content of the digest to ingest. Will be automatically split into semantically coherent chunks.' },
|
|
654
|
+
source_file: { type: 'string', description: 'Logical file path or identifier for the source of this digest (e.g. "hermes/daily/2026-06-10")' },
|
|
655
|
+
section: { type: 'string', description: 'Optional section label prefix to tag all resulting chunks' },
|
|
614
656
|
tags: {
|
|
615
657
|
type: 'array',
|
|
616
658
|
items: { type: 'string' },
|
|
617
|
-
description: '
|
|
659
|
+
description: 'Tags for categorization (e.g. ["digest", "daily", "report"])'
|
|
618
660
|
},
|
|
619
|
-
type: { type: 'string', description: 'Chunk type', default: 'digest' },
|
|
620
|
-
date: { type: 'string', description: '
|
|
621
|
-
body: { type: 'string', enum: PROFILE.recallBodyEnum, default: DB_PROFILE },
|
|
622
|
-
visibility: { type: 'string', enum: ['private', 'shared', 'global'], default: 'private' },
|
|
623
|
-
origin: { type: 'string', description: 'Origin marker', default: PROFILE.digestDefaultOrigin },
|
|
624
|
-
max_chunk_chars: { type: 'number', minimum: 300, maximum: 3000, default: 1200 }
|
|
661
|
+
type: { type: 'string', description: 'Chunk type label', default: 'digest' },
|
|
662
|
+
date: { type: 'string', description: 'Date for this digest in YYYY-MM-DD format. Defaults to today if omitted.' },
|
|
663
|
+
body: { type: 'string', enum: PROFILE.recallBodyEnum, default: DB_PROFILE, description: 'Target persona profile for storage' },
|
|
664
|
+
visibility: { type: 'string', enum: ['private', 'shared', 'global'], default: 'private', description: 'Access level: "private" = owner only, "shared" = cross-profile, "global" = system-wide' },
|
|
665
|
+
origin: { type: 'string', description: 'Origin marker identifying where this content came from', default: PROFILE.digestDefaultOrigin },
|
|
666
|
+
max_chunk_chars: { type: 'number', minimum: 300, maximum: 3000, default: 1200, description: 'Maximum character length per chunk (default 1200). Larger values = fewer, longer chunks.' }
|
|
625
667
|
},
|
|
626
668
|
required: ['content'],
|
|
627
669
|
additionalProperties: false
|
|
@@ -629,48 +671,48 @@ function buildTools() {
|
|
|
629
671
|
},
|
|
630
672
|
{
|
|
631
673
|
name: PROFILE.memoryIngestToolName,
|
|
632
|
-
description: `
|
|
674
|
+
description: `Promote content into permanent long-term memory (${DB_PROFILE}.marsvault_chunks). Automatically chunks the input text, generates vector embeddings, and stores each segment for semantic recall. Use this to preserve important insights, decisions, patterns, or knowledge that should survive beyond the current session. This is the primary path from ephemeral to permanent memory.`,
|
|
633
675
|
inputSchema: {
|
|
634
676
|
type: 'object',
|
|
635
677
|
properties: {
|
|
636
|
-
content: { type: 'string', description: '
|
|
637
|
-
source_file: { type: 'string', description: 'Logical
|
|
638
|
-
section: { type: 'string', description: '
|
|
678
|
+
content: { type: 'string', description: 'The insight content to promote to long-term memory. Be specific and self-contained — future recall depends on the quality of this text.' },
|
|
679
|
+
source_file: { type: 'string', description: 'Logical file path or identifier for the source (e.g. "sessions/2026-06-10-session-notes")' },
|
|
680
|
+
section: { type: 'string', description: 'Optional section label prefix to organize chunks within the source' },
|
|
639
681
|
tags: {
|
|
640
682
|
type: 'array',
|
|
641
683
|
items: { type: 'string' },
|
|
642
|
-
description: '
|
|
684
|
+
description: 'Categorization tags (e.g. ["decision", "architecture"])'
|
|
643
685
|
},
|
|
644
|
-
type: { type: 'string', description: '
|
|
645
|
-
date: { type: 'string', description: '
|
|
646
|
-
visibility: { type: 'string', enum: ['private', 'shared', 'global'], default: 'private' },
|
|
686
|
+
type: { type: 'string', description: 'Content type label (e.g. "insight", "observation", "decision")', default: 'insight' },
|
|
687
|
+
date: { type: 'string', description: 'Date for this content in YYYY-MM-DD format. Defaults to today if omitted.' },
|
|
688
|
+
visibility: { type: 'string', enum: ['private', 'shared', 'global'], default: 'private', description: 'Access level: "private" = this profile only, "shared" = cross-profile readable, "global" = system-wide' },
|
|
647
689
|
origin: buildMemoryIngestOriginSchema(),
|
|
648
690
|
source_memory_id: {
|
|
649
691
|
type: 'string',
|
|
650
|
-
description: '
|
|
692
|
+
description: 'Link this promotion to a specific short-term memory ID (for traceability)'
|
|
651
693
|
},
|
|
652
694
|
source_session_id: {
|
|
653
695
|
type: 'string',
|
|
654
|
-
description: '
|
|
696
|
+
description: 'Session ID where this insight originated (for provenance tracking)'
|
|
655
697
|
},
|
|
656
698
|
source_tool: {
|
|
657
699
|
type: 'string',
|
|
658
700
|
enum: PROFILE.sourceWhitelist,
|
|
659
|
-
description: '
|
|
701
|
+
description: 'The tool/platform where this insight was originally captured'
|
|
660
702
|
},
|
|
661
703
|
source_user_note: {
|
|
662
704
|
type: 'string',
|
|
663
|
-
description: '
|
|
705
|
+
description: 'Brief note explaining why this memory was selected for promotion'
|
|
664
706
|
},
|
|
665
707
|
agent_body: {
|
|
666
708
|
type: 'string',
|
|
667
|
-
description: '
|
|
709
|
+
description: 'The persona/body this memory belongs to (e.g. "coco", "toto")'
|
|
668
710
|
},
|
|
669
711
|
environment: {
|
|
670
712
|
type: 'string',
|
|
671
|
-
description: '
|
|
713
|
+
description: 'Environment label (e.g. "production", "staging")'
|
|
672
714
|
},
|
|
673
|
-
max_chunk_chars: { type: 'number', minimum: 300, maximum: 3000, default: 1200 }
|
|
715
|
+
max_chunk_chars: { type: 'number', minimum: 300, maximum: 3000, default: 1200, description: 'Maximum characters per chunk (default 1200). Adjust for finer or coarser granularity.' }
|
|
674
716
|
},
|
|
675
717
|
required: ['content'],
|
|
676
718
|
additionalProperties: false
|
|
@@ -678,19 +720,19 @@ function buildTools() {
|
|
|
678
720
|
},
|
|
679
721
|
{
|
|
680
722
|
name: 'demote_memory',
|
|
681
|
-
description: `
|
|
723
|
+
description: `Deprecate a long-term memory chunk in ${DB_PROFILE}.marsvault_chunks, marking it as outdated or superseded. The chunk is not deleted — it is flagged so it no longer appears in recall results. Use this when information becomes incorrect, irrelevant, or is replaced by newer knowledge. Optionally link to the replacement chunk for traceability.`,
|
|
682
724
|
inputSchema: {
|
|
683
725
|
type: 'object',
|
|
684
726
|
properties: {
|
|
685
|
-
id: { type: 'string', description: '
|
|
686
|
-
deprecated_reason: { type: 'string', description: '
|
|
727
|
+
id: { type: 'string', description: 'UUID of the long-term memory chunk to deprecate' },
|
|
728
|
+
deprecated_reason: { type: 'string', description: 'Why this memory is being deprecated (e.g. "superseded by newer architecture decision", "information confirmed incorrect")' },
|
|
687
729
|
superseded_by: {
|
|
688
730
|
type: 'string',
|
|
689
|
-
description: '
|
|
731
|
+
description: 'UUID of the replacement chunk, if this memory is being superseded by updated content'
|
|
690
732
|
},
|
|
691
733
|
deprecated_at: {
|
|
692
734
|
type: 'string',
|
|
693
|
-
description: '
|
|
735
|
+
description: 'Custom deprecation timestamp in ISO 8601 format. Defaults to now if omitted.'
|
|
694
736
|
}
|
|
695
737
|
},
|
|
696
738
|
required: ['id', 'deprecated_reason'],
|
|
@@ -699,7 +741,7 @@ function buildTools() {
|
|
|
699
741
|
},
|
|
700
742
|
{
|
|
701
743
|
name: 'soft_forget',
|
|
702
|
-
description: `Expire short-
|
|
744
|
+
description: `Expire short-term memories in ${DB_PROFILE}.memories early without permanently deleting them. Marked memories become invisible to search and list operations but remain in the database for audit purposes. Use this to clean up irrelevant or incorrect short-term memories before their natural expiration.`,
|
|
703
745
|
inputSchema: {
|
|
704
746
|
type: 'object',
|
|
705
747
|
properties: {
|
|
@@ -708,15 +750,15 @@ function buildTools() {
|
|
|
708
750
|
items: { type: 'string' },
|
|
709
751
|
minItems: 1,
|
|
710
752
|
maxItems: 50,
|
|
711
|
-
description: '
|
|
753
|
+
description: 'Array of short-term memory UUIDs to expire. Up to 50 at a time.'
|
|
712
754
|
},
|
|
713
755
|
reason: {
|
|
714
756
|
type: 'string',
|
|
715
|
-
description: '
|
|
757
|
+
description: 'Brief explanation of why these memories are being forgotten (for audit trail)'
|
|
716
758
|
},
|
|
717
759
|
forgotten_at: {
|
|
718
760
|
type: 'string',
|
|
719
|
-
description: '
|
|
761
|
+
description: 'Custom expiration timestamp in ISO 8601 format. Defaults to now if omitted.'
|
|
720
762
|
}
|
|
721
763
|
},
|
|
722
764
|
required: ['ids'],
|
|
@@ -725,11 +767,11 @@ function buildTools() {
|
|
|
725
767
|
},
|
|
726
768
|
{
|
|
727
769
|
name: 'explain_memory',
|
|
728
|
-
description: `
|
|
770
|
+
description: `Trace the full provenance of a long-term memory chunk in ${DB_PROFILE}.marsvault_chunks — where it came from, how it was created, and what source material it was derived from. Returns the original source memory, session, tool, and timeline information. Use this to understand why a piece of knowledge exists or to verify its reliability.`,
|
|
729
771
|
inputSchema: {
|
|
730
772
|
type: 'object',
|
|
731
773
|
properties: {
|
|
732
|
-
id: { type: 'string', description: '
|
|
774
|
+
id: { type: 'string', description: 'UUID of the long-term memory chunk to explain' }
|
|
733
775
|
},
|
|
734
776
|
required: ['id'],
|
|
735
777
|
additionalProperties: false
|
|
@@ -737,7 +779,7 @@ function buildTools() {
|
|
|
737
779
|
},
|
|
738
780
|
{
|
|
739
781
|
name: 'batch_promote',
|
|
740
|
-
description: `
|
|
782
|
+
description: `Automatically promote expiring short-term memories from ${DB_PROFILE}.memories to permanent long-term storage (${DB_PROFILE}.marsvault_chunks). Scans for memories that will expire within the alert window, then chunks and ingests each one with vector embeddings. Use this to prevent valuable context from being lost when short-term memories expire. Supports dry-run mode to preview candidates before committing.`,
|
|
741
783
|
inputSchema: {
|
|
742
784
|
type: 'object',
|
|
743
785
|
properties: {
|
|
@@ -746,29 +788,29 @@ function buildTools() {
|
|
|
746
788
|
minimum: 1,
|
|
747
789
|
maximum: 720,
|
|
748
790
|
default: 48,
|
|
749
|
-
description: 'Look-ahead window
|
|
791
|
+
description: 'Look-ahead window in hours. Memories expiring within this window are candidates for promotion (default 48).'
|
|
750
792
|
},
|
|
751
793
|
max_promote: {
|
|
752
794
|
type: 'number',
|
|
753
795
|
minimum: 1,
|
|
754
796
|
maximum: 50,
|
|
755
797
|
default: 10,
|
|
756
|
-
description: 'Maximum memories to promote in
|
|
798
|
+
description: 'Maximum number of memories to promote in a single batch (default 10).'
|
|
757
799
|
},
|
|
758
800
|
dry_run: {
|
|
759
801
|
type: 'boolean',
|
|
760
802
|
default: false,
|
|
761
|
-
description: '
|
|
803
|
+
description: 'When true, list candidate memories without actually promoting them. Use to preview before committing.'
|
|
762
804
|
},
|
|
763
805
|
memory_ids: {
|
|
764
806
|
type: 'array',
|
|
765
807
|
items: { type: 'string' },
|
|
766
808
|
maxItems: 50,
|
|
767
|
-
description: '
|
|
809
|
+
description: 'Explicit list of short-term memory UUIDs to promote. When provided, auto-detection is skipped and only these IDs are processed.'
|
|
768
810
|
},
|
|
769
811
|
origin: {
|
|
770
812
|
type: 'string',
|
|
771
|
-
description: 'Origin marker
|
|
813
|
+
description: 'Origin marker tagged on all promoted chunks for traceability',
|
|
772
814
|
default: 'batch-promote'
|
|
773
815
|
}
|
|
774
816
|
},
|
|
@@ -1054,6 +1096,8 @@ function listAvailableToolsForAuthMode(authMode) {
|
|
|
1054
1096
|
function buildSupabaseCapabilityPayload() {
|
|
1055
1097
|
return {
|
|
1056
1098
|
mode: SUPABASE_AUTH_MODE,
|
|
1099
|
+
tool_surface: 'coco',
|
|
1100
|
+
prd_tools_enabled: false,
|
|
1057
1101
|
anon_allowed_tools: listAvailableToolNamesForAuthMode('anon'),
|
|
1058
1102
|
effective_tools: listAvailableToolNamesForAuthMode(SUPABASE_AUTH_MODE)
|
|
1059
1103
|
};
|
|
@@ -1381,6 +1425,46 @@ function normalizeOptionalText(value, maxLength = 200) {
|
|
|
1381
1425
|
if (!text) return '';
|
|
1382
1426
|
return text.slice(0, Math.max(1, maxLength));
|
|
1383
1427
|
}
|
|
1428
|
+
|
|
1429
|
+
function truncateChunkContent(text, maxChars) {
|
|
1430
|
+
const raw = String(text ?? '');
|
|
1431
|
+
if (raw.length <= maxChars) {
|
|
1432
|
+
return { content: raw, preview_truncated: false, content_chars: raw.length };
|
|
1433
|
+
}
|
|
1434
|
+
return {
|
|
1435
|
+
content: raw.slice(0, maxChars),
|
|
1436
|
+
preview_truncated: true,
|
|
1437
|
+
content_chars: raw.length
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
function applyRecallPreviewToItem(item) {
|
|
1442
|
+
const fullContent = String(item?.content ?? '');
|
|
1443
|
+
const preview = truncateChunkContent(fullContent, RECALL_PREVIEW_MAX_CHARS);
|
|
1444
|
+
return {
|
|
1445
|
+
...item,
|
|
1446
|
+
content: preview.content,
|
|
1447
|
+
preview_truncated: preview.preview_truncated,
|
|
1448
|
+
content_chars: preview.content_chars
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
function buildChunkToolFields(chunk) {
|
|
1453
|
+
return {
|
|
1454
|
+
id: chunk.id,
|
|
1455
|
+
source_file: chunk.source_file || null,
|
|
1456
|
+
section: chunk.section || null,
|
|
1457
|
+
body: chunk.body || null,
|
|
1458
|
+
visibility: chunk.visibility || null,
|
|
1459
|
+
tags: chunk.tags ?? [],
|
|
1460
|
+
type: chunk.type || null,
|
|
1461
|
+
date: chunk.date || null,
|
|
1462
|
+
origin: chunk.origin || null,
|
|
1463
|
+
agent_body: chunk.agent_body || null,
|
|
1464
|
+
environment: chunk.environment || null,
|
|
1465
|
+
created_at: chunk.created_at || null
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1384
1468
|
function normalizeOptionalSourceTool(value) {
|
|
1385
1469
|
const normalized = String(value || '').trim();
|
|
1386
1470
|
if (!normalized) return '';
|
|
@@ -1517,6 +1601,10 @@ function estimateToolUsageTokens(toolName, args = {}) {
|
|
|
1517
1601
|
appendTokenCharCount(safeArgs.query, state);
|
|
1518
1602
|
appendTokenCharCount(safeArgs.type, state);
|
|
1519
1603
|
break;
|
|
1604
|
+
case 'get_summary':
|
|
1605
|
+
case 'get_full':
|
|
1606
|
+
appendTokenCharCount(safeArgs.id, state);
|
|
1607
|
+
break;
|
|
1520
1608
|
case 'session_close':
|
|
1521
1609
|
appendTokenCharCount(safeArgs.summary, state);
|
|
1522
1610
|
appendTokenCharCount(safeArgs.topics, state);
|
|
@@ -2088,6 +2176,64 @@ async function upsertMemoryBySession({
|
|
|
2088
2176
|
return fetched || null;
|
|
2089
2177
|
}
|
|
2090
2178
|
|
|
2179
|
+
// ─── 便條 (Body-to-Body handoff notes) ────────────────────────────────────────
|
|
2180
|
+
// Stored as memories rows with recipient_body/note/read_at columns.
|
|
2181
|
+
// session_close(to, note) writes; session_boot(body) reads + marks read.
|
|
2182
|
+
// If the note columns are not yet deployed, these degrade gracefully (caller wraps in try/catch).
|
|
2183
|
+
|
|
2184
|
+
const NOTE_SELECT_COLUMNS =
|
|
2185
|
+
'id,body,source,session_id,tags,agent_body,environment,created_at,expires_at,recipient_body,note,read_at';
|
|
2186
|
+
|
|
2187
|
+
async function insertNoteMemory({ source, to, note, body, tags, expires_at }) {
|
|
2188
|
+
const resolvedAgentBody = resolveWriteAgentBody({ source });
|
|
2189
|
+
const resolvedEnvironment = resolveWriteEnvironment({});
|
|
2190
|
+
const createdId = crypto.randomUUID();
|
|
2191
|
+
const payload = {
|
|
2192
|
+
id: createdId,
|
|
2193
|
+
body,
|
|
2194
|
+
source,
|
|
2195
|
+
session_id: `note:to:${to}:${createdId.slice(0, 8)}`,
|
|
2196
|
+
tags: normalizeMemoryTagsWithContext(tags, resolvedAgentBody, resolvedEnvironment),
|
|
2197
|
+
agent_body: resolvedAgentBody,
|
|
2198
|
+
environment: resolvedEnvironment,
|
|
2199
|
+
recipient_body: to,
|
|
2200
|
+
note,
|
|
2201
|
+
read_at: null
|
|
2202
|
+
};
|
|
2203
|
+
if (expires_at) payload.expires_at = expires_at;
|
|
2204
|
+
const inserted = await supabaseRequest(`/rest/v1/memories?select=${NOTE_SELECT_COLUMNS}`, {
|
|
2205
|
+
method: 'POST',
|
|
2206
|
+
profile: DB_PROFILE,
|
|
2207
|
+
prefer: 'return=representation',
|
|
2208
|
+
body: [payload]
|
|
2209
|
+
});
|
|
2210
|
+
return Array.isArray(inserted) && inserted.length > 0 ? inserted[0] : null;
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
async function listUnreadNotes(bodyName, limit = 3) {
|
|
2214
|
+
const query = new URLSearchParams();
|
|
2215
|
+
query.set('select', NOTE_SELECT_COLUMNS);
|
|
2216
|
+
query.set('recipient_body', `eq.${bodyName}`);
|
|
2217
|
+
query.set('read_at', 'is.null');
|
|
2218
|
+
query.set('order', 'created_at.desc');
|
|
2219
|
+
query.set('limit', String(Math.min(limit, 10)));
|
|
2220
|
+
const rows = await supabaseRequest(`/rest/v1/memories?${query.toString()}`, {
|
|
2221
|
+
profile: DB_PROFILE
|
|
2222
|
+
});
|
|
2223
|
+
return Array.isArray(rows) ? rows : [];
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
async function markNoteRead(id) {
|
|
2227
|
+
const query = new URLSearchParams();
|
|
2228
|
+
query.set('id', `eq.${id}`);
|
|
2229
|
+
await supabaseRequest(`/rest/v1/memories?${query.toString()}`, {
|
|
2230
|
+
method: 'PATCH',
|
|
2231
|
+
profile: DB_PROFILE,
|
|
2232
|
+
prefer: 'return=minimal',
|
|
2233
|
+
body: { read_at: new Date().toISOString() }
|
|
2234
|
+
});
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2091
2237
|
async function ensureDailyCloseRecovery(source, nowTs) {
|
|
2092
2238
|
const yesterdayDate = new Date(toUtcStartOfDay(nowTs).getTime() - 86400000);
|
|
2093
2239
|
const yesterdayDateKey = formatDateOnly(yesterdayDate);
|
|
@@ -2179,6 +2325,7 @@ async function runDailyBoot(args = {}) {
|
|
|
2179
2325
|
const source = normalizeDailySource(args.source);
|
|
2180
2326
|
const topic = normalizeOptionalText(args.topic, 200);
|
|
2181
2327
|
const mood = normalizeOptionalText(args.mood, 80);
|
|
2328
|
+
const bodyName = normalizeOptionalText(args.body, 60) || DB_PROFILE;
|
|
2182
2329
|
const queries = buildDailyBootQueries(args);
|
|
2183
2330
|
const inferredSourceAgentBody = inferAgentBodyFromSource(source);
|
|
2184
2331
|
const recallScopeArgs = {
|
|
@@ -2272,12 +2419,34 @@ async function runDailyBoot(args = {}) {
|
|
|
2272
2419
|
}
|
|
2273
2420
|
: await ensureDailyCloseRecovery(source, nowTs);
|
|
2274
2421
|
|
|
2422
|
+
// 便條: surface unread handoff notes addressed to this body, then mark read
|
|
2423
|
+
let notes = [];
|
|
2424
|
+
if (bodyName) {
|
|
2425
|
+
try {
|
|
2426
|
+
const unread = await listUnreadNotes(bodyName);
|
|
2427
|
+
for (const n of unread) {
|
|
2428
|
+
notes.push({
|
|
2429
|
+
id: n.id,
|
|
2430
|
+
from: normalizeOptionalText(n.source, 60) || 'unknown',
|
|
2431
|
+
to: bodyName,
|
|
2432
|
+
note: normalizeOptionalText(n.note, 1000) || '',
|
|
2433
|
+
created_at: n.created_at
|
|
2434
|
+
});
|
|
2435
|
+
await markNoteRead(n.id);
|
|
2436
|
+
}
|
|
2437
|
+
} catch (error) {
|
|
2438
|
+
// note columns may not be deployed yet — degrade gracefully, boot itself still succeeds
|
|
2439
|
+
notes = [];
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2275
2443
|
return {
|
|
2276
2444
|
ok: true,
|
|
2277
2445
|
profile: DB_PROFILE,
|
|
2278
2446
|
mode: existingHeartbeat ? 'welcome_back' : 'new_day',
|
|
2279
2447
|
source,
|
|
2280
2448
|
date: dateKey,
|
|
2449
|
+
notes,
|
|
2281
2450
|
heartbeat_id: savedHeartbeat?.id || null,
|
|
2282
2451
|
agent_name: queries.agent_name,
|
|
2283
2452
|
last_topic: previousTopic || null,
|
|
@@ -2312,6 +2481,8 @@ async function runDailyClose(args = {}) {
|
|
|
2312
2481
|
}
|
|
2313
2482
|
const topics = normalizeDailyTopics(args.topics);
|
|
2314
2483
|
const mood = normalizeOptionalText(args.mood, 80);
|
|
2484
|
+
const to = normalizeOptionalText(args.to, 60);
|
|
2485
|
+
const note = to ? normalizeOptionalText(args.note, 1000) : null;
|
|
2315
2486
|
const nowTs = new Date();
|
|
2316
2487
|
const dateKey = dailyDateKey(nowTs);
|
|
2317
2488
|
const toolUsageDailySummary = await fetchToolUsageDailySummary(dateKey);
|
|
@@ -2343,6 +2514,23 @@ async function runDailyClose(args = {}) {
|
|
|
2343
2514
|
expires_at: expiresAt,
|
|
2344
2515
|
existing: existingClose
|
|
2345
2516
|
});
|
|
2517
|
+
|
|
2518
|
+
// Auto-promote soon-expiring short-term memories (replaces Hermes dream schedule).
|
|
2519
|
+
// Failure must never break session_close — summary is already stored.
|
|
2520
|
+
let promotedCount = 0;
|
|
2521
|
+
try {
|
|
2522
|
+
const promoteResult = await runBatchPromote({
|
|
2523
|
+
alert_window_hours: 48,
|
|
2524
|
+
max_promote: 5
|
|
2525
|
+
});
|
|
2526
|
+
promotedCount = Math.max(0, Number(promoteResult?.promoted_count) || 0);
|
|
2527
|
+
} catch (error) {
|
|
2528
|
+
console.warn(
|
|
2529
|
+
`[session_close] auto batch_promote failed: ${String(error?.message || error)}`
|
|
2530
|
+
);
|
|
2531
|
+
promotedCount = 0;
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2346
2534
|
const expirySnapshot = await runHealthExpiryCheck({
|
|
2347
2535
|
alert_window_hours: 48
|
|
2348
2536
|
});
|
|
@@ -2355,11 +2543,42 @@ async function runDailyClose(args = {}) {
|
|
|
2355
2543
|
Number(expirySnapshot?.expiry_alert?.recommended_for_promotion_count) || 0
|
|
2356
2544
|
);
|
|
2357
2545
|
const expiryAction =
|
|
2358
|
-
|
|
2359
|
-
?
|
|
2360
|
-
:
|
|
2361
|
-
? '
|
|
2362
|
-
:
|
|
2546
|
+
promotedCount > 0
|
|
2547
|
+
? `auto-promoted ${promotedCount} expiring memor${promotedCount === 1 ? 'y' : 'ies'}`
|
|
2548
|
+
: recommendPromoteCount > 0
|
|
2549
|
+
? 'expiring memories remain — call batch_promote or wait for next session_close'
|
|
2550
|
+
: soonExpiringCount > 0
|
|
2551
|
+
? 'short-term memories approaching expiry — review with health_check if needed'
|
|
2552
|
+
: null;
|
|
2553
|
+
|
|
2554
|
+
// 便條: optional handoff note addressed to another body
|
|
2555
|
+
let noteResult = null;
|
|
2556
|
+
if (to) {
|
|
2557
|
+
try {
|
|
2558
|
+
const noteTags = buildLifecycleMemoryTags(['note', `to:${to}`], source, dateKey);
|
|
2559
|
+
const notePayload = {
|
|
2560
|
+
type: 'note',
|
|
2561
|
+
profile: DB_PROFILE,
|
|
2562
|
+
source,
|
|
2563
|
+
to,
|
|
2564
|
+
note,
|
|
2565
|
+
date: dateKey,
|
|
2566
|
+
created_at: nowTs.toISOString()
|
|
2567
|
+
};
|
|
2568
|
+
const savedNote = await insertNoteMemory({
|
|
2569
|
+
source,
|
|
2570
|
+
to,
|
|
2571
|
+
note: note || '',
|
|
2572
|
+
body: JSON.stringify(notePayload),
|
|
2573
|
+
tags: noteTags,
|
|
2574
|
+
expires_at: expiresAt
|
|
2575
|
+
});
|
|
2576
|
+
noteResult = { to, note_id: savedNote?.id || null, delivered: Boolean(savedNote) };
|
|
2577
|
+
} catch (error) {
|
|
2578
|
+
// note columns may not be deployed yet — degrade gracefully, close itself still succeeds
|
|
2579
|
+
noteResult = { to, delivered: false, error: String(error?.message || error) };
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2363
2582
|
|
|
2364
2583
|
return {
|
|
2365
2584
|
ok: true,
|
|
@@ -2371,6 +2590,8 @@ async function runDailyClose(args = {}) {
|
|
|
2371
2590
|
topics,
|
|
2372
2591
|
mood: mood || null,
|
|
2373
2592
|
tool_usage_daily_summary: toolUsageDailySummary,
|
|
2593
|
+
note: noteResult,
|
|
2594
|
+
promoted_count: promotedCount,
|
|
2374
2595
|
expiry_alert: {
|
|
2375
2596
|
soon_expiring_count: soonExpiringCount,
|
|
2376
2597
|
recommend_promote_count: recommendPromoteCount,
|
|
@@ -3438,6 +3659,45 @@ function buildPromoteTimeline(chunk, sourceMemory) {
|
|
|
3438
3659
|
source_to_promoted_seconds: sourceToPromotedSeconds
|
|
3439
3660
|
};
|
|
3440
3661
|
}
|
|
3662
|
+
|
|
3663
|
+
async function runGetSummary(args = {}) {
|
|
3664
|
+
const chunkId = normalizeOptionalUuid(args.id, 'id');
|
|
3665
|
+
if (!chunkId) {
|
|
3666
|
+
throw new Error('id is required');
|
|
3667
|
+
}
|
|
3668
|
+
const chunk = await fetchChunkById(chunkId);
|
|
3669
|
+
if (!chunk) {
|
|
3670
|
+
throw new Error(`memory chunk not found: ${chunkId}`);
|
|
3671
|
+
}
|
|
3672
|
+
const preview = truncateChunkContent(chunk.content, CHUNK_SUMMARY_MAX_CHARS);
|
|
3673
|
+
return {
|
|
3674
|
+
ok: true,
|
|
3675
|
+
...buildChunkToolFields(chunk),
|
|
3676
|
+
content: preview.content,
|
|
3677
|
+
preview_truncated: preview.preview_truncated,
|
|
3678
|
+
content_chars: preview.content_chars,
|
|
3679
|
+
drill_down: 'Use get_full for complete chunk text.'
|
|
3680
|
+
};
|
|
3681
|
+
}
|
|
3682
|
+
|
|
3683
|
+
async function runGetFull(args = {}) {
|
|
3684
|
+
const chunkId = normalizeOptionalUuid(args.id, 'id');
|
|
3685
|
+
if (!chunkId) {
|
|
3686
|
+
throw new Error('id is required');
|
|
3687
|
+
}
|
|
3688
|
+
const chunk = await fetchChunkById(chunkId);
|
|
3689
|
+
if (!chunk) {
|
|
3690
|
+
throw new Error(`memory chunk not found: ${chunkId}`);
|
|
3691
|
+
}
|
|
3692
|
+
const fullContent = String(chunk.content ?? '');
|
|
3693
|
+
return {
|
|
3694
|
+
ok: true,
|
|
3695
|
+
...buildChunkToolFields(chunk),
|
|
3696
|
+
content: fullContent,
|
|
3697
|
+
content_chars: fullContent.length
|
|
3698
|
+
};
|
|
3699
|
+
}
|
|
3700
|
+
|
|
3441
3701
|
async function runExplainMemory(args = {}) {
|
|
3442
3702
|
const chunkId = normalizeOptionalUuid(args.id, 'id');
|
|
3443
3703
|
if (!chunkId) {
|
|
@@ -3764,9 +4024,15 @@ async function callTool(name, args = {}) {
|
|
|
3764
4024
|
}
|
|
3765
4025
|
}
|
|
3766
4026
|
: {}),
|
|
3767
|
-
items: explained.items
|
|
4027
|
+
items: explained.items.map(applyRecallPreviewToItem)
|
|
3768
4028
|
};
|
|
3769
4029
|
}
|
|
4030
|
+
if (toolName === 'get_summary') {
|
|
4031
|
+
return await runGetSummary(args);
|
|
4032
|
+
}
|
|
4033
|
+
if (toolName === 'get_full') {
|
|
4034
|
+
return await runGetFull(args);
|
|
4035
|
+
}
|
|
3770
4036
|
if (toolName === 'explain_memory') {
|
|
3771
4037
|
return await runExplainMemory(args);
|
|
3772
4038
|
}
|
|
@@ -4564,6 +4830,25 @@ const server = http.createServer(async (req, res) => {
|
|
|
4564
4830
|
return;
|
|
4565
4831
|
}
|
|
4566
4832
|
|
|
4833
|
+
// Streamable HTTP: this server is POST-only (no SSE streaming of
|
|
4834
|
+
// server-initiated notifications). Per MCP spec, respond to GET /mcp with
|
|
4835
|
+
// 405 Method Not Allowed + an `allow` header so compliant clients (Cursor,
|
|
4836
|
+
// Claude Desktop, etc.) fall back to POST-only request/response instead
|
|
4837
|
+
// of treating the missing stream as a fatal transport error (404 would
|
|
4838
|
+
// cause Cursor's V2 FSM to tombstone the transport after 5 retries).
|
|
4839
|
+
if (
|
|
4840
|
+
req.method === 'GET' &&
|
|
4841
|
+
['/mcp', '/mcp/', '/'].includes(url.pathname)
|
|
4842
|
+
) {
|
|
4843
|
+
res.writeHead(405, {
|
|
4844
|
+
'access-control-allow-origin': '*',
|
|
4845
|
+
allow: 'POST',
|
|
4846
|
+
'cache-control': 'no-store'
|
|
4847
|
+
});
|
|
4848
|
+
res.end();
|
|
4849
|
+
return;
|
|
4850
|
+
}
|
|
4851
|
+
|
|
4567
4852
|
if (req.method !== 'POST' || !['/mcp', '/mcp/', '/'].includes(url.pathname)) {
|
|
4568
4853
|
json(res, 404, { error: 'not_found' });
|
|
4569
4854
|
return;
|