1414 * Designed to run as a managed sidecar alongside Hermes.
1515 */
1616
17+ import { createHash } from "node:crypto" ;
1718import http from "node:http" ;
19+ import path from "node:path" ;
1820import { URL } from "node:url" ;
1921import { TdaiCore } from "../core/tdai-core.js" ;
2022import { StandaloneHostAdapter } from "../adapters/standalone/host-adapter.js" ;
@@ -46,6 +48,43 @@ import type { SeedProgress } from "../core/seed/types.js";
4648const TAG = "[tdai-gateway]" ;
4749const VERSION = "0.1.0" ;
4850
51+ // ============================
52+ // User data scope
53+ // ============================
54+
55+ export interface GatewayUserScope {
56+ cacheKey : string ;
57+ dataDir : string ;
58+ isolated : boolean ;
59+ }
60+
61+ function safePathSegment ( value : string ) : string {
62+ const normalized = value . normalize ( "NFKC" ) . trim ( ) ;
63+ const slug = normalized
64+ . replace ( / [ ^ A - Z a - z 0 - 9 _ - ] + / g, "-" )
65+ . replace ( / ^ - + | - + $ / g, "" )
66+ . slice ( 0 , 48 ) || "user" ;
67+ const digest = createHash ( "sha256" ) . update ( normalized ) . digest ( "hex" ) . slice ( 0 , 12 ) ;
68+ return `${ slug } -${ digest } ` ;
69+ }
70+
71+ export function resolveGatewayUserScope ( baseDir : string , userId ?: string ) : GatewayUserScope {
72+ const identity = userId ?. trim ( ) ;
73+ if ( ! identity ) {
74+ return {
75+ cacheKey : "legacy" ,
76+ dataDir : baseDir ,
77+ isolated : false ,
78+ } ;
79+ }
80+
81+ return {
82+ cacheKey : `user:${ identity } ` ,
83+ dataDir : path . join ( baseDir , "users" , safePathSegment ( identity ) ) ,
84+ isolated : true ,
85+ } ;
86+ }
87+
4988// ============================
5089// Console logger (for standalone gateway — no OpenClaw logger available)
5190// ============================
@@ -100,6 +139,7 @@ export class TdaiGateway {
100139 private config : GatewayConfig ;
101140 private logger : Logger ;
102141 private core : TdaiCore ;
142+ private readonly scopedCores = new Map < string , Promise < TdaiCore > > ( ) ;
103143 private server : http . Server | null = null ;
104144 private startTime = Date . now ( ) ;
105145
@@ -121,6 +161,7 @@ export class TdaiGateway {
121161 config : this . config . memory ,
122162 sessionFilter : new SessionFilter ( this . config . memory . capture . excludeAgents ) ,
123163 } ) ;
164+ this . scopedCores . set ( "legacy" , Promise . resolve ( this . core ) ) ;
124165 }
125166
126167 /**
@@ -160,7 +201,17 @@ export class TdaiGateway {
160201 } ) ;
161202 }
162203
163- await this . core . destroy ( ) ;
204+ const corePromises = [ ...this . scopedCores . values ( ) ] ;
205+ const settled = await Promise . allSettled ( corePromises ) ;
206+ const cores = new Set < TdaiCore > ( [ this . core ] ) ;
207+ for ( const item of settled ) {
208+ if ( item . status === "fulfilled" ) cores . add ( item . value ) ;
209+ }
210+ for ( const core of cores ) {
211+ await core . destroy ( ) ;
212+ }
213+ this . scopedCores . clear ( ) ;
214+ this . scopedCores . set ( "legacy" , Promise . resolve ( this . core ) ) ;
164215 this . logger . info ( "Gateway stopped" ) ;
165216 }
166217
@@ -236,7 +287,8 @@ export class TdaiGateway {
236287 }
237288
238289 const startMs = Date . now ( ) ;
239- const result = await this . core . handleBeforeRecall ( body . query , body . session_key ) ;
290+ const core = await this . getCoreForUser ( body . user_id ) ;
291+ const result = await core . handleBeforeRecall ( body . query , body . session_key ) ;
240292 const elapsed = Date . now ( ) - startMs ;
241293
242294 this . logger . info ( `Recall completed in ${ elapsed } ms: context=${ ( result . appendSystemContext ?. length ?? 0 ) } chars` ) ;
@@ -258,7 +310,8 @@ export class TdaiGateway {
258310 }
259311
260312 const startMs = Date . now ( ) ;
261- const result = await this . core . handleTurnCommitted ( {
313+ const core = await this . getCoreForUser ( body . user_id ) ;
314+ const result = await core . handleTurnCommitted ( {
262315 userText : body . user_content ,
263316 assistantText : body . assistant_content ,
264317 messages : body . messages ?? [
@@ -287,7 +340,8 @@ export class TdaiGateway {
287340 return ;
288341 }
289342
290- const result = await this . core . searchMemories ( {
343+ const core = await this . getCoreForUser ( body . user_id ) ;
344+ const result = await core . searchMemories ( {
291345 query : body . query ,
292346 limit : body . limit ,
293347 type : body . type ,
@@ -310,7 +364,8 @@ export class TdaiGateway {
310364 return ;
311365 }
312366
313- const result = await this . core . searchConversations ( {
367+ const core = await this . getCoreForUser ( body . user_id ) ;
368+ const result = await core . searchConversations ( {
314369 query : body . query ,
315370 limit : body . limit ,
316371 sessionKey : body . session_key ,
@@ -331,7 +386,8 @@ export class TdaiGateway {
331386 return ;
332387 }
333388
334- await this . core . handleSessionEnd ( body . session_key ) ;
389+ const core = await this . getCoreForUser ( body . user_id ) ;
390+ await core . handleSessionEnd ( body . session_key ) ;
335391
336392 const response : SessionEndResponse = { flushed : true } ;
337393 sendJson ( res , 200 , response ) ;
@@ -375,7 +431,8 @@ export class TdaiGateway {
375431 const ts =
376432 `${ now . getFullYear ( ) } ${ pad ( now . getMonth ( ) + 1 ) } ${ pad ( now . getDate ( ) ) } -` +
377433 `${ pad ( now . getHours ( ) ) } ${ pad ( now . getMinutes ( ) ) } ${ pad ( now . getSeconds ( ) ) } ` ;
378- const outputDir = `${ this . config . data . baseDir } /seed-${ ts } ` ;
434+ const userScope = resolveGatewayUserScope ( this . config . data . baseDir , body . user_id ) ;
435+ const outputDir = path . join ( userScope . dataDir , `seed-${ ts } ` ) ;
379436
380437 // Merge config overrides if provided
381438 // Start with the base memory config + inject llm config from gateway settings
@@ -434,6 +491,42 @@ export class TdaiGateway {
434491 } ;
435492 sendJson ( res , 200 , response ) ;
436493 }
494+
495+ private async getCoreForUser ( userId ?: string ) : Promise < TdaiCore > {
496+ const scope = resolveGatewayUserScope ( this . config . data . baseDir , userId ) ;
497+ if ( ! scope . isolated ) return this . core ;
498+
499+ const existing = this . scopedCores . get ( scope . cacheKey ) ;
500+ if ( existing ) return existing ;
501+
502+ const corePromise = this . createScopedCore ( scope . dataDir ) . catch ( ( err ) => {
503+ this . scopedCores . delete ( scope . cacheKey ) ;
504+ throw err ;
505+ } ) ;
506+ this . scopedCores . set ( scope . cacheKey , corePromise ) ;
507+ return corePromise ;
508+ }
509+
510+ private async createScopedCore ( dataDir : string ) : Promise < TdaiCore > {
511+ initDataDirectories ( dataDir ) ;
512+
513+ const adapter = new StandaloneHostAdapter ( {
514+ dataDir,
515+ llmConfig : this . config . llm ,
516+ logger : this . logger ,
517+ platform : "gateway" ,
518+ } ) ;
519+
520+ const core = new TdaiCore ( {
521+ hostAdapter : adapter ,
522+ config : this . config . memory ,
523+ sessionFilter : new SessionFilter ( this . config . memory . capture . excludeAgents ) ,
524+ } ) ;
525+
526+ await core . initialize ( ) ;
527+ this . logger . info ( `Gateway user scope initialized: ${ dataDir } ` ) ;
528+ return core ;
529+ }
437530}
438531
439532// ============================
0 commit comments