@prmichaelsen/remember-mcp 3.15.7 → 3.16.2
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/.github/workflows/publish.yml +55 -0
- package/CHANGELOG.md +20 -0
- package/agent/design/local.unified-internal-memory-tools.md +325 -0
- package/agent/milestones/milestone-20-unified-internal-memory-tools.md +58 -0
- package/agent/progress.yaml +115 -1
- package/agent/tasks/milestone-20-unified-internal-memory-tools/task-212-add-internal-context-type.md +54 -0
- package/agent/tasks/milestone-20-unified-internal-memory-tools/task-213-update-server-factory-internal-context.md +117 -0
- package/agent/tasks/milestone-20-unified-internal-memory-tools/task-214-create-tag-builder-utility.md +50 -0
- package/agent/tasks/milestone-20-unified-internal-memory-tools/task-215-create-unified-internal-memory-tools.md +65 -0
- package/agent/tasks/milestone-20-unified-internal-memory-tools/task-216-update-default-search-filters.md +46 -0
- package/agent/tasks/milestone-20-unified-internal-memory-tools/task-217-delete-standalone-ghost-tools.md +46 -0
- package/agent/tasks/milestone-20-unified-internal-memory-tools/task-218-add-tests-unified-internal-tools.md +66 -0
- package/dist/e2e-helpers.d.ts +1 -1
- package/dist/server-factory.d.ts +17 -41
- package/dist/server-factory.js +420 -149
- package/dist/server.js +202 -20
- package/dist/tools/{create-ghost-memory.d.ts → create-internal-memory.d.ts} +7 -9
- package/dist/tools/internal-tools.spec.d.ts +2 -0
- package/dist/tools/{query-ghost-memory.d.ts → query-internal-memory.d.ts} +6 -6
- package/dist/tools/{search-ghost-memory-by.d.ts → search-internal-memory-by.d.ts} +6 -6
- package/dist/tools/{search-ghost-memory.d.ts → search-internal-memory.d.ts} +6 -6
- package/dist/tools/{update-ghost-memory.d.ts → update-internal-memory.d.ts} +6 -6
- package/dist/types/auth.d.ts +22 -8
- package/dist/utils/internal-tags.d.ts +14 -0
- package/dist/utils/internal-tags.spec.d.ts +2 -0
- package/package.json +2 -3
- package/src/e2e-helpers.ts +4 -2
- package/src/ghost-persona.e2e.ts +18 -17
- package/src/server-factory.ts +117 -55
- package/src/tools/create-internal-memory.ts +105 -0
- package/src/tools/find-similar.ts +2 -2
- package/src/tools/internal-tools.spec.ts +312 -0
- package/src/tools/query-internal-memory.ts +73 -0
- package/src/tools/query-memory.ts +15 -12
- package/src/tools/search-by.spec.ts +6 -2
- package/src/tools/search-by.ts +6 -6
- package/src/tools/{search-ghost-memory-by.ts → search-internal-memory-by.ts} +34 -27
- package/src/tools/search-internal-memory.ts +87 -0
- package/src/tools/search-memory.ts +15 -12
- package/src/tools/search-space.ts +1 -0
- package/src/tools/{update-ghost-memory.ts → update-internal-memory.ts} +23 -17
- package/src/types/auth.ts +22 -8
- package/src/utils/internal-tags.spec.ts +104 -0
- package/src/utils/internal-tags.ts +46 -0
- package/dist/tools/ghost-tools.spec.d.ts +0 -2
- package/src/tools/create-ghost-memory.ts +0 -103
- package/src/tools/ghost-tools.spec.ts +0 -361
- package/src/tools/query-ghost-memory.ts +0 -63
- package/src/tools/search-ghost-memory.ts +0 -73
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* remember_search_internal_memory tool
|
|
3
|
+
* Wraps search_memory with auto-scoped content type and ghost source filters.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { handleToolError } from '../utils/error-handler.js';
|
|
7
|
+
import { createDebugLogger } from '../utils/debug.js';
|
|
8
|
+
import type { AuthContext } from '../types/auth.js';
|
|
9
|
+
import { handleSearchMemory } from './search-memory.js';
|
|
10
|
+
import { buildInternalTags } from '../utils/internal-tags.js';
|
|
11
|
+
|
|
12
|
+
export const searchInternalMemoryTool = {
|
|
13
|
+
name: 'remember_search_internal_memory',
|
|
14
|
+
description: `Search internal memories (ghost or agent) using hybrid semantic + keyword search.
|
|
15
|
+
|
|
16
|
+
Automatically scoped to the current session's content type and ghost source.
|
|
17
|
+
In ghost mode, only shows memories from the current ghost conversation
|
|
18
|
+
(e.g., only alice's ghost memories, not carol's).`,
|
|
19
|
+
inputSchema: {
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {
|
|
22
|
+
query: { type: 'string', description: 'Search query' },
|
|
23
|
+
alpha: { type: 'number', minimum: 0, maximum: 1, description: 'Semantic vs keyword balance. Default: 0.7' },
|
|
24
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Additional tag filters' },
|
|
25
|
+
limit: { type: 'number', description: 'Max results. Default: 10' },
|
|
26
|
+
offset: { type: 'number' },
|
|
27
|
+
deleted_filter: { type: 'string', enum: ['exclude', 'include', 'only'] },
|
|
28
|
+
},
|
|
29
|
+
required: ['query'],
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export interface SearchInternalMemoryArgs {
|
|
34
|
+
query: string;
|
|
35
|
+
alpha?: number;
|
|
36
|
+
tags?: string[];
|
|
37
|
+
limit?: number;
|
|
38
|
+
offset?: number;
|
|
39
|
+
deleted_filter?: 'exclude' | 'include' | 'only';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function handleSearchInternalMemory(
|
|
43
|
+
args: SearchInternalMemoryArgs,
|
|
44
|
+
userId: string,
|
|
45
|
+
authContext?: AuthContext
|
|
46
|
+
): Promise<string> {
|
|
47
|
+
const debug = createDebugLogger({ tool: 'remember_search_internal_memory', userId, operation: 'search internal memories' });
|
|
48
|
+
try {
|
|
49
|
+
debug.info('Tool invoked');
|
|
50
|
+
|
|
51
|
+
const ctx = authContext?.internalContext;
|
|
52
|
+
if (!ctx) {
|
|
53
|
+
return JSON.stringify({ error: 'Internal context required. X-Internal-Type header must be set.' });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Build scope tags for ghost source isolation (no override allowed)
|
|
57
|
+
const allTags = buildInternalTags(authContext!);
|
|
58
|
+
// Scope tags = everything except the base 'ghost' or 'agent' tag (those are covered by type filter)
|
|
59
|
+
const scopeTags = allTags.filter(t => t !== 'ghost' && t !== 'agent');
|
|
60
|
+
const userTags = args.tags ?? [];
|
|
61
|
+
const mergedTags = [...new Set([...scopeTags, ...userTags])];
|
|
62
|
+
|
|
63
|
+
// Delegate to search_memory with type + scope filters
|
|
64
|
+
return await handleSearchMemory(
|
|
65
|
+
{
|
|
66
|
+
query: args.query,
|
|
67
|
+
alpha: args.alpha,
|
|
68
|
+
limit: args.limit,
|
|
69
|
+
offset: args.offset,
|
|
70
|
+
filters: {
|
|
71
|
+
types: [ctx.type] as any,
|
|
72
|
+
tags: mergedTags.length > 0 ? mergedTags : undefined,
|
|
73
|
+
},
|
|
74
|
+
deleted_filter: args.deleted_filter,
|
|
75
|
+
},
|
|
76
|
+
userId,
|
|
77
|
+
authContext
|
|
78
|
+
);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
debug.error('Tool failed', { error: error instanceof Error ? error.message : String(error) });
|
|
81
|
+
handleToolError(error, {
|
|
82
|
+
toolName: 'remember_search_internal_memory',
|
|
83
|
+
operation: 'search internal memories',
|
|
84
|
+
userId,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -155,13 +155,13 @@ export async function handleSearchMemory(
|
|
|
155
155
|
userId: string,
|
|
156
156
|
authContext?: AuthContext
|
|
157
157
|
): Promise<string> {
|
|
158
|
-
const
|
|
158
|
+
const internalContext = authContext?.internalContext;
|
|
159
159
|
// In ghost mode, search the ghost owner's collection instead of the caller's
|
|
160
|
-
const searchUserId =
|
|
161
|
-
const debug = createDebugLogger({ tool: 'remember_search_memory', userId: searchUserId, operation:
|
|
160
|
+
const searchUserId = internalContext?.owner_user_id ?? userId;
|
|
161
|
+
const debug = createDebugLogger({ tool: 'remember_search_memory', userId: searchUserId, operation: internalContext ? 'internal search' : 'search memory' });
|
|
162
162
|
try {
|
|
163
163
|
debug.info('Tool invoked');
|
|
164
|
-
debug.trace('Arguments', { args,
|
|
164
|
+
debug.trace('Arguments', { args, internalContext: !!internalContext });
|
|
165
165
|
// Validate query is not empty
|
|
166
166
|
if (!args.query || args.query.trim() === '') {
|
|
167
167
|
throw new Error('Query cannot be empty');
|
|
@@ -173,7 +173,7 @@ export async function handleSearchMemory(
|
|
|
173
173
|
userId: searchUserId,
|
|
174
174
|
query: args.query,
|
|
175
175
|
includeRelationships,
|
|
176
|
-
|
|
176
|
+
internalContext: !!internalContext,
|
|
177
177
|
});
|
|
178
178
|
|
|
179
179
|
const collection = getMemoryCollection(searchUserId);
|
|
@@ -185,8 +185,8 @@ export async function handleSearchMemory(
|
|
|
185
185
|
const deletedFilter = buildDeletedFilter(collection, args.deleted_filter || 'exclude');
|
|
186
186
|
|
|
187
187
|
// Build trust filter for ghost mode (resolved server-side, never from tool args)
|
|
188
|
-
const trustFilter =
|
|
189
|
-
? buildTrustFilter(collection,
|
|
188
|
+
const trustFilter = internalContext?.accessor_trust_level != null
|
|
189
|
+
? buildTrustFilter(collection, internalContext.accessor_trust_level)
|
|
190
190
|
: null;
|
|
191
191
|
|
|
192
192
|
// Build filters using v3 API
|
|
@@ -195,14 +195,17 @@ export async function handleSearchMemory(
|
|
|
195
195
|
? buildCombinedSearchFilters(collection, args.filters)
|
|
196
196
|
: buildMemoryOnlyFilters(collection, args.filters);
|
|
197
197
|
|
|
198
|
-
// Exclude ghost memories by default (unless explicitly searching for them)
|
|
198
|
+
// Exclude ghost and agent memories by default (unless explicitly searching for them)
|
|
199
199
|
const hasExplicitTypeFilter = args.filters?.types && args.filters.types.length > 0;
|
|
200
|
-
const
|
|
201
|
-
?
|
|
200
|
+
const internalExclusionFilter = !hasExplicitTypeFilter
|
|
201
|
+
? combineFiltersWithAnd([
|
|
202
|
+
collection.filter.byProperty('content_type').notEqual('ghost'),
|
|
203
|
+
collection.filter.byProperty('content_type').notEqual('agent'),
|
|
204
|
+
])
|
|
202
205
|
: null;
|
|
203
206
|
|
|
204
|
-
// Combine deleted filter, trust filter,
|
|
205
|
-
const combinedFilters = combineFiltersWithAnd([deletedFilter, trustFilter,
|
|
207
|
+
// Combine deleted filter, trust filter, internal exclusion, and search filters
|
|
208
|
+
const combinedFilters = combineFiltersWithAnd([deletedFilter, trustFilter, internalExclusionFilter, searchFilters].filter(f => f !== null));
|
|
206
209
|
|
|
207
210
|
// Build search options (native offset handled by Weaviate)
|
|
208
211
|
const searchOptions: any = {
|
|
@@ -161,6 +161,7 @@ export function buildBaseFilters(collection: any, args: SearchSpaceArgs): any[]
|
|
|
161
161
|
if (args.content_type) filterList.push(collection.filter.byProperty('content_type').equal(args.content_type));
|
|
162
162
|
if (!args.include_comments && !args.content_type) filterList.push(collection.filter.byProperty('content_type').notEqual('comment'));
|
|
163
163
|
if (!args.content_type) filterList.push(collection.filter.byProperty('content_type').notEqual('ghost'));
|
|
164
|
+
if (!args.content_type) filterList.push(collection.filter.byProperty('content_type').notEqual('agent'));
|
|
164
165
|
if (args.tags && args.tags.length > 0) args.tags.forEach(tag => filterList.push(collection.filter.byProperty('tags').containsAny([tag])));
|
|
165
166
|
if (args.min_weight !== undefined) filterList.push(collection.filter.byProperty('weight').greaterOrEqual(args.min_weight));
|
|
166
167
|
if (args.max_weight !== undefined) filterList.push(collection.filter.byProperty('weight').lessOrEqual(args.max_weight));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* Updates a ghost memory after validating content_type
|
|
2
|
+
* remember_update_internal_memory tool
|
|
3
|
+
* Updates a ghost or agent memory after validating content_type matches session context.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { handleToolError } from '../utils/error-handler.js';
|
|
@@ -9,13 +9,14 @@ import type { AuthContext } from '../types/auth.js';
|
|
|
9
9
|
import { createCoreServices } from '../core-services.js';
|
|
10
10
|
import { getMemoryCollection } from '../weaviate/schema.js';
|
|
11
11
|
|
|
12
|
-
export const
|
|
13
|
-
name: '
|
|
14
|
-
description:
|
|
12
|
+
export const updateInternalMemoryTool = {
|
|
13
|
+
name: 'remember_update_internal_memory',
|
|
14
|
+
description: `Update an internal memory (ghost or agent). Only works on memories matching
|
|
15
|
+
the current session's content type. Use remember_update_memory for regular memories.`,
|
|
15
16
|
inputSchema: {
|
|
16
17
|
type: 'object',
|
|
17
18
|
properties: {
|
|
18
|
-
memory_id: { type: 'string', description: '
|
|
19
|
+
memory_id: { type: 'string', description: 'Memory ID to update' },
|
|
19
20
|
content: { type: 'string' },
|
|
20
21
|
title: { type: 'string' },
|
|
21
22
|
tags: { type: 'array', items: { type: 'string' } },
|
|
@@ -26,7 +27,7 @@ export const updateGhostMemoryTool = {
|
|
|
26
27
|
},
|
|
27
28
|
};
|
|
28
29
|
|
|
29
|
-
export interface
|
|
30
|
+
export interface UpdateInternalMemoryArgs {
|
|
30
31
|
memory_id: string;
|
|
31
32
|
content?: string;
|
|
32
33
|
title?: string;
|
|
@@ -35,25 +36,30 @@ export interface UpdateGhostMemoryArgs {
|
|
|
35
36
|
trust?: number;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
export async function
|
|
39
|
-
args:
|
|
39
|
+
export async function handleUpdateInternalMemory(
|
|
40
|
+
args: UpdateInternalMemoryArgs,
|
|
40
41
|
userId: string,
|
|
41
42
|
authContext?: AuthContext
|
|
42
43
|
): Promise<string> {
|
|
43
|
-
const debug = createDebugLogger({ tool: '
|
|
44
|
+
const debug = createDebugLogger({ tool: 'remember_update_internal_memory', userId, operation: 'update internal memory' });
|
|
44
45
|
try {
|
|
45
46
|
debug.info('Tool invoked');
|
|
46
|
-
debug.trace('Arguments', { args });
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
const ctx = authContext?.internalContext;
|
|
49
|
+
if (!ctx) {
|
|
50
|
+
return JSON.stringify({ error: 'Internal context required. X-Internal-Type header must be set.' });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Validate the memory matches the current content type
|
|
49
54
|
const collection = getMemoryCollection(userId);
|
|
50
55
|
const existing = await collection.query.fetchObjectById(args.memory_id);
|
|
51
56
|
if (!existing) {
|
|
52
57
|
return JSON.stringify({ error: `Memory ${args.memory_id} not found` });
|
|
53
58
|
}
|
|
54
|
-
|
|
59
|
+
const existingType = (existing.properties as any).content_type;
|
|
60
|
+
if (existingType !== ctx.type) {
|
|
55
61
|
return JSON.stringify({
|
|
56
|
-
error: `Memory ${args.memory_id} is
|
|
62
|
+
error: `Memory ${args.memory_id} is content_type: ${existingType}, but current session is ${ctx.type}. Use remember_update_memory for non-internal memories.`,
|
|
57
63
|
});
|
|
58
64
|
}
|
|
59
65
|
|
|
@@ -72,13 +78,13 @@ export async function handleUpdateGhostMemory(
|
|
|
72
78
|
updated_at: result.updated_at,
|
|
73
79
|
version: result.version,
|
|
74
80
|
updated_fields: result.updated_fields,
|
|
75
|
-
message:
|
|
81
|
+
message: `${ctx.type} memory updated successfully. Updated fields: ${result.updated_fields.join(', ')}`,
|
|
76
82
|
}, null, 2);
|
|
77
83
|
} catch (error) {
|
|
78
84
|
debug.error('Tool failed', { error: error instanceof Error ? error.message : String(error) });
|
|
79
85
|
handleToolError(error, {
|
|
80
|
-
toolName: '
|
|
81
|
-
operation: 'update
|
|
86
|
+
toolName: 'remember_update_internal_memory',
|
|
87
|
+
operation: 'update internal memory',
|
|
82
88
|
userId,
|
|
83
89
|
memoryId: args.memory_id,
|
|
84
90
|
});
|
package/src/types/auth.ts
CHANGED
|
@@ -28,21 +28,35 @@ export interface UserCredentials {
|
|
|
28
28
|
group_memberships: GroupMembership[];
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Internal context for ghost/agent sessions — resolved server-side from
|
|
33
|
+
* platform HTTP headers, never from tool args.
|
|
34
|
+
*
|
|
35
|
+
* Replaces the former GhostModeContext. All ghost identity, trust, and
|
|
36
|
+
* agent context is unified here.
|
|
37
|
+
*/
|
|
38
|
+
export interface InternalContext {
|
|
39
|
+
/** Whether this is a ghost or agent session */
|
|
40
|
+
type: 'ghost' | 'agent';
|
|
41
|
+
/** Ghost sub-type (required when type is 'ghost') */
|
|
42
|
+
ghost_type?: 'user' | 'space' | 'group';
|
|
43
|
+
/** Space ID (space ghosts only) */
|
|
44
|
+
ghost_space?: string;
|
|
45
|
+
/** Group ID (group ghosts only) */
|
|
46
|
+
ghost_group?: string;
|
|
47
|
+
/** The ghost owner's user ID (user ghosts — whose memories are searched) */
|
|
48
|
+
owner_user_id?: string;
|
|
49
|
+
/** The accessor's user ID (who is conversing) */
|
|
36
50
|
accessor_user_id: string;
|
|
37
51
|
/** Resolved trust level (looked up from GhostConfig, not user-supplied) */
|
|
38
|
-
accessor_trust_level
|
|
52
|
+
accessor_trust_level?: number;
|
|
39
53
|
}
|
|
40
54
|
|
|
41
55
|
export interface AuthContext {
|
|
42
56
|
accessToken: string | null;
|
|
43
57
|
credentials: UserCredentials | null;
|
|
44
|
-
/** Present when the server is running in ghost
|
|
45
|
-
|
|
58
|
+
/** Present when the server is running in ghost or agent mode */
|
|
59
|
+
internalContext?: InternalContext;
|
|
46
60
|
}
|
|
47
61
|
|
|
48
62
|
export type WriteMode = 'owner_only' | 'group_editors' | 'anyone';
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { buildInternalTags } from './internal-tags.js';
|
|
2
|
+
import type { AuthContext } from '../types/auth.js';
|
|
3
|
+
|
|
4
|
+
describe('buildInternalTags', () => {
|
|
5
|
+
const baseAuth: AuthContext = { accessToken: null, credentials: null };
|
|
6
|
+
|
|
7
|
+
it('returns empty array when no internalContext', () => {
|
|
8
|
+
expect(buildInternalTags(baseAuth)).toEqual([]);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('returns ["agent"] for agent context', () => {
|
|
12
|
+
const auth: AuthContext = {
|
|
13
|
+
...baseAuth,
|
|
14
|
+
internalContext: { type: 'agent', accessor_user_id: 'bob' },
|
|
15
|
+
};
|
|
16
|
+
expect(buildInternalTags(auth)).toEqual(['agent']);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('returns correct tags for user ghost', () => {
|
|
20
|
+
const auth: AuthContext = {
|
|
21
|
+
...baseAuth,
|
|
22
|
+
internalContext: {
|
|
23
|
+
type: 'ghost',
|
|
24
|
+
ghost_type: 'user',
|
|
25
|
+
owner_user_id: 'alice',
|
|
26
|
+
accessor_user_id: 'bob',
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
expect(buildInternalTags(auth)).toEqual([
|
|
30
|
+
'ghost',
|
|
31
|
+
'ghost_type:user',
|
|
32
|
+
'ghost_owner:user:alice',
|
|
33
|
+
]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('returns correct tags for space ghost', () => {
|
|
37
|
+
const auth: AuthContext = {
|
|
38
|
+
...baseAuth,
|
|
39
|
+
internalContext: {
|
|
40
|
+
type: 'ghost',
|
|
41
|
+
ghost_type: 'space',
|
|
42
|
+
ghost_space: 'music-lovers',
|
|
43
|
+
accessor_user_id: 'bob',
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
expect(buildInternalTags(auth)).toEqual([
|
|
47
|
+
'ghost',
|
|
48
|
+
'ghost_type:space',
|
|
49
|
+
'ghost_owner:space:music-lovers',
|
|
50
|
+
]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('returns correct tags for group ghost', () => {
|
|
54
|
+
const auth: AuthContext = {
|
|
55
|
+
...baseAuth,
|
|
56
|
+
internalContext: {
|
|
57
|
+
type: 'ghost',
|
|
58
|
+
ghost_type: 'group',
|
|
59
|
+
ghost_group: 'band-mates',
|
|
60
|
+
accessor_user_id: 'bob',
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
expect(buildInternalTags(auth)).toEqual([
|
|
64
|
+
'ghost',
|
|
65
|
+
'ghost_type:group',
|
|
66
|
+
'ghost_owner:group:band-mates',
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('returns ["ghost"] only when ghost_type is missing', () => {
|
|
71
|
+
const auth: AuthContext = {
|
|
72
|
+
...baseAuth,
|
|
73
|
+
internalContext: {
|
|
74
|
+
type: 'ghost',
|
|
75
|
+
accessor_user_id: 'bob',
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
expect(buildInternalTags(auth)).toEqual(['ghost']);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('returns user ghost tags without owner when owner_user_id is missing', () => {
|
|
82
|
+
const auth: AuthContext = {
|
|
83
|
+
...baseAuth,
|
|
84
|
+
internalContext: {
|
|
85
|
+
type: 'ghost',
|
|
86
|
+
ghost_type: 'user',
|
|
87
|
+
accessor_user_id: 'bob',
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
expect(buildInternalTags(auth)).toEqual(['ghost', 'ghost_type:user']);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('returns space ghost tags without owner when ghost_space is missing', () => {
|
|
94
|
+
const auth: AuthContext = {
|
|
95
|
+
...baseAuth,
|
|
96
|
+
internalContext: {
|
|
97
|
+
type: 'ghost',
|
|
98
|
+
ghost_type: 'space',
|
|
99
|
+
accessor_user_id: 'bob',
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
expect(buildInternalTags(auth)).toEqual(['ghost', 'ghost_type:space']);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { AuthContext } from '../types/auth.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build tags for internal (ghost/agent) memories based on the current
|
|
5
|
+
* session's InternalContext. These tags implement ghost source isolation
|
|
6
|
+
* so memories from different ghost conversations are distinguishable.
|
|
7
|
+
*
|
|
8
|
+
* Tag scheme:
|
|
9
|
+
* User ghost (alice): ['ghost', 'ghost_type:user', 'ghost_owner:user:alice']
|
|
10
|
+
* Space ghost (music-lovers): ['ghost', 'ghost_type:space', 'ghost_owner:space:music-lovers']
|
|
11
|
+
* Group ghost (band-mates): ['ghost', 'ghost_type:group', 'ghost_owner:group:band-mates']
|
|
12
|
+
* Agent: ['agent']
|
|
13
|
+
*/
|
|
14
|
+
export function buildInternalTags(authContext: AuthContext): string[] {
|
|
15
|
+
const ctx = authContext.internalContext;
|
|
16
|
+
if (!ctx) return [];
|
|
17
|
+
|
|
18
|
+
if (ctx.type === 'agent') {
|
|
19
|
+
return ['agent'];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const tags = ['ghost'];
|
|
23
|
+
|
|
24
|
+
switch (ctx.ghost_type) {
|
|
25
|
+
case 'user':
|
|
26
|
+
tags.push('ghost_type:user');
|
|
27
|
+
if (ctx.owner_user_id) {
|
|
28
|
+
tags.push(`ghost_owner:user:${ctx.owner_user_id}`);
|
|
29
|
+
}
|
|
30
|
+
break;
|
|
31
|
+
case 'space':
|
|
32
|
+
tags.push('ghost_type:space');
|
|
33
|
+
if (ctx.ghost_space) {
|
|
34
|
+
tags.push(`ghost_owner:space:${ctx.ghost_space}`);
|
|
35
|
+
}
|
|
36
|
+
break;
|
|
37
|
+
case 'group':
|
|
38
|
+
tags.push('ghost_type:group');
|
|
39
|
+
if (ctx.ghost_group) {
|
|
40
|
+
tags.push(`ghost_owner:group:${ctx.ghost_group}`);
|
|
41
|
+
}
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return tags;
|
|
46
|
+
}
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* remember_create_ghost_memory tool
|
|
3
|
-
* Creates a ghost memory with hardcoded content_type and ghost-specific tags
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { handleToolError } from '../utils/error-handler.js';
|
|
7
|
-
import { createDebugLogger } from '../utils/debug.js';
|
|
8
|
-
import type { AuthContext } from '../types/auth.js';
|
|
9
|
-
import { createCoreServices } from '../core-services.js';
|
|
10
|
-
|
|
11
|
-
export const createGhostMemoryTool = {
|
|
12
|
-
name: 'remember_create_ghost_memory',
|
|
13
|
-
description: `Create a ghost memory (cross-user interaction record).
|
|
14
|
-
|
|
15
|
-
Ghost memories track what happened during ghost conversations — observations,
|
|
16
|
-
impressions, and insights about the accessor. Automatically sets content_type
|
|
17
|
-
to 'ghost' and adds ghost-specific tags.
|
|
18
|
-
|
|
19
|
-
Ghost memories are excluded from default searches. They are only visible when
|
|
20
|
-
explicitly searching with content_type: 'ghost' or using ghost memory tools.`,
|
|
21
|
-
inputSchema: {
|
|
22
|
-
type: 'object',
|
|
23
|
-
properties: {
|
|
24
|
-
content: { type: 'string', description: 'Ghost memory content' },
|
|
25
|
-
title: { type: 'string', description: 'Optional title' },
|
|
26
|
-
tags: { type: 'array', items: { type: 'string' }, description: 'Additional tags (ghost-specific tags added automatically)' },
|
|
27
|
-
weight: { type: 'number', minimum: 0, maximum: 1, description: 'Significance (0-1)' },
|
|
28
|
-
trust: { type: 'number', minimum: 0, maximum: 1, description: 'Trust level (0-1)' },
|
|
29
|
-
feel_salience: { type: 'number', minimum: 0, maximum: 1, description: 'How unexpected/novel (0-1)' },
|
|
30
|
-
feel_social_weight: { type: 'number', minimum: 0, maximum: 1, description: 'Relationship/reputation impact (0-1)' },
|
|
31
|
-
feel_narrative_importance: { type: 'number', minimum: 0, maximum: 1, description: 'Story arc importance (0-1)' },
|
|
32
|
-
},
|
|
33
|
-
required: ['content'],
|
|
34
|
-
},
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
export interface CreateGhostMemoryArgs {
|
|
38
|
-
content: string;
|
|
39
|
-
title?: string;
|
|
40
|
-
tags?: string[];
|
|
41
|
-
weight?: number;
|
|
42
|
-
trust?: number;
|
|
43
|
-
[key: string]: any;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export async function handleCreateGhostMemory(
|
|
47
|
-
args: CreateGhostMemoryArgs,
|
|
48
|
-
userId: string,
|
|
49
|
-
authContext?: AuthContext
|
|
50
|
-
): Promise<string> {
|
|
51
|
-
const debug = createDebugLogger({ tool: 'remember_create_ghost_memory', userId, operation: 'create ghost memory' });
|
|
52
|
-
try {
|
|
53
|
-
debug.info('Tool invoked');
|
|
54
|
-
debug.trace('Arguments', { args });
|
|
55
|
-
|
|
56
|
-
const { memory } = createCoreServices(userId);
|
|
57
|
-
|
|
58
|
-
// Build ghost-specific tags
|
|
59
|
-
const accessorUserId = authContext?.ghostMode?.accessor_user_id;
|
|
60
|
-
const ghostTags = ['ghost'];
|
|
61
|
-
if (accessorUserId) {
|
|
62
|
-
ghostTags.push(`ghost:${accessorUserId}`);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Merge user tags with ghost tags (avoid duplicates)
|
|
66
|
-
const userTags = args.tags ?? [];
|
|
67
|
-
const mergedTags = [...new Set([...ghostTags, ...userTags])];
|
|
68
|
-
|
|
69
|
-
// Extract feel_* fields
|
|
70
|
-
const feelFields: Record<string, number> = {};
|
|
71
|
-
for (const [key, value] of Object.entries(args)) {
|
|
72
|
-
if (key.startsWith('feel_') && typeof value === 'number') {
|
|
73
|
-
feelFields[key] = value;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const result = await memory.create({
|
|
78
|
-
content: args.content,
|
|
79
|
-
title: args.title,
|
|
80
|
-
type: 'ghost' as any,
|
|
81
|
-
weight: args.weight,
|
|
82
|
-
trust: args.trust,
|
|
83
|
-
tags: mergedTags,
|
|
84
|
-
context_summary: 'Ghost memory created via MCP',
|
|
85
|
-
...feelFields,
|
|
86
|
-
} as any);
|
|
87
|
-
|
|
88
|
-
return JSON.stringify({
|
|
89
|
-
memory_id: result.memory_id,
|
|
90
|
-
created_at: result.created_at,
|
|
91
|
-
content_type: 'ghost',
|
|
92
|
-
tags: mergedTags,
|
|
93
|
-
message: `Ghost memory created successfully with ID: ${result.memory_id}`,
|
|
94
|
-
}, null, 2);
|
|
95
|
-
} catch (error) {
|
|
96
|
-
debug.error('Tool failed', { error: error instanceof Error ? error.message : String(error) });
|
|
97
|
-
handleToolError(error, {
|
|
98
|
-
toolName: 'remember_create_ghost_memory',
|
|
99
|
-
operation: 'create ghost memory',
|
|
100
|
-
userId,
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
}
|