claude-memory-layer 1.0.6 → 1.0.8
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/.claude/settings.local.json +4 -1
- package/.claude-plugin/plugin.json +3 -3
- package/.history/package_20260201142928.json +46 -0
- package/.history/package_20260201192048.json +47 -0
- package/README.md +26 -26
- package/dist/.claude-plugin/plugin.json +3 -3
- package/dist/cli/index.js +1109 -25
- package/dist/cli/index.js.map +4 -4
- package/dist/core/index.js +192 -5
- package/dist/core/index.js.map +4 -4
- package/dist/hooks/session-end.js +262 -18
- package/dist/hooks/session-end.js.map +4 -4
- package/dist/hooks/session-start.js +262 -18
- package/dist/hooks/session-start.js.map +4 -4
- package/dist/hooks/stop.js +262 -18
- package/dist/hooks/stop.js.map +4 -4
- package/dist/hooks/user-prompt-submit.js +262 -18
- package/dist/hooks/user-prompt-submit.js.map +4 -4
- package/dist/server/api/index.js +4728 -0
- package/dist/server/api/index.js.map +7 -0
- package/dist/server/index.js +4790 -0
- package/dist/server/index.js.map +7 -0
- package/dist/services/memory-service.js +269 -18
- package/dist/services/memory-service.js.map +4 -4
- package/dist/ui/index.html +1225 -0
- package/package.json +4 -2
- package/scripts/build.ts +33 -3
- package/src/cli/index.ts +311 -6
- package/src/core/db-wrapper.ts +8 -1
- package/src/core/event-store.ts +52 -3
- package/src/core/graduation-worker.ts +171 -0
- package/src/core/graduation.ts +15 -2
- package/src/core/index.ts +1 -0
- package/src/core/retriever.ts +18 -0
- package/src/core/types.ts +1 -1
- package/src/mcp/index.ts +2 -2
- package/src/mcp/tools.ts +1 -1
- package/src/server/api/citations.ts +7 -3
- package/src/server/api/events.ts +7 -3
- package/src/server/api/search.ts +7 -3
- package/src/server/api/sessions.ts +7 -3
- package/src/server/api/stats.ts +175 -5
- package/src/server/index.ts +18 -9
- package/src/services/memory-service.ts +107 -19
- package/src/ui/index.html +1225 -0
|
@@ -66,7 +66,10 @@ function toDate(value) {
|
|
|
66
66
|
return new Date(value);
|
|
67
67
|
return new Date(String(value));
|
|
68
68
|
}
|
|
69
|
-
function createDatabase(path2) {
|
|
69
|
+
function createDatabase(path2, options) {
|
|
70
|
+
if (options?.readOnly) {
|
|
71
|
+
return new duckdb.Database(path2, { access_mode: "READ_ONLY" });
|
|
72
|
+
}
|
|
70
73
|
return new duckdb.Database(path2);
|
|
71
74
|
}
|
|
72
75
|
function dbRun(db, sql, params = []) {
|
|
@@ -120,18 +123,24 @@ function dbClose(db) {
|
|
|
120
123
|
|
|
121
124
|
// src/core/event-store.ts
|
|
122
125
|
var EventStore = class {
|
|
123
|
-
constructor(dbPath) {
|
|
126
|
+
constructor(dbPath, options) {
|
|
124
127
|
this.dbPath = dbPath;
|
|
125
|
-
this.
|
|
128
|
+
this.readOnly = options?.readOnly ?? false;
|
|
129
|
+
this.db = createDatabase(dbPath, { readOnly: this.readOnly });
|
|
126
130
|
}
|
|
127
131
|
db;
|
|
128
132
|
initialized = false;
|
|
133
|
+
readOnly;
|
|
129
134
|
/**
|
|
130
135
|
* Initialize database schema
|
|
131
136
|
*/
|
|
132
137
|
async initialize() {
|
|
133
138
|
if (this.initialized)
|
|
134
139
|
return;
|
|
140
|
+
if (this.readOnly) {
|
|
141
|
+
this.initialized = true;
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
135
144
|
await dbRun(this.db, `
|
|
136
145
|
CREATE TABLE IF NOT EXISTS events (
|
|
137
146
|
id VARCHAR PRIMARY KEY,
|
|
@@ -608,6 +617,36 @@ var EventStore = class {
|
|
|
608
617
|
);
|
|
609
618
|
return rows;
|
|
610
619
|
}
|
|
620
|
+
/**
|
|
621
|
+
* Get events by memory level
|
|
622
|
+
*/
|
|
623
|
+
async getEventsByLevel(level, options) {
|
|
624
|
+
await this.initialize();
|
|
625
|
+
const limit = options?.limit || 50;
|
|
626
|
+
const offset = options?.offset || 0;
|
|
627
|
+
const rows = await dbAll(
|
|
628
|
+
this.db,
|
|
629
|
+
`SELECT e.* FROM events e
|
|
630
|
+
INNER JOIN memory_levels ml ON e.id = ml.event_id
|
|
631
|
+
WHERE ml.level = ?
|
|
632
|
+
ORDER BY e.timestamp DESC
|
|
633
|
+
LIMIT ? OFFSET ?`,
|
|
634
|
+
[level, limit, offset]
|
|
635
|
+
);
|
|
636
|
+
return rows.map((row) => this.rowToEvent(row));
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Get memory level for a specific event
|
|
640
|
+
*/
|
|
641
|
+
async getEventLevel(eventId) {
|
|
642
|
+
await this.initialize();
|
|
643
|
+
const rows = await dbAll(
|
|
644
|
+
this.db,
|
|
645
|
+
`SELECT level FROM memory_levels WHERE event_id = ?`,
|
|
646
|
+
[eventId]
|
|
647
|
+
);
|
|
648
|
+
return rows.length > 0 ? rows[0].level : null;
|
|
649
|
+
}
|
|
611
650
|
// ============================================================
|
|
612
651
|
// Endless Mode Helper Methods
|
|
613
652
|
// ============================================================
|
|
@@ -1190,6 +1229,7 @@ var Retriever = class {
|
|
|
1190
1229
|
matcher;
|
|
1191
1230
|
sharedStore;
|
|
1192
1231
|
sharedVectorStore;
|
|
1232
|
+
graduation;
|
|
1193
1233
|
constructor(eventStore, vectorStore, embedder, matcher, sharedOptions) {
|
|
1194
1234
|
this.eventStore = eventStore;
|
|
1195
1235
|
this.vectorStore = vectorStore;
|
|
@@ -1198,6 +1238,12 @@ var Retriever = class {
|
|
|
1198
1238
|
this.sharedStore = sharedOptions?.sharedStore;
|
|
1199
1239
|
this.sharedVectorStore = sharedOptions?.sharedVectorStore;
|
|
1200
1240
|
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Set graduation pipeline for access tracking
|
|
1243
|
+
*/
|
|
1244
|
+
setGraduationPipeline(graduation) {
|
|
1245
|
+
this.graduation = graduation;
|
|
1246
|
+
}
|
|
1201
1247
|
/**
|
|
1202
1248
|
* Set shared stores after construction
|
|
1203
1249
|
*/
|
|
@@ -1320,6 +1366,13 @@ var Retriever = class {
|
|
|
1320
1366
|
const event = await this.eventStore.getEvent(result.eventId);
|
|
1321
1367
|
if (!event)
|
|
1322
1368
|
continue;
|
|
1369
|
+
if (this.graduation) {
|
|
1370
|
+
this.graduation.recordAccess(
|
|
1371
|
+
event.id,
|
|
1372
|
+
options.sessionId || "unknown",
|
|
1373
|
+
result.score
|
|
1374
|
+
);
|
|
1375
|
+
}
|
|
1323
1376
|
let sessionContext;
|
|
1324
1377
|
if (options.includeSessionContext) {
|
|
1325
1378
|
sessionContext = await this.getSessionContext(event.sessionId, event.id);
|
|
@@ -1441,15 +1494,26 @@ var GraduationPipeline = class {
|
|
|
1441
1494
|
L3toL4: { ...DEFAULT_CRITERIA.L3toL4, ...criteria.L3toL4 }
|
|
1442
1495
|
};
|
|
1443
1496
|
}
|
|
1497
|
+
// Track which sessions have accessed each event
|
|
1498
|
+
sessionAccesses = /* @__PURE__ */ new Map();
|
|
1444
1499
|
/**
|
|
1445
1500
|
* Record an access to an event (used for graduation scoring)
|
|
1446
1501
|
*/
|
|
1447
1502
|
recordAccess(eventId, fromSessionId, confidence = 1) {
|
|
1448
1503
|
const existing = this.metrics.get(eventId);
|
|
1504
|
+
if (!this.sessionAccesses.has(eventId)) {
|
|
1505
|
+
this.sessionAccesses.set(eventId, /* @__PURE__ */ new Set());
|
|
1506
|
+
}
|
|
1507
|
+
const sessions = this.sessionAccesses.get(eventId);
|
|
1508
|
+
const isNewSession = !sessions.has(fromSessionId);
|
|
1509
|
+
sessions.add(fromSessionId);
|
|
1449
1510
|
if (existing) {
|
|
1450
1511
|
existing.accessCount++;
|
|
1451
1512
|
existing.lastAccessed = /* @__PURE__ */ new Date();
|
|
1452
1513
|
existing.confidence = Math.max(existing.confidence, confidence);
|
|
1514
|
+
if (isNewSession && sessions.size > 1) {
|
|
1515
|
+
existing.crossSessionRefs = sessions.size - 1;
|
|
1516
|
+
}
|
|
1453
1517
|
} else {
|
|
1454
1518
|
this.metrics.set(eventId, {
|
|
1455
1519
|
eventId,
|
|
@@ -3337,6 +3401,127 @@ function createContinuityManager(eventStore, config) {
|
|
|
3337
3401
|
return new ContinuityManager(eventStore, config);
|
|
3338
3402
|
}
|
|
3339
3403
|
|
|
3404
|
+
// src/core/graduation-worker.ts
|
|
3405
|
+
var DEFAULT_CONFIG4 = {
|
|
3406
|
+
evaluationIntervalMs: 3e5,
|
|
3407
|
+
// 5 minutes
|
|
3408
|
+
batchSize: 50,
|
|
3409
|
+
cooldownMs: 36e5
|
|
3410
|
+
// 1 hour cooldown between evaluations
|
|
3411
|
+
};
|
|
3412
|
+
var GraduationWorker = class {
|
|
3413
|
+
constructor(eventStore, graduation, config = DEFAULT_CONFIG4) {
|
|
3414
|
+
this.eventStore = eventStore;
|
|
3415
|
+
this.graduation = graduation;
|
|
3416
|
+
this.config = config;
|
|
3417
|
+
}
|
|
3418
|
+
running = false;
|
|
3419
|
+
timeout = null;
|
|
3420
|
+
lastEvaluated = /* @__PURE__ */ new Map();
|
|
3421
|
+
/**
|
|
3422
|
+
* Start the graduation worker
|
|
3423
|
+
*/
|
|
3424
|
+
start() {
|
|
3425
|
+
if (this.running)
|
|
3426
|
+
return;
|
|
3427
|
+
this.running = true;
|
|
3428
|
+
this.scheduleNext();
|
|
3429
|
+
}
|
|
3430
|
+
/**
|
|
3431
|
+
* Stop the graduation worker
|
|
3432
|
+
*/
|
|
3433
|
+
stop() {
|
|
3434
|
+
this.running = false;
|
|
3435
|
+
if (this.timeout) {
|
|
3436
|
+
clearTimeout(this.timeout);
|
|
3437
|
+
this.timeout = null;
|
|
3438
|
+
}
|
|
3439
|
+
}
|
|
3440
|
+
/**
|
|
3441
|
+
* Check if currently running
|
|
3442
|
+
*/
|
|
3443
|
+
isRunning() {
|
|
3444
|
+
return this.running;
|
|
3445
|
+
}
|
|
3446
|
+
/**
|
|
3447
|
+
* Force a graduation evaluation run
|
|
3448
|
+
*/
|
|
3449
|
+
async forceRun() {
|
|
3450
|
+
return await this.runGraduation();
|
|
3451
|
+
}
|
|
3452
|
+
/**
|
|
3453
|
+
* Schedule the next graduation check
|
|
3454
|
+
*/
|
|
3455
|
+
scheduleNext() {
|
|
3456
|
+
if (!this.running)
|
|
3457
|
+
return;
|
|
3458
|
+
this.timeout = setTimeout(
|
|
3459
|
+
() => this.run(),
|
|
3460
|
+
this.config.evaluationIntervalMs
|
|
3461
|
+
);
|
|
3462
|
+
}
|
|
3463
|
+
/**
|
|
3464
|
+
* Run graduation evaluation
|
|
3465
|
+
*/
|
|
3466
|
+
async run() {
|
|
3467
|
+
if (!this.running)
|
|
3468
|
+
return;
|
|
3469
|
+
try {
|
|
3470
|
+
await this.runGraduation();
|
|
3471
|
+
} catch (error) {
|
|
3472
|
+
console.error("Graduation error:", error);
|
|
3473
|
+
}
|
|
3474
|
+
this.scheduleNext();
|
|
3475
|
+
}
|
|
3476
|
+
/**
|
|
3477
|
+
* Perform graduation evaluation across all levels
|
|
3478
|
+
*/
|
|
3479
|
+
async runGraduation() {
|
|
3480
|
+
const result = {
|
|
3481
|
+
evaluated: 0,
|
|
3482
|
+
graduated: 0,
|
|
3483
|
+
byLevel: {}
|
|
3484
|
+
};
|
|
3485
|
+
const levels = ["L0", "L1", "L2", "L3"];
|
|
3486
|
+
const now = Date.now();
|
|
3487
|
+
for (const level of levels) {
|
|
3488
|
+
const events = await this.eventStore.getEventsByLevel(level, {
|
|
3489
|
+
limit: this.config.batchSize
|
|
3490
|
+
});
|
|
3491
|
+
let levelGraduated = 0;
|
|
3492
|
+
for (const event of events) {
|
|
3493
|
+
const lastEval = this.lastEvaluated.get(event.id);
|
|
3494
|
+
if (lastEval && now - lastEval < this.config.cooldownMs) {
|
|
3495
|
+
continue;
|
|
3496
|
+
}
|
|
3497
|
+
result.evaluated++;
|
|
3498
|
+
this.lastEvaluated.set(event.id, now);
|
|
3499
|
+
const gradResult = await this.graduation.evaluateGraduation(event.id, level);
|
|
3500
|
+
if (gradResult.success) {
|
|
3501
|
+
result.graduated++;
|
|
3502
|
+
levelGraduated++;
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
if (levelGraduated > 0) {
|
|
3506
|
+
result.byLevel[level] = levelGraduated;
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
if (this.lastEvaluated.size > 1e3) {
|
|
3510
|
+
const entries = Array.from(this.lastEvaluated.entries());
|
|
3511
|
+
entries.sort((a, b) => b[1] - a[1]);
|
|
3512
|
+
this.lastEvaluated = new Map(entries.slice(0, 1e3));
|
|
3513
|
+
}
|
|
3514
|
+
return result;
|
|
3515
|
+
}
|
|
3516
|
+
};
|
|
3517
|
+
function createGraduationWorker(eventStore, graduation, config) {
|
|
3518
|
+
return new GraduationWorker(
|
|
3519
|
+
eventStore,
|
|
3520
|
+
graduation,
|
|
3521
|
+
{ ...DEFAULT_CONFIG4, ...config }
|
|
3522
|
+
);
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3340
3525
|
// src/services/memory-service.ts
|
|
3341
3526
|
function normalizePath(projectPath) {
|
|
3342
3527
|
const expanded = projectPath.startsWith("~") ? path.join(os.homedir(), projectPath.slice(1)) : projectPath;
|
|
@@ -3404,6 +3589,7 @@ var MemoryService = class {
|
|
|
3404
3589
|
retriever;
|
|
3405
3590
|
graduation;
|
|
3406
3591
|
vectorWorker = null;
|
|
3592
|
+
graduationWorker = null;
|
|
3407
3593
|
initialized = false;
|
|
3408
3594
|
// Endless Mode components
|
|
3409
3595
|
workingSetStore = null;
|
|
@@ -3418,14 +3604,16 @@ var MemoryService = class {
|
|
|
3418
3604
|
sharedPromoter = null;
|
|
3419
3605
|
sharedStoreConfig = null;
|
|
3420
3606
|
projectHash = null;
|
|
3607
|
+
readOnly;
|
|
3421
3608
|
constructor(config) {
|
|
3422
3609
|
const storagePath = this.expandPath(config.storagePath);
|
|
3423
|
-
|
|
3610
|
+
this.readOnly = config.readOnly ?? false;
|
|
3611
|
+
if (!this.readOnly && !fs.existsSync(storagePath)) {
|
|
3424
3612
|
fs.mkdirSync(storagePath, { recursive: true });
|
|
3425
3613
|
}
|
|
3426
3614
|
this.projectHash = config.projectHash || null;
|
|
3427
3615
|
this.sharedStoreConfig = config.sharedStoreConfig ?? { enabled: true };
|
|
3428
|
-
this.eventStore = new EventStore(path.join(storagePath, "events.duckdb"));
|
|
3616
|
+
this.eventStore = new EventStore(path.join(storagePath, "events.duckdb"), { readOnly: this.readOnly });
|
|
3429
3617
|
this.vectorStore = new VectorStore(path.join(storagePath, "vectors"));
|
|
3430
3618
|
this.embedder = config.embeddingModel ? new Embedder(config.embeddingModel) : getDefaultEmbedder();
|
|
3431
3619
|
this.matcher = getDefaultMatcher();
|
|
@@ -3446,19 +3634,27 @@ var MemoryService = class {
|
|
|
3446
3634
|
await this.eventStore.initialize();
|
|
3447
3635
|
await this.vectorStore.initialize();
|
|
3448
3636
|
await this.embedder.initialize();
|
|
3449
|
-
this.
|
|
3450
|
-
this.
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
this.
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3637
|
+
if (!this.readOnly) {
|
|
3638
|
+
this.vectorWorker = createVectorWorker(
|
|
3639
|
+
this.eventStore,
|
|
3640
|
+
this.vectorStore,
|
|
3641
|
+
this.embedder
|
|
3642
|
+
);
|
|
3643
|
+
this.vectorWorker.start();
|
|
3644
|
+
this.retriever.setGraduationPipeline(this.graduation);
|
|
3645
|
+
this.graduationWorker = createGraduationWorker(
|
|
3646
|
+
this.eventStore,
|
|
3647
|
+
this.graduation
|
|
3648
|
+
);
|
|
3649
|
+
this.graduationWorker.start();
|
|
3650
|
+
const savedMode = await this.eventStore.getEndlessConfig("mode");
|
|
3651
|
+
if (savedMode === "endless") {
|
|
3652
|
+
this.endlessMode = "endless";
|
|
3653
|
+
await this.initializeEndlessMode();
|
|
3654
|
+
}
|
|
3655
|
+
if (this.sharedStoreConfig?.enabled !== false) {
|
|
3656
|
+
await this.initializeSharedStore();
|
|
3657
|
+
}
|
|
3462
3658
|
}
|
|
3463
3659
|
this.initialized = true;
|
|
3464
3660
|
}
|
|
@@ -3639,6 +3835,20 @@ var MemoryService = class {
|
|
|
3639
3835
|
}
|
|
3640
3836
|
return 0;
|
|
3641
3837
|
}
|
|
3838
|
+
/**
|
|
3839
|
+
* Get events by memory level
|
|
3840
|
+
*/
|
|
3841
|
+
async getEventsByLevel(level, options) {
|
|
3842
|
+
await this.initialize();
|
|
3843
|
+
return this.eventStore.getEventsByLevel(level, options);
|
|
3844
|
+
}
|
|
3845
|
+
/**
|
|
3846
|
+
* Get memory level for a specific event
|
|
3847
|
+
*/
|
|
3848
|
+
async getEventLevel(eventId) {
|
|
3849
|
+
await this.initialize();
|
|
3850
|
+
return this.eventStore.getEventLevel(eventId);
|
|
3851
|
+
}
|
|
3642
3852
|
/**
|
|
3643
3853
|
* Format retrieval results as context for Claude
|
|
3644
3854
|
*/
|
|
@@ -3821,6 +4031,22 @@ var MemoryService = class {
|
|
|
3821
4031
|
return [];
|
|
3822
4032
|
return this.consolidatedStore.getAll({ limit });
|
|
3823
4033
|
}
|
|
4034
|
+
/**
|
|
4035
|
+
* Get most accessed consolidated memories
|
|
4036
|
+
*/
|
|
4037
|
+
async getMostAccessedMemories(limit = 10) {
|
|
4038
|
+
if (!this.consolidatedStore)
|
|
4039
|
+
return [];
|
|
4040
|
+
return this.consolidatedStore.getMostAccessed(limit);
|
|
4041
|
+
}
|
|
4042
|
+
/**
|
|
4043
|
+
* Mark a consolidated memory as accessed
|
|
4044
|
+
*/
|
|
4045
|
+
async markMemoryAccessed(memoryId) {
|
|
4046
|
+
if (!this.consolidatedStore)
|
|
4047
|
+
return;
|
|
4048
|
+
await this.consolidatedStore.markAccessed(memoryId);
|
|
4049
|
+
}
|
|
3824
4050
|
/**
|
|
3825
4051
|
* Calculate continuity score for current context
|
|
3826
4052
|
*/
|
|
@@ -3908,10 +4134,28 @@ var MemoryService = class {
|
|
|
3908
4134
|
}
|
|
3909
4135
|
return parts.join("\n");
|
|
3910
4136
|
}
|
|
4137
|
+
/**
|
|
4138
|
+
* Force a graduation evaluation run
|
|
4139
|
+
*/
|
|
4140
|
+
async forceGraduation() {
|
|
4141
|
+
if (!this.graduationWorker) {
|
|
4142
|
+
return { evaluated: 0, graduated: 0, byLevel: {} };
|
|
4143
|
+
}
|
|
4144
|
+
return this.graduationWorker.forceRun();
|
|
4145
|
+
}
|
|
4146
|
+
/**
|
|
4147
|
+
* Record access to a memory event (for graduation scoring)
|
|
4148
|
+
*/
|
|
4149
|
+
recordMemoryAccess(eventId, sessionId, confidence = 1) {
|
|
4150
|
+
this.graduation.recordAccess(eventId, sessionId, confidence);
|
|
4151
|
+
}
|
|
3911
4152
|
/**
|
|
3912
4153
|
* Shutdown service
|
|
3913
4154
|
*/
|
|
3914
4155
|
async shutdown() {
|
|
4156
|
+
if (this.graduationWorker) {
|
|
4157
|
+
this.graduationWorker.stop();
|
|
4158
|
+
}
|
|
3915
4159
|
if (this.consolidationWorker) {
|
|
3916
4160
|
this.consolidationWorker.stop();
|
|
3917
4161
|
}
|
|
@@ -3943,6 +4187,12 @@ function getDefaultMemoryService() {
|
|
|
3943
4187
|
}
|
|
3944
4188
|
return serviceCache.get(GLOBAL_KEY);
|
|
3945
4189
|
}
|
|
4190
|
+
function getReadOnlyMemoryService() {
|
|
4191
|
+
return new MemoryService({
|
|
4192
|
+
storagePath: "~/.claude-code/memory",
|
|
4193
|
+
readOnly: true
|
|
4194
|
+
});
|
|
4195
|
+
}
|
|
3946
4196
|
function getMemoryServiceForProject(projectPath, sharedStoreConfig) {
|
|
3947
4197
|
const hash = hashProjectPath(projectPath);
|
|
3948
4198
|
if (!serviceCache.has(hash)) {
|
|
@@ -3972,6 +4222,7 @@ export {
|
|
|
3972
4222
|
getMemoryServiceForProject,
|
|
3973
4223
|
getMemoryServiceForSession,
|
|
3974
4224
|
getProjectStoragePath,
|
|
4225
|
+
getReadOnlyMemoryService,
|
|
3975
4226
|
getSessionProject,
|
|
3976
4227
|
hashProjectPath,
|
|
3977
4228
|
registerSession
|