@hir4ta/memoria 0.14.1 → 0.14.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/.claude-plugin/plugin.json +1 -1
- package/bin/memoria.js +8 -2
- package/dist/lib/db.js +39 -0
- package/dist/server.js +67 -9
- package/hooks/session-end.sh +29 -0
- package/hooks/session-start.sh +45 -0
- package/package.json +1 -1
- package/skills/resume/skill.md +73 -16
- package/skills/save/skill.md +99 -27
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memoria",
|
|
3
3
|
"description": "A plugin that provides long-term memory for Claude Code. It automatically saves context lost during auto-compact, offering features for session restoration, recording technical decisions, and learning developer patterns.",
|
|
4
|
-
"version": "0.14.
|
|
4
|
+
"version": "0.14.2",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "hir4ta"
|
|
7
7
|
},
|
package/bin/memoria.js
CHANGED
|
@@ -8,7 +8,11 @@ import { fileURLToPath } from "node:url";
|
|
|
8
8
|
// Suppress Node.js SQLite experimental warning (must be before dynamic import)
|
|
9
9
|
const originalEmit = process.emit;
|
|
10
10
|
process.emit = function (name, data, ...args) {
|
|
11
|
-
if (
|
|
11
|
+
if (
|
|
12
|
+
name === "warning" &&
|
|
13
|
+
data?.name === "ExperimentalWarning" &&
|
|
14
|
+
data?.message?.includes("SQLite")
|
|
15
|
+
) {
|
|
12
16
|
return false;
|
|
13
17
|
}
|
|
14
18
|
return originalEmit.call(process, name, data, ...args);
|
|
@@ -106,7 +110,9 @@ function initMemoria() {
|
|
|
106
110
|
}
|
|
107
111
|
db.close();
|
|
108
112
|
} catch (error) {
|
|
109
|
-
console.error(
|
|
113
|
+
console.error(
|
|
114
|
+
`Warning: Failed to initialize SQLite database: ${error.message}`,
|
|
115
|
+
);
|
|
110
116
|
}
|
|
111
117
|
|
|
112
118
|
console.log(`memoria initialized: ${memoriaDir}`);
|
package/dist/lib/db.js
CHANGED
|
@@ -88,6 +88,42 @@ function getInteractionsByOwner(db, sessionId, owner) {
|
|
|
88
88
|
`);
|
|
89
89
|
return stmt.all(sessionId, owner);
|
|
90
90
|
}
|
|
91
|
+
function getInteractionsBySessionIds(db, sessionIds) {
|
|
92
|
+
if (sessionIds.length === 0) {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
const placeholders = sessionIds.map(() => "?").join(", ");
|
|
96
|
+
const stmt = db.prepare(`
|
|
97
|
+
SELECT * FROM interactions
|
|
98
|
+
WHERE session_id IN (${placeholders})
|
|
99
|
+
ORDER BY timestamp ASC, session_id ASC, role ASC
|
|
100
|
+
`);
|
|
101
|
+
return stmt.all(...sessionIds);
|
|
102
|
+
}
|
|
103
|
+
function getInteractionsBySessionIdsAndOwner(db, sessionIds, owner) {
|
|
104
|
+
if (sessionIds.length === 0) {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
const placeholders = sessionIds.map(() => "?").join(", ");
|
|
108
|
+
const stmt = db.prepare(`
|
|
109
|
+
SELECT * FROM interactions
|
|
110
|
+
WHERE session_id IN (${placeholders}) AND owner = ?
|
|
111
|
+
ORDER BY timestamp ASC, session_id ASC, role ASC
|
|
112
|
+
`);
|
|
113
|
+
return stmt.all(...sessionIds, owner);
|
|
114
|
+
}
|
|
115
|
+
function hasInteractionsForSessionIds(db, sessionIds, owner) {
|
|
116
|
+
if (sessionIds.length === 0) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
const placeholders = sessionIds.map(() => "?").join(", ");
|
|
120
|
+
const stmt = db.prepare(`
|
|
121
|
+
SELECT COUNT(*) as count FROM interactions
|
|
122
|
+
WHERE session_id IN (${placeholders}) AND owner = ?
|
|
123
|
+
`);
|
|
124
|
+
const result = stmt.get(...sessionIds, owner);
|
|
125
|
+
return result.count > 0;
|
|
126
|
+
}
|
|
91
127
|
function hasInteractions(db, sessionId, owner) {
|
|
92
128
|
const stmt = db.prepare(`
|
|
93
129
|
SELECT COUNT(*) as count FROM interactions
|
|
@@ -157,8 +193,11 @@ export {
|
|
|
157
193
|
getDbStats,
|
|
158
194
|
getInteractions,
|
|
159
195
|
getInteractionsByOwner,
|
|
196
|
+
getInteractionsBySessionIds,
|
|
197
|
+
getInteractionsBySessionIdsAndOwner,
|
|
160
198
|
getLatestBackup,
|
|
161
199
|
hasInteractions,
|
|
200
|
+
hasInteractionsForSessionIds,
|
|
162
201
|
initDatabase,
|
|
163
202
|
insertInteractions,
|
|
164
203
|
insertPreCompactBackup,
|
package/dist/server.js
CHANGED
|
@@ -2915,20 +2915,28 @@ function openDatabase(memoriaDir2) {
|
|
|
2915
2915
|
db.exec("PRAGMA journal_mode = WAL");
|
|
2916
2916
|
return db;
|
|
2917
2917
|
}
|
|
2918
|
-
function
|
|
2918
|
+
function getInteractionsBySessionIdsAndOwner(db, sessionIds, owner) {
|
|
2919
|
+
if (sessionIds.length === 0) {
|
|
2920
|
+
return [];
|
|
2921
|
+
}
|
|
2922
|
+
const placeholders = sessionIds.map(() => "?").join(", ");
|
|
2919
2923
|
const stmt = db.prepare(`
|
|
2920
2924
|
SELECT * FROM interactions
|
|
2921
|
-
WHERE session_id = ?
|
|
2922
|
-
ORDER BY timestamp ASC
|
|
2925
|
+
WHERE session_id IN (${placeholders}) AND owner = ?
|
|
2926
|
+
ORDER BY timestamp ASC, session_id ASC, role ASC
|
|
2923
2927
|
`);
|
|
2924
|
-
return stmt.all(
|
|
2928
|
+
return stmt.all(...sessionIds, owner);
|
|
2925
2929
|
}
|
|
2926
|
-
function
|
|
2930
|
+
function hasInteractionsForSessionIds(db, sessionIds, owner) {
|
|
2931
|
+
if (sessionIds.length === 0) {
|
|
2932
|
+
return false;
|
|
2933
|
+
}
|
|
2934
|
+
const placeholders = sessionIds.map(() => "?").join(", ");
|
|
2927
2935
|
const stmt = db.prepare(`
|
|
2928
2936
|
SELECT COUNT(*) as count FROM interactions
|
|
2929
|
-
WHERE session_id
|
|
2937
|
+
WHERE session_id IN (${placeholders}) AND owner = ?
|
|
2930
2938
|
`);
|
|
2931
|
-
const result = stmt.get(
|
|
2939
|
+
const result = stmt.get(...sessionIds, owner);
|
|
2932
2940
|
return result.count > 0;
|
|
2933
2941
|
}
|
|
2934
2942
|
|
|
@@ -3553,13 +3561,59 @@ app.get("/api/current-user", async (c) => {
|
|
|
3553
3561
|
app.get("/api/sessions/:id/interactions", async (c) => {
|
|
3554
3562
|
const id = sanitizeId(c.req.param("id"));
|
|
3555
3563
|
const memoriaDir2 = getMemoriaDir();
|
|
3564
|
+
const sessionLinksDir = path3.join(memoriaDir2, "session-links");
|
|
3565
|
+
const sessionsDir = path3.join(memoriaDir2, "sessions");
|
|
3556
3566
|
try {
|
|
3557
3567
|
const currentUser = getCurrentUser();
|
|
3558
3568
|
const db = openDatabase(memoriaDir2);
|
|
3559
3569
|
if (!db) {
|
|
3560
3570
|
return c.json({ interactions: [], count: 0, isOwner: false });
|
|
3561
3571
|
}
|
|
3562
|
-
|
|
3572
|
+
let masterId = id;
|
|
3573
|
+
const myLinkFile = path3.join(sessionLinksDir, `${id}.json`);
|
|
3574
|
+
if (fs4.existsSync(myLinkFile)) {
|
|
3575
|
+
try {
|
|
3576
|
+
const myLinkData = JSON.parse(fs4.readFileSync(myLinkFile, "utf-8"));
|
|
3577
|
+
if (myLinkData.masterSessionId) {
|
|
3578
|
+
masterId = myLinkData.masterSessionId;
|
|
3579
|
+
}
|
|
3580
|
+
} catch {
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
const sessionIds = [masterId];
|
|
3584
|
+
if (masterId !== id) {
|
|
3585
|
+
sessionIds.push(id);
|
|
3586
|
+
}
|
|
3587
|
+
if (fs4.existsSync(sessionLinksDir)) {
|
|
3588
|
+
const linkFiles = fs4.readdirSync(sessionLinksDir);
|
|
3589
|
+
for (const linkFile of linkFiles) {
|
|
3590
|
+
if (!linkFile.endsWith(".json")) continue;
|
|
3591
|
+
const linkPath = path3.join(sessionLinksDir, linkFile);
|
|
3592
|
+
try {
|
|
3593
|
+
const linkData = JSON.parse(fs4.readFileSync(linkPath, "utf-8"));
|
|
3594
|
+
if (linkData.masterSessionId === masterId) {
|
|
3595
|
+
const childId = linkFile.replace(".json", "");
|
|
3596
|
+
if (!sessionIds.includes(childId)) {
|
|
3597
|
+
sessionIds.push(childId);
|
|
3598
|
+
}
|
|
3599
|
+
}
|
|
3600
|
+
} catch {
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
}
|
|
3604
|
+
const sessionFiles = listDatedJsonFiles(sessionsDir);
|
|
3605
|
+
for (const sessionFile of sessionFiles) {
|
|
3606
|
+
try {
|
|
3607
|
+
const sessionData = JSON.parse(fs4.readFileSync(sessionFile, "utf-8"));
|
|
3608
|
+
if (sessionData.resumedFrom === masterId && sessionData.id !== masterId) {
|
|
3609
|
+
if (!sessionIds.includes(sessionData.id)) {
|
|
3610
|
+
sessionIds.push(sessionData.id);
|
|
3611
|
+
}
|
|
3612
|
+
}
|
|
3613
|
+
} catch {
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3616
|
+
const isOwner = hasInteractionsForSessionIds(db, sessionIds, currentUser);
|
|
3563
3617
|
if (!isOwner) {
|
|
3564
3618
|
db.close();
|
|
3565
3619
|
return c.json(
|
|
@@ -3567,7 +3621,11 @@ app.get("/api/sessions/:id/interactions", async (c) => {
|
|
|
3567
3621
|
403
|
|
3568
3622
|
);
|
|
3569
3623
|
}
|
|
3570
|
-
const interactions =
|
|
3624
|
+
const interactions = getInteractionsBySessionIdsAndOwner(
|
|
3625
|
+
db,
|
|
3626
|
+
sessionIds,
|
|
3627
|
+
currentUser
|
|
3628
|
+
);
|
|
3571
3629
|
db.close();
|
|
3572
3630
|
const groupedInteractions = [];
|
|
3573
3631
|
let currentInteraction = null;
|
package/hooks/session-end.sh
CHANGED
|
@@ -38,6 +38,7 @@ fi
|
|
|
38
38
|
cwd=$(cd "$cwd" 2>/dev/null && pwd || echo "$cwd")
|
|
39
39
|
memoria_dir="${cwd}/.memoria"
|
|
40
40
|
sessions_dir="${memoria_dir}/sessions"
|
|
41
|
+
session_links_dir="${memoria_dir}/session-links"
|
|
41
42
|
db_path="${memoria_dir}/local.db"
|
|
42
43
|
|
|
43
44
|
# Find session file
|
|
@@ -274,4 +275,32 @@ else
|
|
|
274
275
|
echo "[memoria] Session completed (no transcript): ${session_file}" >&2
|
|
275
276
|
fi
|
|
276
277
|
|
|
278
|
+
# ============================================
|
|
279
|
+
# Update master session workPeriods.endedAt (if linked)
|
|
280
|
+
# ============================================
|
|
281
|
+
session_link_file="${session_links_dir}/${session_short_id}.json"
|
|
282
|
+
if [ -f "$session_link_file" ]; then
|
|
283
|
+
master_session_id=$(jq -r '.masterSessionId // empty' "$session_link_file" 2>/dev/null || echo "")
|
|
284
|
+
if [ -n "$master_session_id" ]; then
|
|
285
|
+
master_session_path=$(find "$sessions_dir" -name "${master_session_id}.json" -type f 2>/dev/null | head -1)
|
|
286
|
+
if [ -n "$master_session_path" ] && [ -f "$master_session_path" ]; then
|
|
287
|
+
end_now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
288
|
+
claude_session_id="${session_id}"
|
|
289
|
+
# Update the workPeriod entry with matching claudeSessionId
|
|
290
|
+
jq --arg claudeSessionId "$claude_session_id" \
|
|
291
|
+
--arg endedAt "$end_now" '
|
|
292
|
+
.workPeriods = [.workPeriods[]? |
|
|
293
|
+
if .claudeSessionId == $claudeSessionId and .endedAt == null
|
|
294
|
+
then .endedAt = $endedAt
|
|
295
|
+
else .
|
|
296
|
+
end
|
|
297
|
+
] |
|
|
298
|
+
.updatedAt = $endedAt
|
|
299
|
+
' "$master_session_path" > "${master_session_path}.tmp" \
|
|
300
|
+
&& mv "${master_session_path}.tmp" "$master_session_path"
|
|
301
|
+
echo "[memoria] Master session workPeriods.endedAt updated: ${master_session_path}" >&2
|
|
302
|
+
fi
|
|
303
|
+
fi
|
|
304
|
+
fi
|
|
305
|
+
|
|
277
306
|
exit 0
|
package/hooks/session-start.sh
CHANGED
|
@@ -42,6 +42,7 @@ memoria_dir="${cwd}/.memoria"
|
|
|
42
42
|
sessions_dir="${memoria_dir}/sessions"
|
|
43
43
|
rules_dir="${memoria_dir}/rules"
|
|
44
44
|
patterns_dir="${memoria_dir}/patterns"
|
|
45
|
+
session_links_dir="${memoria_dir}/session-links"
|
|
45
46
|
|
|
46
47
|
# Check if memoria is initialized
|
|
47
48
|
if [ ! -d "$memoria_dir" ]; then
|
|
@@ -92,6 +93,22 @@ if git -C "$cwd" rev-parse --git-dir &> /dev/null 2>&1; then
|
|
|
92
93
|
fi
|
|
93
94
|
fi
|
|
94
95
|
|
|
96
|
+
# ============================================
|
|
97
|
+
# Check session-links for master session
|
|
98
|
+
# ============================================
|
|
99
|
+
master_session_id=""
|
|
100
|
+
master_session_path=""
|
|
101
|
+
session_link_file="${session_links_dir}/${file_id}.json"
|
|
102
|
+
|
|
103
|
+
if [ -f "$session_link_file" ]; then
|
|
104
|
+
master_session_id=$(jq -r '.masterSessionId // empty' "$session_link_file" 2>/dev/null || echo "")
|
|
105
|
+
if [ -n "$master_session_id" ]; then
|
|
106
|
+
# Find master session file
|
|
107
|
+
master_session_path=$(find "$sessions_dir" -name "${master_session_id}.json" -type f 2>/dev/null | head -1)
|
|
108
|
+
echo "[memoria] Session linked to master: ${master_session_id}" >&2
|
|
109
|
+
fi
|
|
110
|
+
fi
|
|
111
|
+
|
|
95
112
|
# ============================================
|
|
96
113
|
# Find existing session file or create new one
|
|
97
114
|
# ============================================
|
|
@@ -194,6 +211,34 @@ else
|
|
|
194
211
|
echo "[memoria] Session initialized: ${session_path}" >&2
|
|
195
212
|
fi
|
|
196
213
|
|
|
214
|
+
# ============================================
|
|
215
|
+
# Update master session workPeriods (if linked)
|
|
216
|
+
# ============================================
|
|
217
|
+
if [ -n "$master_session_id" ] && [ -n "$master_session_path" ] && [ -f "$master_session_path" ]; then
|
|
218
|
+
# Use full session_id for consistency with session-end.sh
|
|
219
|
+
claude_session_id="${session_id:-$session_short_id}"
|
|
220
|
+
|
|
221
|
+
# Check if workPeriod already exists for this claudeSessionId (prevent duplicates on clear/compact)
|
|
222
|
+
existing_period=$(jq --arg cid "$claude_session_id" '.workPeriods // [] | map(select(.claudeSessionId == $cid and .endedAt == null)) | length' "$master_session_path" 2>/dev/null || echo "0")
|
|
223
|
+
|
|
224
|
+
if [ "$existing_period" = "0" ]; then
|
|
225
|
+
# Add new workPeriod entry to master session
|
|
226
|
+
jq --arg claudeSessionId "$claude_session_id" \
|
|
227
|
+
--arg startedAt "$now" '
|
|
228
|
+
.workPeriods = ((.workPeriods // []) + [{
|
|
229
|
+
claudeSessionId: $claudeSessionId,
|
|
230
|
+
startedAt: $startedAt,
|
|
231
|
+
endedAt: null
|
|
232
|
+
}]) |
|
|
233
|
+
.updatedAt = $startedAt
|
|
234
|
+
' "$master_session_path" > "${master_session_path}.tmp" \
|
|
235
|
+
&& mv "${master_session_path}.tmp" "$master_session_path"
|
|
236
|
+
echo "[memoria] Master session workPeriods updated: ${master_session_path}" >&2
|
|
237
|
+
else
|
|
238
|
+
echo "[memoria] Master session workPeriod already exists for this Claude session" >&2
|
|
239
|
+
fi
|
|
240
|
+
fi
|
|
241
|
+
|
|
197
242
|
# Get relative path for additionalContext
|
|
198
243
|
# Extract year/month from session_path
|
|
199
244
|
session_relative_path="${session_path#$cwd/}"
|
package/package.json
CHANGED
package/skills/resume/skill.md
CHANGED
|
@@ -55,8 +55,10 @@ Multiple filters can be combined:
|
|
|
55
55
|
3. Sort by `createdAt` descending (most recent first)
|
|
56
56
|
4. Display filtered session list
|
|
57
57
|
5. If session ID specified, read the JSON file and get details
|
|
58
|
-
6. **
|
|
59
|
-
7.
|
|
58
|
+
6. **Create session-link file** (new master session support)
|
|
59
|
+
7. **Update master session JSON with `workPeriods` entry**
|
|
60
|
+
8. **Update current session JSON with `resumedFrom` field** (legacy, for backwards compatibility)
|
|
61
|
+
9. Load session context to resume work
|
|
60
62
|
|
|
61
63
|
### File Operations
|
|
62
64
|
|
|
@@ -70,7 +72,16 @@ Read: .memoria/sessions/{year}/{month}/{filename}.json
|
|
|
70
72
|
# Get interactions from SQLite (private, local only)
|
|
71
73
|
sqlite3 .memoria/local.db "SELECT * FROM interactions WHERE session_id = '{id}' ORDER BY timestamp;"
|
|
72
74
|
|
|
73
|
-
#
|
|
75
|
+
# Create session-link file (NEW - master session support)
|
|
76
|
+
# This links current Claude session to the master memoria session
|
|
77
|
+
Write: .memoria/session-links/{current_session_short_id}.json
|
|
78
|
+
→ {"masterSessionId": "{resumed_session_id}", "claudeSessionId": "{current_full_session_id}", "linkedAt": "{now}"}
|
|
79
|
+
|
|
80
|
+
# Update MASTER session with workPeriods entry (NEW)
|
|
81
|
+
Edit: .memoria/sessions/{master_year}/{master_month}/{master_id}.json
|
|
82
|
+
→ Add entry to workPeriods array: {"claudeSessionId": "{current_full_session_id}", "startedAt": "{now}", "endedAt": null}
|
|
83
|
+
|
|
84
|
+
# Update CURRENT session with resumedFrom (legacy, for backwards compatibility)
|
|
74
85
|
Edit: .memoria/sessions/{current_year}/{current_month}/{current_id}.json
|
|
75
86
|
→ Add "resumedFrom": "{resumed_session_id}"
|
|
76
87
|
|
|
@@ -198,19 +209,65 @@ If you're resuming a session created by another team member, interactions won't
|
|
|
198
209
|
- SQLite contains interactions (local, private)
|
|
199
210
|
- Always update the CURRENT session's JSON with `resumedFrom` to track session chains.
|
|
200
211
|
|
|
201
|
-
## Session Chain Tracking
|
|
212
|
+
## Session Chain Tracking (Master Session Support)
|
|
213
|
+
|
|
214
|
+
When resuming session `abc123` (master) in a new Claude session `xyz789`:
|
|
215
|
+
|
|
216
|
+
### Step 1: Create session-link file
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
# Create .memoria/session-links/ directory if not exists
|
|
220
|
+
mkdir -p .memoria/session-links/
|
|
221
|
+
|
|
222
|
+
# Write session-link file
|
|
223
|
+
Write: .memoria/session-links/xyz78901.json
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
```json
|
|
227
|
+
{
|
|
228
|
+
"masterSessionId": "abc12345",
|
|
229
|
+
"claudeSessionId": "xyz78901-38e9-464d-9b7c-a9cdca203b5e",
|
|
230
|
+
"linkedAt": "2026-01-27T09:10:00Z"
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Step 2: Update master session workPeriods
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
Edit: .memoria/sessions/{year}/{month}/abc12345.json
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Add to `workPeriods` array:
|
|
241
|
+
```json
|
|
242
|
+
{
|
|
243
|
+
"workPeriods": [
|
|
244
|
+
{"claudeSessionId": "abc12345-...", "startedAt": "...", "endedAt": "..."},
|
|
245
|
+
{"claudeSessionId": "xyz78901-...", "startedAt": "2026-01-27T09:10:00Z", "endedAt": null}
|
|
246
|
+
]
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Step 3: Update current session (legacy, backwards compatibility)
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
Edit: .memoria/sessions/{year}/{month}/xyz78901.json
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
```json
|
|
257
|
+
{
|
|
258
|
+
"id": "xyz78901",
|
|
259
|
+
"resumedFrom": "abc12345",
|
|
260
|
+
...
|
|
261
|
+
}
|
|
262
|
+
```
|
|
202
263
|
|
|
203
|
-
|
|
264
|
+
### Result
|
|
204
265
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
{
|
|
209
|
-
"id": "xyz789",
|
|
210
|
-
"resumedFrom": "abc123",
|
|
211
|
-
...
|
|
212
|
-
}
|
|
213
|
-
```
|
|
214
|
-
3. This creates a chain: `xyz789 ← abc123`
|
|
266
|
+
- **session-link file**: Links Claude session → memoria master session
|
|
267
|
+
- **workPeriods**: Tracks all work periods in the master session
|
|
268
|
+
- **resumedFrom**: Legacy chain tracking (backwards compatible)
|
|
215
269
|
|
|
216
|
-
|
|
270
|
+
This design allows:
|
|
271
|
+
1. Multiple Claude sessions to contribute to one logical memoria session
|
|
272
|
+
2. `/memoria:save` to merge all data into the master session
|
|
273
|
+
3. Dashboard to show unified conversation history
|
package/skills/save/skill.md
CHANGED
|
@@ -37,16 +37,79 @@ Extract and save all meaningful data from the current session.
|
|
|
37
37
|
<phases>
|
|
38
38
|
Execute all phases in order. Each phase builds on the previous.
|
|
39
39
|
|
|
40
|
-
- Phase 0:
|
|
41
|
-
- Phase 1:
|
|
42
|
-
- Phase 2:
|
|
43
|
-
- Phase 3:
|
|
44
|
-
- Phase 4:
|
|
40
|
+
- Phase 0: Master Session - Identify master and merge child sessions
|
|
41
|
+
- Phase 1: Interactions - Merge preCompactBackups with current conversation
|
|
42
|
+
- Phase 2: Summary - Extract session metadata (considering ALL interactions)
|
|
43
|
+
- Phase 3: Decisions - Save to decisions/
|
|
44
|
+
- Phase 4: Patterns - Save to patterns/
|
|
45
|
+
- Phase 5: Rules - Extract development standards
|
|
45
46
|
</phases>
|
|
46
47
|
|
|
47
|
-
### Phase 0:
|
|
48
|
+
### Phase 0: Identify Master Session and Merge Children
|
|
48
49
|
|
|
49
|
-
|
|
50
|
+
**Purpose:** Support multiple Claude sessions contributing to one logical memoria session.
|
|
51
|
+
|
|
52
|
+
1. Get current session path from additionalContext (e.g., `.memoria/sessions/2026/01/xyz78901.json`)
|
|
53
|
+
2. Get session ID from the path (e.g., `xyz78901`)
|
|
54
|
+
|
|
55
|
+
3. **Check for session-link file:**
|
|
56
|
+
```bash
|
|
57
|
+
Read: .memoria/session-links/xyz78901.json
|
|
58
|
+
```
|
|
59
|
+
If exists, extract `masterSessionId`. If not, current session IS the master.
|
|
60
|
+
|
|
61
|
+
4. **Find master session file:**
|
|
62
|
+
```bash
|
|
63
|
+
Glob: .memoria/sessions/**/{masterSessionId}.json
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
5. **Find all child sessions linked to this master:**
|
|
67
|
+
```bash
|
|
68
|
+
# Read all session-link files
|
|
69
|
+
Glob: .memoria/session-links/*.json
|
|
70
|
+
|
|
71
|
+
# Filter by masterSessionId
|
|
72
|
+
for each link:
|
|
73
|
+
if link.masterSessionId == masterSessionId:
|
|
74
|
+
childSessionIds.push(link file's session ID)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
6. **Also check legacy `resumedFrom` chains:**
|
|
78
|
+
```bash
|
|
79
|
+
# Find sessions where resumedFrom points to master or any child
|
|
80
|
+
Glob: .memoria/sessions/**/*.json
|
|
81
|
+
for each session:
|
|
82
|
+
if session.resumedFrom == masterSessionId or session.resumedFrom in childSessionIds:
|
|
83
|
+
childSessionIds.push(session.id)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
7. **Merge child session data into master:**
|
|
87
|
+
For each child session JSON:
|
|
88
|
+
- Merge `workPeriods` (add any missing entries)
|
|
89
|
+
- Merge `files` (union, deduplicate by path)
|
|
90
|
+
- Merge `discussions` (append unique items)
|
|
91
|
+
- Merge `errors` (append unique items)
|
|
92
|
+
- Merge `metrics.toolUsage` (combine counts)
|
|
93
|
+
- Update `metrics.userMessages` (will be recalculated from SQLite)
|
|
94
|
+
|
|
95
|
+
8. **Mark child sessions as merged:**
|
|
96
|
+
```bash
|
|
97
|
+
Edit: .memoria/sessions/{year}/{month}/{childId}.json
|
|
98
|
+
```
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"status": "merged",
|
|
102
|
+
"mergedAt": "2026-01-27T12:00:00Z",
|
|
103
|
+
"masterSessionId": "abc12345"
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Important:** After this phase, all subsequent operations work on the MASTER session.
|
|
108
|
+
|
|
109
|
+
### Phase 1: Save Conversation History (interactions)
|
|
110
|
+
|
|
111
|
+
Execute this phase after identifying the master session.
|
|
112
|
+
Interactions are stored in SQLite (`local.db`) for privacy.
|
|
50
113
|
If auto-compact occurred, `pre_compact_backups` table contains earlier conversations.
|
|
51
114
|
|
|
52
115
|
**Storage Location:**
|
|
@@ -61,16 +124,16 @@ Without merge: Only 8 interactions saved (data loss)
|
|
|
61
124
|
With merge: All 24 interactions saved in SQLite
|
|
62
125
|
```
|
|
63
126
|
|
|
64
|
-
1.
|
|
65
|
-
2.
|
|
127
|
+
1. Use master session ID from Phase 0 (e.g., `abc12345`)
|
|
128
|
+
2. Collect all related session IDs: `[masterSessionId] + childSessionIds`
|
|
66
129
|
|
|
67
|
-
3. **Check for existing data in SQLite**:
|
|
130
|
+
3. **Check for existing data in SQLite (all related sessions)**:
|
|
68
131
|
```bash
|
|
69
|
-
# Check for pre_compact_backups
|
|
70
|
-
sqlite3 .memoria/local.db "SELECT interactions FROM pre_compact_backups WHERE session_id
|
|
132
|
+
# Check for pre_compact_backups from ALL related sessions
|
|
133
|
+
sqlite3 .memoria/local.db "SELECT session_id, interactions FROM pre_compact_backups WHERE session_id IN ('abc12345', 'xyz78901') ORDER BY created_at DESC;"
|
|
71
134
|
|
|
72
|
-
#
|
|
73
|
-
sqlite3 .memoria/local.db "SELECT
|
|
135
|
+
# Get interactions from ALL related sessions
|
|
136
|
+
sqlite3 .memoria/local.db "SELECT * FROM interactions WHERE session_id IN ('abc12345', 'xyz78901') ORDER BY timestamp ASC;"
|
|
74
137
|
```
|
|
75
138
|
|
|
76
139
|
4. **Determine the most complete source**:
|
|
@@ -112,10 +175,10 @@ With merge: All 24 interactions saved in SQLite
|
|
|
112
175
|
|
|
113
176
|
**Note:** Interactions are stored in SQLite for privacy. JSON contains only metadata.
|
|
114
177
|
|
|
115
|
-
### Phase
|
|
178
|
+
### Phase 2: Extract Session Data
|
|
116
179
|
|
|
117
|
-
1.
|
|
118
|
-
2. Read
|
|
180
|
+
1. Use master session from Phase 0
|
|
181
|
+
2. Read master session file (already updated with merged data from Phase 0-1)
|
|
119
182
|
3. **Scan entire conversation** (including long sessions) to extract:
|
|
120
183
|
|
|
121
184
|
#### Summary
|
|
@@ -166,7 +229,7 @@ With merge: All 24 interactions saved in SQLite
|
|
|
166
229
|
- **handoff**: stoppedReason, notes, nextSteps
|
|
167
230
|
- **references**: URLs and files referenced
|
|
168
231
|
|
|
169
|
-
### Phase
|
|
232
|
+
### Phase 3: Save to decisions/
|
|
170
233
|
|
|
171
234
|
**For each discussion with a clear decision:**
|
|
172
235
|
|
|
@@ -200,7 +263,7 @@ With merge: All 24 interactions saved in SQLite
|
|
|
200
263
|
- No clear decision was made (just discussion)
|
|
201
264
|
- Similar decision already exists (check by title/topic)
|
|
202
265
|
|
|
203
|
-
### Phase
|
|
266
|
+
### Phase 4: Save to patterns/
|
|
204
267
|
|
|
205
268
|
**For each error that was solved:**
|
|
206
269
|
|
|
@@ -242,7 +305,7 @@ With merge: All 24 interactions saved in SQLite
|
|
|
242
305
|
- No root cause was identified
|
|
243
306
|
- Error was environment-specific
|
|
244
307
|
|
|
245
|
-
### Phase
|
|
308
|
+
### Phase 5: Extract Rules
|
|
246
309
|
|
|
247
310
|
Scan conversation for development standards. These include both explicit user
|
|
248
311
|
instructions and implicit standards from technical discussions.
|
|
@@ -362,11 +425,20 @@ Report each phase result:
|
|
|
362
425
|
---
|
|
363
426
|
**Session saved.**
|
|
364
427
|
|
|
365
|
-
**Session ID:** abc12345
|
|
428
|
+
**Master Session ID:** abc12345
|
|
366
429
|
**Path:** .memoria/sessions/2026/01/abc12345.json
|
|
367
430
|
|
|
368
|
-
**Phase 0 -
|
|
369
|
-
|
|
431
|
+
**Phase 0 - Master Session:**
|
|
432
|
+
Master: abc12345
|
|
433
|
+
Children merged: xyz78901, def45678
|
|
434
|
+
Work periods: 3
|
|
435
|
+
|
|
436
|
+
**Phase 1 - Interactions:** 42 saved to SQLite
|
|
437
|
+
- From abc12345: 15 interactions
|
|
438
|
+
- From xyz78901: 18 interactions
|
|
439
|
+
- From def45678: 9 interactions
|
|
440
|
+
|
|
441
|
+
**Phase 2 - Summary:**
|
|
370
442
|
| Field | Value |
|
|
371
443
|
|-------|-------|
|
|
372
444
|
| Title | JWT authentication implementation |
|
|
@@ -374,14 +446,14 @@ Report each phase result:
|
|
|
374
446
|
| Outcome | success |
|
|
375
447
|
| Type | implementation |
|
|
376
448
|
|
|
377
|
-
**Phase
|
|
449
|
+
**Phase 3 - Decisions (2):**
|
|
378
450
|
- `[jwt-auth-001]` Authentication method selection → decisions/2026/01/
|
|
379
451
|
- `[token-expiry-001]` Token expiry strategy → decisions/2026/01/
|
|
380
452
|
|
|
381
|
-
**Phase
|
|
453
|
+
**Phase 4 - Patterns (1):**
|
|
382
454
|
- `[error-solution]` secretOrPrivateKey must be asymmetric → patterns/user.json
|
|
383
455
|
|
|
384
|
-
**Phase
|
|
456
|
+
**Phase 5 - Rules:**
|
|
385
457
|
dev-rules.json:
|
|
386
458
|
+ [code-style] Use early return pattern
|
|
387
459
|
~ [architecture] Avoid circular dependencies (skipped: similar exists)
|
|
@@ -392,7 +464,7 @@ Report each phase result:
|
|
|
392
464
|
|
|
393
465
|
If no rules are found, report what was scanned:
|
|
394
466
|
```
|
|
395
|
-
**Phase
|
|
467
|
+
**Phase 5 - Rules:**
|
|
396
468
|
Scanned for: user instructions, technical standards from Codex review, security requirements
|
|
397
469
|
Result: No new rules identified
|
|
398
470
|
```
|