@prmichaelsen/remember-mcp 3.14.11 → 3.14.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/agent/milestones/milestone-18-performance-tuning.md +45 -0
- package/agent/progress.yaml +86 -2
- package/agent/tasks/task-77-parallelize-checkiffriend.md +62 -0
- package/agent/tasks/task-78-cache-checkiffriend.md +78 -0
- package/agent/tasks/task-79-memoize-core-services.md +75 -0
- package/agent/tasks/task-80-parallelize-startup-health-checks.md +57 -0
- package/agent/tasks/task-81-optimize-ghost-config-block-unblock.md +64 -0
- package/agent/tasks/task-82-native-weaviate-offset.md +69 -0
- package/agent/tasks/task-83-eliminate-redundant-validatetoken.md +53 -0
- package/agent/tasks/task-84-static-imports-server-factory.md +52 -0
- package/dist/core-services.d.ts +3 -1
- package/dist/server-factory.js +250 -482
- package/dist/server.js +29 -140
- package/dist/services/access-control.d.ts +5 -5
- package/dist/services/ghost-config.service.d.ts +2 -0
- package/package.json +1 -1
- package/src/core-services.ts +16 -2
- package/src/server-factory.ts +5 -3
- package/src/server.ts +5 -3
- package/src/services/access-control.spec.ts +11 -11
- package/src/services/access-control.ts +74 -8
- package/src/services/ghost-config.service.spec.ts +22 -15
- package/src/services/ghost-config.service.ts +9 -15
- package/src/tools/query-memory.ts +1 -2
- package/src/tools/search-memory.ts +5 -8
- package/dist/services/trust-enforcement.d.ts +0 -83
- package/dist/services/trust-enforcement.spec.d.ts +0 -2
- package/dist/utils/weaviate-filters.d.ts +0 -56
- package/dist/utils/weaviate-filters.spec.d.ts +0 -8
- package/src/services/trust-enforcement.spec.ts +0 -309
- package/src/services/trust-enforcement.ts +0 -197
- package/src/utils/weaviate-filters.spec.ts +0 -312
- package/src/utils/weaviate-filters.ts +0 -236
package/dist/server.js
CHANGED
|
@@ -3676,16 +3676,22 @@ function getMemoryCollection(userId) {
|
|
|
3676
3676
|
var coreLogger = createLogger("info");
|
|
3677
3677
|
var tokenService = new ConfirmationTokenService(coreLogger);
|
|
3678
3678
|
var preferencesService = new PreferencesDatabaseService(coreLogger);
|
|
3679
|
+
var coreServicesCache = /* @__PURE__ */ new Map();
|
|
3679
3680
|
function createCoreServices(userId) {
|
|
3681
|
+
const cached = coreServicesCache.get(userId);
|
|
3682
|
+
if (cached)
|
|
3683
|
+
return cached;
|
|
3680
3684
|
const collection = getMemoryCollection(userId);
|
|
3681
3685
|
const weaviateClient = getWeaviateClient();
|
|
3682
|
-
|
|
3686
|
+
const services = {
|
|
3683
3687
|
memory: new MemoryService(collection, userId, coreLogger),
|
|
3684
3688
|
relationship: new RelationshipService(collection, userId, coreLogger),
|
|
3685
3689
|
space: new SpaceService(weaviateClient, collection, userId, tokenService, coreLogger),
|
|
3686
3690
|
preferences: preferencesService,
|
|
3687
3691
|
token: tokenService
|
|
3688
3692
|
};
|
|
3693
|
+
coreServicesCache.set(userId, services);
|
|
3694
|
+
return services;
|
|
3689
3695
|
}
|
|
3690
3696
|
|
|
3691
3697
|
// src/tools/create-memory.ts
|
|
@@ -3815,118 +3821,6 @@ async function handleCreateMemory(args, userId, authContext, context) {
|
|
|
3815
3821
|
|
|
3816
3822
|
// src/tools/search-memory.ts
|
|
3817
3823
|
init_logger();
|
|
3818
|
-
|
|
3819
|
-
// src/utils/weaviate-filters.ts
|
|
3820
|
-
import { Filters as Filters6 } from "weaviate-client";
|
|
3821
|
-
function buildCombinedSearchFilters2(collection, filters) {
|
|
3822
|
-
const memoryFilters = buildDocTypeFilters2(collection, "memory", filters);
|
|
3823
|
-
const relationshipFilters = buildDocTypeFilters2(collection, "relationship", filters);
|
|
3824
|
-
const validFilters = [memoryFilters, relationshipFilters].filter((f) => f !== void 0 && f !== null);
|
|
3825
|
-
if (validFilters.length === 0) {
|
|
3826
|
-
return void 0;
|
|
3827
|
-
} else if (validFilters.length === 1) {
|
|
3828
|
-
return validFilters[0];
|
|
3829
|
-
} else {
|
|
3830
|
-
return combineFiltersWithOr2(validFilters);
|
|
3831
|
-
}
|
|
3832
|
-
}
|
|
3833
|
-
function buildDocTypeFilters2(collection, docType, filters) {
|
|
3834
|
-
const filterList = [];
|
|
3835
|
-
filterList.push(
|
|
3836
|
-
collection.filter.byProperty("doc_type").equal(docType)
|
|
3837
|
-
);
|
|
3838
|
-
if (docType === "memory" && filters?.types && filters.types.length > 0) {
|
|
3839
|
-
if (filters.types.length === 1) {
|
|
3840
|
-
filterList.push(
|
|
3841
|
-
collection.filter.byProperty("content_type").equal(filters.types[0])
|
|
3842
|
-
);
|
|
3843
|
-
} else {
|
|
3844
|
-
filterList.push(
|
|
3845
|
-
collection.filter.byProperty("content_type").containsAny(filters.types)
|
|
3846
|
-
);
|
|
3847
|
-
}
|
|
3848
|
-
}
|
|
3849
|
-
if (filters?.weight_min !== void 0) {
|
|
3850
|
-
filterList.push(
|
|
3851
|
-
collection.filter.byProperty("weight").greaterThanOrEqual(filters.weight_min)
|
|
3852
|
-
);
|
|
3853
|
-
}
|
|
3854
|
-
if (filters?.weight_max !== void 0) {
|
|
3855
|
-
filterList.push(
|
|
3856
|
-
collection.filter.byProperty("weight").lessThanOrEqual(filters.weight_max)
|
|
3857
|
-
);
|
|
3858
|
-
}
|
|
3859
|
-
if (filters?.trust_min !== void 0) {
|
|
3860
|
-
filterList.push(
|
|
3861
|
-
collection.filter.byProperty("trust_score").greaterThanOrEqual(filters.trust_min)
|
|
3862
|
-
);
|
|
3863
|
-
}
|
|
3864
|
-
if (filters?.trust_max !== void 0) {
|
|
3865
|
-
filterList.push(
|
|
3866
|
-
collection.filter.byProperty("trust_score").lessThanOrEqual(filters.trust_max)
|
|
3867
|
-
);
|
|
3868
|
-
}
|
|
3869
|
-
if (filters?.date_from) {
|
|
3870
|
-
filterList.push(
|
|
3871
|
-
collection.filter.byProperty("created_at").greaterThanOrEqual(new Date(filters.date_from))
|
|
3872
|
-
);
|
|
3873
|
-
}
|
|
3874
|
-
if (filters?.date_to) {
|
|
3875
|
-
filterList.push(
|
|
3876
|
-
collection.filter.byProperty("created_at").lessThanOrEqual(new Date(filters.date_to))
|
|
3877
|
-
);
|
|
3878
|
-
}
|
|
3879
|
-
if (filters?.tags && filters.tags.length > 0) {
|
|
3880
|
-
if (filters.tags.length === 1) {
|
|
3881
|
-
filterList.push(
|
|
3882
|
-
collection.filter.byProperty("tags").containsAny([filters.tags[0]])
|
|
3883
|
-
);
|
|
3884
|
-
} else {
|
|
3885
|
-
filterList.push(
|
|
3886
|
-
collection.filter.byProperty("tags").containsAny(filters.tags)
|
|
3887
|
-
);
|
|
3888
|
-
}
|
|
3889
|
-
}
|
|
3890
|
-
return combineFiltersWithAnd2(filterList);
|
|
3891
|
-
}
|
|
3892
|
-
function buildMemoryOnlyFilters2(collection, filters) {
|
|
3893
|
-
return buildDocTypeFilters2(collection, "memory", filters);
|
|
3894
|
-
}
|
|
3895
|
-
function combineFiltersWithAnd2(filters) {
|
|
3896
|
-
const validFilters = filters.filter((f) => f !== void 0 && f !== null);
|
|
3897
|
-
if (validFilters.length === 0) {
|
|
3898
|
-
return void 0;
|
|
3899
|
-
}
|
|
3900
|
-
if (validFilters.length === 1) {
|
|
3901
|
-
return validFilters[0];
|
|
3902
|
-
}
|
|
3903
|
-
return Filters6.and(...validFilters);
|
|
3904
|
-
}
|
|
3905
|
-
function combineFiltersWithOr2(filters) {
|
|
3906
|
-
const validFilters = filters.filter((f) => f !== void 0 && f !== null);
|
|
3907
|
-
if (validFilters.length === 0) {
|
|
3908
|
-
return void 0;
|
|
3909
|
-
}
|
|
3910
|
-
if (validFilters.length === 1) {
|
|
3911
|
-
return validFilters[0];
|
|
3912
|
-
}
|
|
3913
|
-
return Filters6.or(...validFilters);
|
|
3914
|
-
}
|
|
3915
|
-
function buildDeletedFilter2(collection, deletedFilter = "exclude") {
|
|
3916
|
-
if (deletedFilter === "exclude") {
|
|
3917
|
-
return collection.filter.byProperty("deleted_at").isNull(true);
|
|
3918
|
-
} else if (deletedFilter === "only") {
|
|
3919
|
-
return collection.filter.byProperty("deleted_at").isNull(false);
|
|
3920
|
-
}
|
|
3921
|
-
return null;
|
|
3922
|
-
}
|
|
3923
|
-
|
|
3924
|
-
// src/services/trust-enforcement.ts
|
|
3925
|
-
function buildTrustFilter2(collection, accessorTrustLevel) {
|
|
3926
|
-
return collection.filter.byProperty("trust_score").lessThanOrEqual(accessorTrustLevel);
|
|
3927
|
-
}
|
|
3928
|
-
|
|
3929
|
-
// src/tools/search-memory.ts
|
|
3930
3824
|
var searchMemoryTool = {
|
|
3931
3825
|
name: "remember_search_memory",
|
|
3932
3826
|
description: `Search memories AND relationships using hybrid semantic and keyword search.
|
|
@@ -4057,16 +3951,16 @@ async function handleSearchMemory(args, userId, authContext) {
|
|
|
4057
3951
|
const alpha = args.alpha ?? 0.7;
|
|
4058
3952
|
const limit = args.limit ?? 10;
|
|
4059
3953
|
const offset = args.offset ?? 0;
|
|
4060
|
-
const deletedFilter =
|
|
4061
|
-
const trustFilter = ghostMode ?
|
|
4062
|
-
const searchFilters = includeRelationships ?
|
|
3954
|
+
const deletedFilter = buildDeletedFilter(collection, args.deleted_filter || "exclude");
|
|
3955
|
+
const trustFilter = ghostMode ? buildTrustFilter(collection, ghostMode.accessor_trust_level) : null;
|
|
3956
|
+
const searchFilters = includeRelationships ? buildCombinedSearchFilters(collection, args.filters) : buildMemoryOnlyFilters(collection, args.filters);
|
|
4063
3957
|
const hasExplicitTypeFilter = args.filters?.types && args.filters.types.length > 0;
|
|
4064
3958
|
const ghostExclusionFilter = !hasExplicitTypeFilter ? collection.filter.byProperty("content_type").notEqual("ghost") : null;
|
|
4065
|
-
const combinedFilters =
|
|
3959
|
+
const combinedFilters = combineFiltersWithAnd([deletedFilter, trustFilter, ghostExclusionFilter, searchFilters].filter((f) => f !== null));
|
|
4066
3960
|
const searchOptions = {
|
|
4067
3961
|
alpha,
|
|
4068
|
-
limit
|
|
4069
|
-
|
|
3962
|
+
limit,
|
|
3963
|
+
offset
|
|
4070
3964
|
};
|
|
4071
3965
|
if (combinedFilters) {
|
|
4072
3966
|
searchOptions.filters = combinedFilters;
|
|
@@ -4078,10 +3972,9 @@ async function handleSearchMemory(args, userId, authContext) {
|
|
|
4078
3972
|
deletedFilter: args.deleted_filter || "exclude"
|
|
4079
3973
|
});
|
|
4080
3974
|
const results = await collection.query.hybrid(args.query, searchOptions);
|
|
4081
|
-
const paginatedResults = results.objects.slice(offset);
|
|
4082
3975
|
const memories = [];
|
|
4083
3976
|
const relationships = [];
|
|
4084
|
-
for (const obj of
|
|
3977
|
+
for (const obj of results.objects) {
|
|
4085
3978
|
const doc = {
|
|
4086
3979
|
id: obj.uuid,
|
|
4087
3980
|
...obj.properties
|
|
@@ -4524,12 +4417,12 @@ async function handleQueryMemory(args, userId, authContext) {
|
|
|
4524
4417
|
const minRelevance = args.min_relevance ?? 0.6;
|
|
4525
4418
|
const includeContext = args.include_context ?? true;
|
|
4526
4419
|
const format = args.format ?? "detailed";
|
|
4527
|
-
const deletedFilter =
|
|
4528
|
-
const trustFilter = ghostMode ?
|
|
4529
|
-
const searchFilters =
|
|
4420
|
+
const deletedFilter = buildDeletedFilter(collection, args.deleted_filter || "exclude");
|
|
4421
|
+
const trustFilter = ghostMode ? buildTrustFilter(collection, ghostMode.accessor_trust_level) : null;
|
|
4422
|
+
const searchFilters = buildCombinedSearchFilters(collection, args.filters);
|
|
4530
4423
|
const hasExplicitTypeFilter = args.filters?.types && args.filters.types.length > 0;
|
|
4531
4424
|
const ghostExclusionFilter = !hasExplicitTypeFilter ? collection.filter.byProperty("content_type").notEqual("ghost") : null;
|
|
4532
|
-
const combinedFilters =
|
|
4425
|
+
const combinedFilters = combineFiltersWithAnd([deletedFilter, trustFilter, ghostExclusionFilter, searchFilters].filter((f) => f !== null));
|
|
4533
4426
|
const searchOptions = {
|
|
4534
4427
|
limit,
|
|
4535
4428
|
distance: 1 - minRelevance,
|
|
@@ -5641,7 +5534,7 @@ async function handleDeny(args, userId, authContext) {
|
|
|
5641
5534
|
}
|
|
5642
5535
|
|
|
5643
5536
|
// src/tools/search-space.ts
|
|
5644
|
-
import { Filters as
|
|
5537
|
+
import { Filters as Filters6 } from "weaviate-client";
|
|
5645
5538
|
var searchSpaceTool = {
|
|
5646
5539
|
name: "remember_search_space",
|
|
5647
5540
|
description: `Search shared spaces and/or groups to discover memories from other users.
|
|
@@ -6125,13 +6018,10 @@ async function removeUserTrust2(ownerUserId, targetUserId) {
|
|
|
6125
6018
|
});
|
|
6126
6019
|
}
|
|
6127
6020
|
async function blockUser2(ownerUserId, targetUserId) {
|
|
6128
|
-
const current = await getGhostConfig2(ownerUserId);
|
|
6129
|
-
if (current.blocked_users.includes(targetUserId)) {
|
|
6130
|
-
return;
|
|
6131
|
-
}
|
|
6132
|
-
const blocked_users = [...current.blocked_users, targetUserId];
|
|
6133
6021
|
const { collectionPath, docId } = getGhostConfigPath(ownerUserId);
|
|
6134
|
-
await setDocument(collectionPath, docId, {
|
|
6022
|
+
await setDocument(collectionPath, docId, {
|
|
6023
|
+
blocked_users: FieldValue.arrayUnion(targetUserId)
|
|
6024
|
+
}, { merge: true });
|
|
6135
6025
|
logger.info("User blocked from ghost access", {
|
|
6136
6026
|
service: SERVICE,
|
|
6137
6027
|
ownerUserId,
|
|
@@ -6139,13 +6029,10 @@ async function blockUser2(ownerUserId, targetUserId) {
|
|
|
6139
6029
|
});
|
|
6140
6030
|
}
|
|
6141
6031
|
async function unblockUser2(ownerUserId, targetUserId) {
|
|
6142
|
-
const current = await getGhostConfig2(ownerUserId);
|
|
6143
|
-
if (!current.blocked_users.includes(targetUserId)) {
|
|
6144
|
-
return;
|
|
6145
|
-
}
|
|
6146
|
-
const blocked_users = current.blocked_users.filter((id) => id !== targetUserId);
|
|
6147
6032
|
const { collectionPath, docId } = getGhostConfigPath(ownerUserId);
|
|
6148
|
-
await setDocument(collectionPath, docId, {
|
|
6033
|
+
await setDocument(collectionPath, docId, {
|
|
6034
|
+
blocked_users: FieldValue.arrayRemove(targetUserId)
|
|
6035
|
+
}, { merge: true });
|
|
6149
6036
|
logger.info("User unblocked from ghost access", {
|
|
6150
6037
|
service: SERVICE,
|
|
6151
6038
|
ownerUserId,
|
|
@@ -6361,8 +6248,10 @@ async function initServer() {
|
|
|
6361
6248
|
logger.info("Connecting to databases...");
|
|
6362
6249
|
await initWeaviateClient();
|
|
6363
6250
|
initFirestore();
|
|
6364
|
-
const weaviateOk = await
|
|
6365
|
-
|
|
6251
|
+
const [weaviateOk, firestoreOk] = await Promise.all([
|
|
6252
|
+
testWeaviateConnection(),
|
|
6253
|
+
testFirestoreConnection()
|
|
6254
|
+
]);
|
|
6366
6255
|
if (!weaviateOk || !firestoreOk) {
|
|
6367
6256
|
throw new Error("Database connection failed");
|
|
6368
6257
|
}
|
|
@@ -89,13 +89,13 @@ export declare function resetBlock(ownerUserId: string, accessorUserId: string,
|
|
|
89
89
|
/**
|
|
90
90
|
* Resolve the trust level for an accessor from GhostConfig.
|
|
91
91
|
*
|
|
92
|
-
* Priority: per_user_trust → default_friend_trust → default_public_trust → 0
|
|
92
|
+
* Priority: per_user_trust → default_friend_trust (if friends) → default_public_trust → 0
|
|
93
93
|
*
|
|
94
|
-
*
|
|
95
|
-
* context in M16 (friend list, social graph). For now, non-per_user accessors
|
|
96
|
-
* fall through to default_public_trust.
|
|
94
|
+
* Checks Firestore relationships collection to determine friend status.
|
|
97
95
|
*/
|
|
98
|
-
export declare function resolveAccessorTrustLevel(ghostConfig: GhostConfig, accessorUserId: string): number
|
|
96
|
+
export declare function resolveAccessorTrustLevel(ghostConfig: GhostConfig, ownerUserId: string, accessorUserId: string): Promise<number>;
|
|
97
|
+
/** Clear the friend status cache (for tests or forced refresh) */
|
|
98
|
+
export declare function invalidateFriendCache(): void;
|
|
99
99
|
/**
|
|
100
100
|
* Format an AccessResult into a human-readable message.
|
|
101
101
|
*/
|
|
@@ -30,10 +30,12 @@ export declare function setUserTrust(ownerUserId: string, targetUserId: string,
|
|
|
30
30
|
export declare function removeUserTrust(ownerUserId: string, targetUserId: string): Promise<void>;
|
|
31
31
|
/**
|
|
32
32
|
* Block a user from ghost access.
|
|
33
|
+
* Uses FieldValue.arrayUnion for atomic add without reading first (idempotent).
|
|
33
34
|
*/
|
|
34
35
|
export declare function blockUser(ownerUserId: string, targetUserId: string): Promise<void>;
|
|
35
36
|
/**
|
|
36
37
|
* Unblock a user from ghost access.
|
|
38
|
+
* Uses FieldValue.arrayRemove for atomic remove without reading first (safe if not present).
|
|
37
39
|
*/
|
|
38
40
|
export declare function unblockUser(ownerUserId: string, targetUserId: string): Promise<void>;
|
|
39
41
|
/**
|
package/package.json
CHANGED
package/src/core-services.ts
CHANGED
|
@@ -30,21 +30,35 @@ const coreLogger: Logger = createLogger('info');
|
|
|
30
30
|
const tokenService = new ConfirmationTokenService(coreLogger);
|
|
31
31
|
const preferencesService = new PreferencesDatabaseService(coreLogger);
|
|
32
32
|
|
|
33
|
+
/** Cached CoreServices per userId — avoids re-instantiation on every tool call */
|
|
34
|
+
const coreServicesCache = new Map<string, CoreServices>();
|
|
35
|
+
|
|
33
36
|
/**
|
|
34
|
-
* Create core services scoped to a specific user.
|
|
37
|
+
* Create (or return cached) core services scoped to a specific user.
|
|
35
38
|
* Call after databases have been initialized (initWeaviateClient + initFirestore).
|
|
36
39
|
*/
|
|
37
40
|
export function createCoreServices(userId: string): CoreServices {
|
|
41
|
+
const cached = coreServicesCache.get(userId);
|
|
42
|
+
if (cached) return cached;
|
|
43
|
+
|
|
38
44
|
const collection = getMemoryCollection(userId);
|
|
39
45
|
const weaviateClient = getWeaviateClient();
|
|
40
46
|
|
|
41
|
-
|
|
47
|
+
const services: CoreServices = {
|
|
42
48
|
memory: new MemoryService(collection, userId, coreLogger),
|
|
43
49
|
relationship: new RelationshipService(collection, userId, coreLogger),
|
|
44
50
|
space: new SpaceService(weaviateClient, collection, userId, tokenService, coreLogger),
|
|
45
51
|
preferences: preferencesService,
|
|
46
52
|
token: tokenService,
|
|
47
53
|
};
|
|
54
|
+
|
|
55
|
+
coreServicesCache.set(userId, services);
|
|
56
|
+
return services;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Clear the core services cache (for tests or forced refresh) */
|
|
60
|
+
export function invalidateCoreServicesCache(): void {
|
|
61
|
+
coreServicesCache.clear();
|
|
48
62
|
}
|
|
49
63
|
|
|
50
64
|
export { coreLogger, tokenService, preferencesService };
|
package/src/server-factory.ts
CHANGED
|
@@ -45,6 +45,10 @@ import { querySpaceTool, handleQuerySpace } from './tools/query-space.js';
|
|
|
45
45
|
import { moderateTool, handleModerate } from './tools/moderate.js';
|
|
46
46
|
import { ghostConfigTool, handleGhostConfig } from './tools/ghost-config.js';
|
|
47
47
|
|
|
48
|
+
// Import services (static — avoids dynamic import overhead on hot path)
|
|
49
|
+
import { getGhostConfig } from './services/ghost-config.service.js';
|
|
50
|
+
import { resolveAccessorTrustLevel } from './services/access-control.js';
|
|
51
|
+
|
|
48
52
|
export interface ServerOptions {
|
|
49
53
|
name?: string;
|
|
50
54
|
version?: string;
|
|
@@ -175,10 +179,8 @@ export async function createServer(
|
|
|
175
179
|
// Resolve ghost mode trust level from Firestore if ghost mode is configured
|
|
176
180
|
let resolvedGhostMode: import('./types/auth.js').GhostModeContext | undefined;
|
|
177
181
|
if (options.ghostMode) {
|
|
178
|
-
const { getGhostConfig } = await import('./services/ghost-config.service.js');
|
|
179
|
-
const { resolveAccessorTrustLevel } = await import('./services/access-control.js');
|
|
180
182
|
const ghostConfig = await getGhostConfig(options.ghostMode.owner_user_id);
|
|
181
|
-
const trustLevel = resolveAccessorTrustLevel(ghostConfig, options.ghostMode.accessor_user_id);
|
|
183
|
+
const trustLevel = await resolveAccessorTrustLevel(ghostConfig, options.ghostMode.owner_user_id, options.ghostMode.accessor_user_id);
|
|
182
184
|
resolvedGhostMode = {
|
|
183
185
|
owner_user_id: options.ghostMode.owner_user_id,
|
|
184
186
|
accessor_user_id: options.ghostMode.accessor_user_id,
|
package/src/server.ts
CHANGED
|
@@ -57,9 +57,11 @@ async function initServer(): Promise<Server> {
|
|
|
57
57
|
await initWeaviateClient();
|
|
58
58
|
initFirestore();
|
|
59
59
|
|
|
60
|
-
// Test connections
|
|
61
|
-
const weaviateOk = await
|
|
62
|
-
|
|
60
|
+
// Test connections in parallel
|
|
61
|
+
const [weaviateOk, firestoreOk] = await Promise.all([
|
|
62
|
+
testWeaviateConnection(),
|
|
63
|
+
testFirestoreConnection(),
|
|
64
|
+
]);
|
|
63
65
|
|
|
64
66
|
if (!weaviateOk || !firestoreOk) {
|
|
65
67
|
throw new Error('Database connection failed');
|
|
@@ -207,36 +207,36 @@ describe('checkMemoryAccess', () => {
|
|
|
207
207
|
// ─── resolveAccessorTrustLevel ─────────────────────────────────────────────
|
|
208
208
|
|
|
209
209
|
describe('resolveAccessorTrustLevel', () => {
|
|
210
|
-
it('returns per_user_trust when set', () => {
|
|
210
|
+
it('returns per_user_trust when set', async () => {
|
|
211
211
|
const config = createEnabledGhostConfig({ per_user_trust: { 'alice': 0.9 } });
|
|
212
|
-
expect(resolveAccessorTrustLevel(config, 'alice')).toBe(0.9);
|
|
212
|
+
expect(await resolveAccessorTrustLevel(config, 'owner', 'alice')).toBe(0.9);
|
|
213
213
|
});
|
|
214
214
|
|
|
215
|
-
it('falls through to default_public_trust when no per_user_trust', () => {
|
|
215
|
+
it('falls through to default_public_trust when no per_user_trust', async () => {
|
|
216
216
|
const config = createEnabledGhostConfig({ default_public_trust: 0.3 });
|
|
217
|
-
expect(resolveAccessorTrustLevel(config, 'stranger')).toBe(0.3);
|
|
217
|
+
expect(await resolveAccessorTrustLevel(config, 'owner', 'stranger')).toBe(0.3);
|
|
218
218
|
});
|
|
219
219
|
|
|
220
|
-
it('returns 0 when default_public_trust not set', () => {
|
|
220
|
+
it('returns 0 when default_public_trust not set', async () => {
|
|
221
221
|
const config = createEnabledGhostConfig({ default_public_trust: 0 });
|
|
222
|
-
expect(resolveAccessorTrustLevel(config, 'unknown')).toBe(0);
|
|
222
|
+
expect(await resolveAccessorTrustLevel(config, 'owner', 'unknown')).toBe(0);
|
|
223
223
|
});
|
|
224
224
|
|
|
225
|
-
it('per_user_trust takes priority over default', () => {
|
|
225
|
+
it('per_user_trust takes priority over default', async () => {
|
|
226
226
|
const config = createEnabledGhostConfig({
|
|
227
227
|
default_public_trust: 0.1,
|
|
228
228
|
per_user_trust: { 'bob': 0.8 },
|
|
229
229
|
});
|
|
230
|
-
expect(resolveAccessorTrustLevel(config, 'bob')).toBe(0.8);
|
|
231
|
-
expect(resolveAccessorTrustLevel(config, 'carol')).toBe(0.1);
|
|
230
|
+
expect(await resolveAccessorTrustLevel(config, 'owner', 'bob')).toBe(0.8);
|
|
231
|
+
expect(await resolveAccessorTrustLevel(config, 'owner', 'carol')).toBe(0.1);
|
|
232
232
|
});
|
|
233
233
|
|
|
234
|
-
it('per_user_trust of 0 is used (not falsy fallthrough)', () => {
|
|
234
|
+
it('per_user_trust of 0 is used (not falsy fallthrough)', async () => {
|
|
235
235
|
const config = createEnabledGhostConfig({
|
|
236
236
|
default_public_trust: 0.5,
|
|
237
237
|
per_user_trust: { 'restricted': 0 },
|
|
238
238
|
});
|
|
239
|
-
expect(resolveAccessorTrustLevel(config, 'restricted')).toBe(0);
|
|
239
|
+
expect(await resolveAccessorTrustLevel(config, 'owner', 'restricted')).toBe(0);
|
|
240
240
|
});
|
|
241
241
|
});
|
|
242
242
|
|
|
@@ -16,7 +16,7 @@ import type { Memory } from '../types/memory.js';
|
|
|
16
16
|
import type { AccessResult } from '../types/access-result.js';
|
|
17
17
|
import type { GhostConfig } from '../types/ghost-config.js';
|
|
18
18
|
import { DEFAULT_GHOST_CONFIG } from '../types/ghost-config.js';
|
|
19
|
-
import { isTrustSufficient } from '
|
|
19
|
+
import { isTrustSufficient } from '@prmichaelsen/remember-core';
|
|
20
20
|
|
|
21
21
|
// ─── Types ─────────────────────────────────────────────────────────────────
|
|
22
22
|
|
|
@@ -164,7 +164,7 @@ export async function checkMemoryAccess(
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
// 5. Check trust level
|
|
167
|
-
const accessorTrust = resolveAccessorTrustLevel(ghostConfig, accessorUserId);
|
|
167
|
+
const accessorTrust = await resolveAccessorTrustLevel(ghostConfig, ownerUserId, accessorUserId);
|
|
168
168
|
const memoryTrust = memory.trust;
|
|
169
169
|
|
|
170
170
|
if (!isTrustSufficient(memoryTrust, accessorTrust)) {
|
|
@@ -250,22 +250,88 @@ export async function resetBlock(
|
|
|
250
250
|
/**
|
|
251
251
|
* Resolve the trust level for an accessor from GhostConfig.
|
|
252
252
|
*
|
|
253
|
-
* Priority: per_user_trust → default_friend_trust → default_public_trust → 0
|
|
253
|
+
* Priority: per_user_trust → default_friend_trust (if friends) → default_public_trust → 0
|
|
254
254
|
*
|
|
255
|
-
*
|
|
256
|
-
* context in M16 (friend list, social graph). For now, non-per_user accessors
|
|
257
|
-
* fall through to default_public_trust.
|
|
255
|
+
* Checks Firestore relationships collection to determine friend status.
|
|
258
256
|
*/
|
|
259
|
-
export function resolveAccessorTrustLevel(
|
|
257
|
+
export async function resolveAccessorTrustLevel(
|
|
258
|
+
ghostConfig: GhostConfig,
|
|
259
|
+
ownerUserId: string,
|
|
260
|
+
accessorUserId: string
|
|
261
|
+
): Promise<number> {
|
|
260
262
|
// 1. Per-user override
|
|
261
263
|
if (accessorUserId in ghostConfig.per_user_trust) {
|
|
262
264
|
return ghostConfig.per_user_trust[accessorUserId];
|
|
263
265
|
}
|
|
264
266
|
|
|
265
|
-
// 2.
|
|
267
|
+
// 2. Check if accessor is a friend
|
|
268
|
+
const isFriend = await checkIfFriend(ownerUserId, accessorUserId);
|
|
269
|
+
|
|
270
|
+
if (isFriend) {
|
|
271
|
+
return ghostConfig.default_friend_trust ?? 0.25;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// 3. Fall through to public trust
|
|
266
275
|
return ghostConfig.default_public_trust ?? 0;
|
|
267
276
|
}
|
|
268
277
|
|
|
278
|
+
// ─── Friend Cache ─────────────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
/** TTL cache for friend status lookups (avoids redundant Firestore reads) */
|
|
281
|
+
const friendCache = new Map<string, { result: boolean; expiresAt: number }>();
|
|
282
|
+
const FRIEND_CACHE_TTL_MS = 60_000; // 60 seconds
|
|
283
|
+
|
|
284
|
+
/** Clear the friend status cache (for tests or forced refresh) */
|
|
285
|
+
export function invalidateFriendCache(): void {
|
|
286
|
+
friendCache.clear();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Check if accessor is a friend of owner by querying relationships collection.
|
|
291
|
+
* Results are cached for 60 seconds per user pair.
|
|
292
|
+
*/
|
|
293
|
+
async function checkIfFriend(ownerUserId: string, accessorUserId: string): Promise<boolean> {
|
|
294
|
+
// Check cache first (sorted key so a:b and b:a hit the same entry)
|
|
295
|
+
const cacheKey = [ownerUserId, accessorUserId].sort().join(':');
|
|
296
|
+
const cached = friendCache.get(cacheKey);
|
|
297
|
+
if (cached && Date.now() < cached.expiresAt) {
|
|
298
|
+
return cached.result;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
const { queryDocuments } = await import('../firestore/init.js');
|
|
303
|
+
const BASE = process.env.FIRESTORE_BASE_PATH || 'agentbase';
|
|
304
|
+
|
|
305
|
+
// Run both direction queries in parallel
|
|
306
|
+
const [forward, reverse] = await Promise.all([
|
|
307
|
+
queryDocuments(`${BASE}.relationships`, {
|
|
308
|
+
where: [
|
|
309
|
+
{ field: 'from_user_id', op: '==', value: ownerUserId },
|
|
310
|
+
{ field: 'to_user_id', op: '==', value: accessorUserId },
|
|
311
|
+
{ field: 'friend', op: '==', value: true },
|
|
312
|
+
],
|
|
313
|
+
limit: 1,
|
|
314
|
+
}),
|
|
315
|
+
queryDocuments(`${BASE}.relationships`, {
|
|
316
|
+
where: [
|
|
317
|
+
{ field: 'from_user_id', op: '==', value: accessorUserId },
|
|
318
|
+
{ field: 'to_user_id', op: '==', value: ownerUserId },
|
|
319
|
+
{ field: 'friend', op: '==', value: true },
|
|
320
|
+
],
|
|
321
|
+
limit: 1,
|
|
322
|
+
}),
|
|
323
|
+
]);
|
|
324
|
+
|
|
325
|
+
const result = forward.length > 0 || reverse.length > 0;
|
|
326
|
+
friendCache.set(cacheKey, { result, expiresAt: Date.now() + FRIEND_CACHE_TTL_MS });
|
|
327
|
+
return result;
|
|
328
|
+
} catch (error) {
|
|
329
|
+
console.error('[checkIfFriend] Error checking friend status:', error);
|
|
330
|
+
// On error, treat as not friends (safer default)
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
269
335
|
// ─── Message Formatting ───────────────────────────────────────────────────
|
|
270
336
|
|
|
271
337
|
/**
|
|
@@ -15,6 +15,10 @@ import * as firestoreInit from '../firestore/init';
|
|
|
15
15
|
jest.mock('../firestore/init', () => ({
|
|
16
16
|
getDocument: jest.fn(),
|
|
17
17
|
setDocument: jest.fn(),
|
|
18
|
+
FieldValue: {
|
|
19
|
+
arrayUnion: (...elements: any[]) => ({ _type: 'arrayUnion', _value: elements }),
|
|
20
|
+
arrayRemove: (...elements: any[]) => ({ _type: 'arrayRemove', _value: elements }),
|
|
21
|
+
},
|
|
18
22
|
}));
|
|
19
23
|
|
|
20
24
|
jest.mock('../firestore/paths', () => ({
|
|
@@ -179,8 +183,7 @@ describe('GhostConfigService', () => {
|
|
|
179
183
|
});
|
|
180
184
|
|
|
181
185
|
describe('blockUser', () => {
|
|
182
|
-
it('adds user to blocked list', async () => {
|
|
183
|
-
mockGetDocument.mockResolvedValue({ blocked_users: [] });
|
|
186
|
+
it('adds user to blocked list using FieldValue.arrayUnion', async () => {
|
|
184
187
|
mockSetDocument.mockResolvedValue(undefined);
|
|
185
188
|
|
|
186
189
|
await blockUser('owner-1', 'bad-user');
|
|
@@ -188,21 +191,23 @@ describe('GhostConfigService', () => {
|
|
|
188
191
|
expect(mockSetDocument).toHaveBeenCalledWith(
|
|
189
192
|
'test-remember-mcp.users/owner-1/ghost_config',
|
|
190
193
|
'settings',
|
|
191
|
-
{ blocked_users: ['bad-user'] },
|
|
194
|
+
{ blocked_users: { _type: 'arrayUnion', _value: ['bad-user'] } },
|
|
192
195
|
{ merge: true }
|
|
193
196
|
);
|
|
197
|
+
// No read needed — arrayUnion is idempotent
|
|
198
|
+
expect(mockGetDocument).not.toHaveBeenCalled();
|
|
194
199
|
});
|
|
195
200
|
|
|
196
|
-
it('does not duplicate already blocked user', async () => {
|
|
197
|
-
|
|
201
|
+
it('does not duplicate already blocked user (arrayUnion is idempotent)', async () => {
|
|
202
|
+
mockSetDocument.mockResolvedValue(undefined);
|
|
198
203
|
|
|
199
204
|
await blockUser('owner-1', 'bad-user');
|
|
200
205
|
|
|
201
|
-
|
|
206
|
+
// arrayUnion handles dedup atomically — always writes
|
|
207
|
+
expect(mockSetDocument).toHaveBeenCalledTimes(1);
|
|
202
208
|
});
|
|
203
209
|
|
|
204
|
-
it('preserves existing blocked users', async () => {
|
|
205
|
-
mockGetDocument.mockResolvedValue({ blocked_users: ['user-a'] });
|
|
210
|
+
it('preserves existing blocked users (arrayUnion appends atomically)', async () => {
|
|
206
211
|
mockSetDocument.mockResolvedValue(undefined);
|
|
207
212
|
|
|
208
213
|
await blockUser('owner-1', 'user-b');
|
|
@@ -210,15 +215,14 @@ describe('GhostConfigService', () => {
|
|
|
210
215
|
expect(mockSetDocument).toHaveBeenCalledWith(
|
|
211
216
|
'test-remember-mcp.users/owner-1/ghost_config',
|
|
212
217
|
'settings',
|
|
213
|
-
{ blocked_users:
|
|
218
|
+
{ blocked_users: { _type: 'arrayUnion', _value: ['user-b'] } },
|
|
214
219
|
{ merge: true }
|
|
215
220
|
);
|
|
216
221
|
});
|
|
217
222
|
});
|
|
218
223
|
|
|
219
224
|
describe('unblockUser', () => {
|
|
220
|
-
it('removes user from blocked list', async () => {
|
|
221
|
-
mockGetDocument.mockResolvedValue({ blocked_users: ['bad-user', 'other'] });
|
|
225
|
+
it('removes user from blocked list using FieldValue.arrayRemove', async () => {
|
|
222
226
|
mockSetDocument.mockResolvedValue(undefined);
|
|
223
227
|
|
|
224
228
|
await unblockUser('owner-1', 'bad-user');
|
|
@@ -226,17 +230,20 @@ describe('GhostConfigService', () => {
|
|
|
226
230
|
expect(mockSetDocument).toHaveBeenCalledWith(
|
|
227
231
|
'test-remember-mcp.users/owner-1/ghost_config',
|
|
228
232
|
'settings',
|
|
229
|
-
{ blocked_users: ['
|
|
233
|
+
{ blocked_users: { _type: 'arrayRemove', _value: ['bad-user'] } },
|
|
230
234
|
{ merge: true }
|
|
231
235
|
);
|
|
236
|
+
// No read needed — arrayRemove is safe if element not present
|
|
237
|
+
expect(mockGetDocument).not.toHaveBeenCalled();
|
|
232
238
|
});
|
|
233
239
|
|
|
234
|
-
it('
|
|
235
|
-
|
|
240
|
+
it('is safe when user not blocked (arrayRemove is idempotent)', async () => {
|
|
241
|
+
mockSetDocument.mockResolvedValue(undefined);
|
|
236
242
|
|
|
237
243
|
await unblockUser('owner-1', 'not-blocked');
|
|
238
244
|
|
|
239
|
-
|
|
245
|
+
// arrayRemove always writes — safe no-op if element not present
|
|
246
|
+
expect(mockSetDocument).toHaveBeenCalledTimes(1);
|
|
240
247
|
});
|
|
241
248
|
});
|
|
242
249
|
|