exovault-mcp-server 1.4.0 → 1.4.1
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/dist/db.d.ts +2 -0
- package/dist/db.js +28 -17
- package/dist/embedding-config.js +1 -1
- package/dist/episodic-headline.d.ts +5 -2
- package/dist/episodic-headline.js +15 -7
- package/dist/gateway-client.d.ts +4 -0
- package/dist/gateway-client.js +4 -0
- package/dist/index.js +18 -4
- package/dist/scripts/backfill-memory-embeddings.js +1 -1
- package/dist/session-buffer.js +29 -11
- package/dist/tools/update-memory.d.ts +1 -0
- package/dist/tools/update-memory.js +2 -0
- package/package.json +1 -1
package/dist/db.d.ts
CHANGED
|
@@ -97,6 +97,8 @@ export declare function getMemories(supabase: SupabaseClient, userId: string, op
|
|
|
97
97
|
includeArchived?: boolean;
|
|
98
98
|
limit?: number;
|
|
99
99
|
orderBy?: "updated_at" | "importance";
|
|
100
|
+
/** Use lightweight columns (no encrypted content/summary). Default: false. */
|
|
101
|
+
lightweight?: boolean;
|
|
100
102
|
}): Promise<MemoryRow[]>;
|
|
101
103
|
export declare function getMemory(supabase: SupabaseClient, userId: string, memoryId: string): Promise<MemoryRow | null>;
|
|
102
104
|
export declare function getMemoryByExternalWriteId(supabase: SupabaseClient, userId: string, externalWriteId: string): Promise<MemoryRow | null>;
|
package/dist/db.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { sanitizeDbError } from "./error-sanitizer.js";
|
|
2
|
+
// ─── Column lists (explicit to avoid SELECT * egress overhead) ───────────────
|
|
3
|
+
const VAULT_COLUMNS = "id, user_id, encrypted_name, name_iv, icon, color, sort_order, created_at, updated_at";
|
|
4
|
+
const NOTE_COLUMNS = "id, user_id, vault_id, encrypted_title, title_iv, encrypted_content, content_iv, content_type, encrypted_tags, tags_iv, embedding_status, import_source, import_source_id, is_trashed, created_at, updated_at";
|
|
5
|
+
const LINK_COLUMNS = "id, user_id, source_type, source_id, target_type, target_id, relation_type, encrypted_label, label_iv, is_pending, pending_target_hash, created_at, created_by";
|
|
6
|
+
const MESSAGE_COLUMNS = "id, user_id, vault_id, sender_type, sender_id, target_type, target_id, category, priority, subject, encrypted_content, content_iv, status, delivered_at, acknowledged_at, parent_message_id, thread_id, expires_at, metadata, created_at, updated_at";
|
|
2
7
|
// ─── Query helpers ────────────────────────────────────────────────────────────
|
|
3
8
|
export async function getVaults(supabase, userId) {
|
|
4
9
|
const { data, error } = await supabase
|
|
5
10
|
.from("vaults")
|
|
6
|
-
.select(
|
|
11
|
+
.select(VAULT_COLUMNS)
|
|
7
12
|
.eq("user_id", userId)
|
|
8
13
|
.order("sort_order", { ascending: true });
|
|
9
14
|
if (error)
|
|
@@ -13,7 +18,7 @@ export async function getVaults(supabase, userId) {
|
|
|
13
18
|
export async function getNotes(supabase, userId, vaultId, limit = 20) {
|
|
14
19
|
let query = supabase
|
|
15
20
|
.from("notes")
|
|
16
|
-
.select(
|
|
21
|
+
.select(NOTE_COLUMNS)
|
|
17
22
|
.eq("user_id", userId)
|
|
18
23
|
.eq("is_trashed", false)
|
|
19
24
|
.order("updated_at", { ascending: false })
|
|
@@ -29,7 +34,7 @@ export async function getNotes(supabase, userId, vaultId, limit = 20) {
|
|
|
29
34
|
export async function getNote(supabase, userId, noteId) {
|
|
30
35
|
const { data, error } = await supabase
|
|
31
36
|
.from("notes")
|
|
32
|
-
.select(
|
|
37
|
+
.select(NOTE_COLUMNS)
|
|
33
38
|
.eq("id", noteId)
|
|
34
39
|
.eq("user_id", userId)
|
|
35
40
|
.single();
|
|
@@ -43,7 +48,7 @@ export async function getNote(supabase, userId, noteId) {
|
|
|
43
48
|
export async function getVault(supabase, userId, vaultId) {
|
|
44
49
|
const { data, error } = await supabase
|
|
45
50
|
.from("vaults")
|
|
46
|
-
.select(
|
|
51
|
+
.select(VAULT_COLUMNS)
|
|
47
52
|
.eq("id", vaultId)
|
|
48
53
|
.eq("user_id", userId)
|
|
49
54
|
.single();
|
|
@@ -102,9 +107,15 @@ export async function getMemories(supabase, userId, options) {
|
|
|
102
107
|
const includeArchived = options?.includeArchived ?? false;
|
|
103
108
|
const limit = options?.limit ?? 50;
|
|
104
109
|
const orderBy = options?.orderBy ?? "updated_at";
|
|
110
|
+
// Lightweight mode: select only metadata columns (no encrypted content/summary) to reduce egress.
|
|
111
|
+
// Supabase's TS type parser can't handle long literal strings, so we build the query separately.
|
|
112
|
+
const columns = options?.lightweight
|
|
113
|
+
? "id,user_id,vault_id,memory_type,agent_id,agent_type,model_id,agent_run_id,importance,confidence,access_count,is_archived,entities,superseded_by_id,created_at,updated_at"
|
|
114
|
+
: "*";
|
|
105
115
|
let query = supabase
|
|
106
116
|
.from("memories")
|
|
107
|
-
|
|
117
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
118
|
+
.select(columns)
|
|
108
119
|
.eq("user_id", userId)
|
|
109
120
|
.order(orderBy, { ascending: false })
|
|
110
121
|
.limit(limit);
|
|
@@ -339,7 +350,7 @@ export async function getNotesByIds(supabase, userId, noteIds) {
|
|
|
339
350
|
return [];
|
|
340
351
|
const { data, error } = await supabase
|
|
341
352
|
.from("notes")
|
|
342
|
-
.select(
|
|
353
|
+
.select(NOTE_COLUMNS)
|
|
343
354
|
.eq("user_id", userId)
|
|
344
355
|
.eq("is_trashed", false)
|
|
345
356
|
.in("id", noteIds);
|
|
@@ -350,7 +361,7 @@ export async function getNotesByIds(supabase, userId, noteIds) {
|
|
|
350
361
|
export async function getNoteByImportSource(supabase, userId, vaultId, importSource, importSourceId) {
|
|
351
362
|
const { data, error } = await supabase
|
|
352
363
|
.from("notes")
|
|
353
|
-
.select(
|
|
364
|
+
.select(NOTE_COLUMNS)
|
|
354
365
|
.eq("user_id", userId)
|
|
355
366
|
.eq("vault_id", vaultId)
|
|
356
367
|
.eq("import_source", importSource)
|
|
@@ -583,7 +594,7 @@ export async function insertKnowledgeLink(supabase, data) {
|
|
|
583
594
|
pending_target_hash: data.pending_target_hash ?? null,
|
|
584
595
|
created_by: data.created_by ?? null,
|
|
585
596
|
})
|
|
586
|
-
.select()
|
|
597
|
+
.select(LINK_COLUMNS)
|
|
587
598
|
.single();
|
|
588
599
|
if (error)
|
|
589
600
|
throw new Error(sanitizeDbError("insert knowledge link", error.message));
|
|
@@ -604,7 +615,7 @@ export async function getLinksForNode(supabase, userId, nodeType, nodeId, option
|
|
|
604
615
|
if (direction === "outgoing") {
|
|
605
616
|
const { data, error } = await supabase
|
|
606
617
|
.from("knowledge_links")
|
|
607
|
-
.select()
|
|
618
|
+
.select(LINK_COLUMNS)
|
|
608
619
|
.eq("user_id", userId)
|
|
609
620
|
.eq("source_type", nodeType)
|
|
610
621
|
.eq("source_id", nodeId)
|
|
@@ -617,7 +628,7 @@ export async function getLinksForNode(supabase, userId, nodeType, nodeId, option
|
|
|
617
628
|
if (direction === "incoming") {
|
|
618
629
|
const { data, error } = await supabase
|
|
619
630
|
.from("knowledge_links")
|
|
620
|
-
.select()
|
|
631
|
+
.select(LINK_COLUMNS)
|
|
621
632
|
.eq("user_id", userId)
|
|
622
633
|
.eq("target_type", nodeType)
|
|
623
634
|
.eq("target_id", nodeId)
|
|
@@ -631,7 +642,7 @@ export async function getLinksForNode(supabase, userId, nodeType, nodeId, option
|
|
|
631
642
|
const [outgoing, incoming] = await Promise.all([
|
|
632
643
|
supabase
|
|
633
644
|
.from("knowledge_links")
|
|
634
|
-
.select()
|
|
645
|
+
.select(LINK_COLUMNS)
|
|
635
646
|
.eq("user_id", userId)
|
|
636
647
|
.eq("source_type", nodeType)
|
|
637
648
|
.eq("source_id", nodeId)
|
|
@@ -639,7 +650,7 @@ export async function getLinksForNode(supabase, userId, nodeType, nodeId, option
|
|
|
639
650
|
.limit(limit),
|
|
640
651
|
supabase
|
|
641
652
|
.from("knowledge_links")
|
|
642
|
-
.select()
|
|
653
|
+
.select(LINK_COLUMNS)
|
|
643
654
|
.eq("user_id", userId)
|
|
644
655
|
.eq("target_type", nodeType)
|
|
645
656
|
.eq("target_id", nodeId)
|
|
@@ -724,7 +735,7 @@ export async function resolvePendingLinks(supabase, userId, hashToNoteId) {
|
|
|
724
735
|
// Fetch all pending links for this user
|
|
725
736
|
const { data: pending, error } = await supabase
|
|
726
737
|
.from("knowledge_links")
|
|
727
|
-
.select()
|
|
738
|
+
.select(LINK_COLUMNS)
|
|
728
739
|
.eq("user_id", userId)
|
|
729
740
|
.eq("is_pending", true)
|
|
730
741
|
.limit(10_000);
|
|
@@ -786,7 +797,7 @@ export async function insertAgentMessage(supabase, data) {
|
|
|
786
797
|
parent_message_id: data.parent_message_id ?? null,
|
|
787
798
|
thread_id: data.thread_id ?? null,
|
|
788
799
|
})
|
|
789
|
-
.select(
|
|
800
|
+
.select(MESSAGE_COLUMNS)
|
|
790
801
|
.single();
|
|
791
802
|
if (error)
|
|
792
803
|
throw new Error(sanitizeDbError("insert agent message", error.message));
|
|
@@ -799,7 +810,7 @@ export async function insertAgentMessage(supabase, data) {
|
|
|
799
810
|
export async function getPendingMessages(supabase, userId, targetId, limit = 5) {
|
|
800
811
|
const { data, error } = await supabase
|
|
801
812
|
.from("agent_messages")
|
|
802
|
-
.select(
|
|
813
|
+
.select(MESSAGE_COLUMNS)
|
|
803
814
|
.eq("user_id", userId)
|
|
804
815
|
.eq("status", "pending")
|
|
805
816
|
.or(`target_id.eq.${targetId},target_id.eq.*`)
|
|
@@ -819,7 +830,7 @@ export async function getAgentMessages(supabase, userId, targetId, options) {
|
|
|
819
830
|
const includeBroadcast = options?.includeBroadcast ?? true;
|
|
820
831
|
let query = supabase
|
|
821
832
|
.from("agent_messages")
|
|
822
|
-
.select(
|
|
833
|
+
.select(MESSAGE_COLUMNS)
|
|
823
834
|
.eq("user_id", userId)
|
|
824
835
|
.order("priority", { ascending: false })
|
|
825
836
|
.order("created_at", { ascending: false })
|
|
@@ -890,7 +901,7 @@ export async function resolveThreadId(supabase, userId, parentMessageId) {
|
|
|
890
901
|
export async function getThread(supabase, userId, threadId) {
|
|
891
902
|
const { data, error } = await supabase
|
|
892
903
|
.from("agent_messages")
|
|
893
|
-
.select(
|
|
904
|
+
.select(MESSAGE_COLUMNS)
|
|
894
905
|
.eq("user_id", userId)
|
|
895
906
|
.eq("thread_id", threadId)
|
|
896
907
|
.order("created_at", { ascending: true });
|
package/dist/embedding-config.js
CHANGED
|
@@ -10,7 +10,7 @@ export function resolveEmbeddingConfig(ctx) {
|
|
|
10
10
|
if (envApiKey) {
|
|
11
11
|
return {
|
|
12
12
|
apiKey: envApiKey,
|
|
13
|
-
model:
|
|
13
|
+
model: DEFAULT_EMBEDDING_MODEL, // Hardcoded — EMBEDDING_MODEL is for OpenAI text
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
16
|
// Fallback to MCP context keys
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Extracts a short headline (max 200 chars) from episodic memory content.
|
|
3
|
-
*
|
|
4
|
-
* first
|
|
3
|
+
*
|
|
4
|
+
* New format: first line is the session title, followed by details.
|
|
5
|
+
* Legacy format: "Session: Xm, agent=Y." prefix is stripped.
|
|
6
|
+
*
|
|
7
|
+
* Pure function, no LLM calls.
|
|
5
8
|
*/
|
|
6
9
|
export declare function generateEpisodicHeadline(content: string): string;
|
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Extracts a short headline (max 200 chars) from episodic memory content.
|
|
3
|
-
*
|
|
4
|
-
* first
|
|
3
|
+
*
|
|
4
|
+
* New format: first line is the session title, followed by details.
|
|
5
|
+
* Legacy format: "Session: Xm, agent=Y." prefix is stripped.
|
|
6
|
+
*
|
|
7
|
+
* Pure function, no LLM calls.
|
|
5
8
|
*/
|
|
6
9
|
const MAX_HEADLINE_CHARS = 200;
|
|
7
|
-
//
|
|
10
|
+
// Legacy prefix — matches "Session: <duration>, agent=<name>." (with optional newlines after)
|
|
8
11
|
const SESSION_PREFIX_RE = /^Session:\s*\S+,\s*agent=\S+\.\s*/;
|
|
9
12
|
export function generateEpisodicHeadline(content) {
|
|
10
13
|
if (!content)
|
|
11
14
|
return "";
|
|
12
|
-
// Strip
|
|
15
|
+
// Strip legacy metadata prefix if present
|
|
13
16
|
const body = content.replace(SESSION_PREFIX_RE, "").trim();
|
|
14
17
|
// If stripping left nothing, return original content
|
|
15
18
|
if (!body)
|
|
16
19
|
return content;
|
|
17
|
-
//
|
|
18
|
-
|
|
20
|
+
// First line is the session title — use it as headline if it fits
|
|
21
|
+
const firstLineEnd = body.indexOf("\n");
|
|
22
|
+
const firstLine = firstLineEnd > 0 ? body.slice(0, firstLineEnd).trim() : body.trim();
|
|
23
|
+
// If first line is short enough and not a "Explored:/Outcomes:" detail line, use it directly
|
|
24
|
+
if (firstLine.length <= MAX_HEADLINE_CHARS && !firstLine.startsWith("Explored:") && !firstLine.startsWith("Outcomes:")) {
|
|
25
|
+
return firstLine;
|
|
26
|
+
}
|
|
27
|
+
// Fallback: take first 1-2 sentences from body
|
|
19
28
|
const sentences = splitIntoSentences(body);
|
|
20
29
|
// Take enough sentences to fill ~200 chars
|
|
21
30
|
let headline = "";
|
|
@@ -24,7 +33,6 @@ export function generateEpisodicHeadline(content) {
|
|
|
24
33
|
continue;
|
|
25
34
|
const candidate = headline ? `${headline} ${sentence}` : sentence;
|
|
26
35
|
if (candidate.length > MAX_HEADLINE_CHARS) {
|
|
27
|
-
// If we have nothing yet, take this sentence (will be clipped)
|
|
28
36
|
if (!headline)
|
|
29
37
|
headline = sentence;
|
|
30
38
|
break;
|
package/dist/gateway-client.d.ts
CHANGED
|
@@ -195,6 +195,10 @@ export declare class GatewayClient {
|
|
|
195
195
|
icon?: string;
|
|
196
196
|
color?: string;
|
|
197
197
|
}): Promise<string>;
|
|
198
|
+
deleteVault(params: {
|
|
199
|
+
vaultId: string;
|
|
200
|
+
confirm: true;
|
|
201
|
+
}): Promise<string>;
|
|
198
202
|
createTask(params: {
|
|
199
203
|
title: string;
|
|
200
204
|
description?: string;
|
package/dist/gateway-client.js
CHANGED
|
@@ -191,6 +191,10 @@ export class GatewayClient {
|
|
|
191
191
|
const result = await this.request("POST", "/api/agent/create-vault", params);
|
|
192
192
|
return JSON.stringify(result);
|
|
193
193
|
}
|
|
194
|
+
async deleteVault(params) {
|
|
195
|
+
const result = await this.request("POST", "/api/agent/delete-vault", params);
|
|
196
|
+
return JSON.stringify(result);
|
|
197
|
+
}
|
|
194
198
|
// ─── Task operations ─────────────────────────────────────────────────────
|
|
195
199
|
async createTask(params) {
|
|
196
200
|
const result = await this.request("POST", "/api/agent/create-task", params);
|
package/dist/index.js
CHANGED
|
@@ -49,7 +49,7 @@ import { scanOrphanedBuffers, deleteBuffer as deleteBufferFile } from "./buffer-
|
|
|
49
49
|
import { flushSession } from "./session-flush.js";
|
|
50
50
|
import { coerceSchema } from "./coerce-params.js";
|
|
51
51
|
const s = (schema) => coerceSchema(schema);
|
|
52
|
-
const MEMORY_TYPES = ["fact", "skill", "preference", "constraint", "task", "episodic", "correction"];
|
|
52
|
+
const MEMORY_TYPES = ["fact", "skill", "preference", "constraint", "task", "episodic", "correction", "decision"];
|
|
53
53
|
const memoryTypeEnum = z.enum(MEMORY_TYPES);
|
|
54
54
|
/** Remind agents to checkpoint every N tool calls. */
|
|
55
55
|
const CHECKPOINT_REMINDER_INTERVAL = 20;
|
|
@@ -352,7 +352,7 @@ async function main() {
|
|
|
352
352
|
" b. **WRITE on these triggers** — call `write_memory` IMMEDIATELY when:",
|
|
353
353
|
" - User states a preference, rule, or convention → `preference` or `constraint`",
|
|
354
354
|
" - You discover a non-obvious fact about the domain/project/topic → `fact` (importance 3-5)",
|
|
355
|
-
" - A decision is made (by user or jointly) → `
|
|
355
|
+
" - A decision is made (by user or jointly) → `decision` (importance 4-5, include inputs, reasoning, alternatives, outcome)",
|
|
356
356
|
" - You solve a problem or learn a procedure → `skill`",
|
|
357
357
|
" - You hit a surprising gotcha or limitation → `skill` (importance 4)",
|
|
358
358
|
" - Previous knowledge turns out wrong → `correction` (set supersededById)",
|
|
@@ -412,6 +412,20 @@ async function main() {
|
|
|
412
412
|
? await gw.createVault({ name, icon, color })
|
|
413
413
|
: await createVault(ctx, name, { icon, color });
|
|
414
414
|
})));
|
|
415
|
+
// ─── delete_vault ──────────────────────────────────────────────────────────
|
|
416
|
+
server.registerTool("delete_vault", {
|
|
417
|
+
description: "Permanently delete a vault and all its notes/folders. Memories are preserved (vault_id set to null). Requires admin scope and confirm: true. Cannot delete the default vault.",
|
|
418
|
+
inputSchema: {
|
|
419
|
+
vaultId: s(z.string().uuid().describe("The vault ID to delete")),
|
|
420
|
+
confirm: s(z.literal(true).describe("Must be true to confirm deletion")),
|
|
421
|
+
},
|
|
422
|
+
}, auto.wrap(wrapToolHandler(async (args) => {
|
|
423
|
+
const { vaultId, confirm } = args;
|
|
424
|
+
if (!gw) {
|
|
425
|
+
return JSON.stringify({ success: false, error: "delete_vault requires gateway mode" });
|
|
426
|
+
}
|
|
427
|
+
return await gw.deleteVault({ vaultId, confirm });
|
|
428
|
+
})));
|
|
415
429
|
// ─── list_notes ───────────────────────────────────────────────────────────
|
|
416
430
|
server.registerTool("list_notes", {
|
|
417
431
|
description: "List notes with titles, tags, and content previews. Optionally filter by vault or folder.",
|
|
@@ -558,10 +572,10 @@ async function main() {
|
|
|
558
572
|
})));
|
|
559
573
|
// ─── write_memory ───────────────────────────────────────────────────────────
|
|
560
574
|
server.registerTool("write_memory", {
|
|
561
|
-
description: "Create or upsert a durable memory entry.\n\nMemory types: fact (durable knowledge, importance 3-5), skill (procedures/how-tos, 3-5), preference (user style/choices, 2-4), constraint (hard rules/limits, 4-5), task (active work items, 2-4), episodic (session summaries, 1-3), correction (superseded knowledge — always set supersededById, 3-5).\n\nImportance scale: 5=critical, 4=important, 3=standard (default), 2=supplementary, 1=low-value. Confidence scale: 5=verified, 4=observed multiple times, 3=reasonable inference (default), 2=uncertain, 1=speculative.\n\nRelationship fields: relatedMemoryIds links to related memories (derived_from, contradicts, refines, part_of, supersedes). sourceNoteIds links to source notes. supersededById points to the memory this one replaces. entities array enables cross-linking.\n\nServer dedup (dedup: true): >92% similarity = skip, >80% = supersede old, <80% = create new.",
|
|
575
|
+
description: "Create or upsert a durable memory entry.\n\nMemory types: fact (durable knowledge, importance 3-5), skill (procedures/how-tos, 3-5), preference (user style/choices, 2-4), constraint (hard rules/limits, 4-5), task (active work items, 2-4), episodic (session summaries, 1-3), correction (superseded knowledge — always set supersededById, 3-5), decision (significant choices made — include inputs, reasoning, alternatives considered, outcome; importance 4-5).\n\nImportance scale: 5=critical, 4=important, 3=standard (default), 2=supplementary, 1=low-value. Confidence scale: 5=verified, 4=observed multiple times, 3=reasonable inference (default), 2=uncertain, 1=speculative.\n\nRelationship fields: relatedMemoryIds links to related memories (derived_from, contradicts, refines, part_of, supersedes). sourceNoteIds links to source notes. supersededById points to the memory this one replaces. entities array enables cross-linking.\n\nServer dedup (dedup: true): >92% similarity = skip, >80% = supersede old, <80% = create new.",
|
|
562
576
|
inputSchema: {
|
|
563
577
|
content: s(z.string().min(1).max(1_000_000).describe("Memory content in plain text")),
|
|
564
|
-
memoryType: s(memoryTypeEnum.optional().describe("Type: fact, skill, preference, constraint, task, episodic, correction")),
|
|
578
|
+
memoryType: s(memoryTypeEnum.optional().describe("Type: fact, skill, preference, constraint, task, episodic, correction, decision")),
|
|
565
579
|
summary: s(z.string().max(500).optional().describe("Optional short summary")),
|
|
566
580
|
vaultId: s(z.string().uuid().optional().describe("Vault/project scope. Required unless defaultVaultId is configured for this MCP session.")),
|
|
567
581
|
importance: s(z.number().int().min(1).max(5).optional().describe("Importance from 1 to 5")),
|
|
@@ -67,7 +67,7 @@ async function main() {
|
|
|
67
67
|
}
|
|
68
68
|
const embeddingConfig = {
|
|
69
69
|
apiKey,
|
|
70
|
-
model:
|
|
70
|
+
model: DEFAULT_EMBEDDING_MODEL, // Hardcoded — EMBEDDING_MODEL is for OpenAI text
|
|
71
71
|
};
|
|
72
72
|
const candidates = await loadBackfillCandidates(ctx, limit);
|
|
73
73
|
if (candidates.length === 0) {
|
package/dist/session-buffer.js
CHANGED
|
@@ -159,11 +159,6 @@ export function buildTurnContent(data) {
|
|
|
159
159
|
* using tool call metadata (inputs + outputs).
|
|
160
160
|
*/
|
|
161
161
|
export function buildRuleBasedEpisodic(data) {
|
|
162
|
-
const durationMs = Date.now() - new Date(data.startedAt).getTime();
|
|
163
|
-
const durationMin = Math.round(durationMs / 60_000);
|
|
164
|
-
const lines = [
|
|
165
|
-
`Session: ${durationMin}min, agent=${data.agentId}.`,
|
|
166
|
-
];
|
|
167
162
|
// Extract search topics
|
|
168
163
|
const searchTopics = [...new Set(data.activities
|
|
169
164
|
.filter((a) => a.category === "search" && a.inputSummary)
|
|
@@ -173,6 +168,9 @@ export function buildRuleBasedEpisodic(data) {
|
|
|
173
168
|
const readTopics = extractReadTopics(data.activities);
|
|
174
169
|
// Extract write actions with context
|
|
175
170
|
const writeActions = extractWriteActions(data.activities);
|
|
171
|
+
// Derive a session title from topics/actions
|
|
172
|
+
const title = deriveSessionTitle(searchTopics, readTopics, writeActions);
|
|
173
|
+
const lines = [title];
|
|
176
174
|
if (searchTopics.length > 0 || readTopics.length > 0) {
|
|
177
175
|
const explored = [];
|
|
178
176
|
if (searchTopics.length > 0)
|
|
@@ -186,6 +184,28 @@ export function buildRuleBasedEpisodic(data) {
|
|
|
186
184
|
}
|
|
187
185
|
return lines.join("\n");
|
|
188
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* Derive a short session title from extracted topics and actions.
|
|
189
|
+
* Returns a concise phrase (no trailing period) summarizing the session focus.
|
|
190
|
+
*/
|
|
191
|
+
function deriveSessionTitle(searchTopics, readTopics, writeActions) {
|
|
192
|
+
const allTopics = [...searchTopics, ...readTopics];
|
|
193
|
+
if (allTopics.length > 0) {
|
|
194
|
+
const topicStr = allTopics.slice(0, 2).join(" and ");
|
|
195
|
+
return capitalizeAndClean(topicStr);
|
|
196
|
+
}
|
|
197
|
+
if (writeActions.length > 0) {
|
|
198
|
+
return capitalizeAndClean(writeActions[0]);
|
|
199
|
+
}
|
|
200
|
+
return "Session activity";
|
|
201
|
+
}
|
|
202
|
+
/** Capitalize first letter and strip trailing sentence punctuation. */
|
|
203
|
+
function capitalizeAndClean(phrase) {
|
|
204
|
+
const cleaned = phrase.trim().replace(/[.!?]+$/, "");
|
|
205
|
+
if (!cleaned)
|
|
206
|
+
return "Session activity";
|
|
207
|
+
return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
|
|
208
|
+
}
|
|
189
209
|
/**
|
|
190
210
|
* Extract what was read/explored during the session.
|
|
191
211
|
* Provides context about what the agent was looking at — note titles,
|
|
@@ -453,12 +473,10 @@ export function buildTranscriptEpisodic(data) {
|
|
|
453
473
|
if (data.transcript.length < MIN_TRANSCRIPT_ENTRIES_FOR_EPISODIC) {
|
|
454
474
|
return null;
|
|
455
475
|
}
|
|
456
|
-
|
|
457
|
-
const
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
"",
|
|
461
|
-
];
|
|
476
|
+
// Derive title from transcript entries — use first entry as session focus
|
|
477
|
+
const firstContext = data.transcript[0]?.context ?? "Session activity";
|
|
478
|
+
const title = firstContext.charAt(0).toUpperCase() + firstContext.slice(1);
|
|
479
|
+
const lines = [title, ""];
|
|
462
480
|
for (const entry of data.transcript) {
|
|
463
481
|
lines.push(`- ${entry.context}`);
|
|
464
482
|
}
|
|
@@ -14,6 +14,7 @@ export declare function updateMemoryTool(ctx: McpContext, input: {
|
|
|
14
14
|
relatedMemoryIds?: MemoryRelation[];
|
|
15
15
|
sourceNoteIds?: string[];
|
|
16
16
|
isArchived?: boolean;
|
|
17
|
+
supersededById?: string;
|
|
17
18
|
metadata?: Record<string, unknown>;
|
|
18
19
|
}): Promise<string>;
|
|
19
20
|
export {};
|
|
@@ -72,6 +72,8 @@ export async function updateMemoryTool(ctx, input) {
|
|
|
72
72
|
updates.source_note_ids = input.sourceNoteIds;
|
|
73
73
|
if (input.isArchived !== undefined)
|
|
74
74
|
updates.is_archived = input.isArchived;
|
|
75
|
+
if (input.supersededById !== undefined)
|
|
76
|
+
updates.superseded_by_id = input.supersededById;
|
|
75
77
|
// Merge metadata (task-specific fields like taskStatus, assignedAgentId)
|
|
76
78
|
if (input.metadata !== undefined) {
|
|
77
79
|
const existingMeta = (existing.metadata ?? {});
|