@hir4ta/mneme 0.24.3 → 0.25.0
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/README.ja.md +1 -1
- package/README.md +1 -1
- package/bin/mneme.js +4 -1
- package/dist/lib/save/index.js +134 -20
- package/dist/lib/session/finalize.js +134 -20
- package/dist/lib/session/init.js +64 -0
- package/dist/server.js +1 -1
- package/hooks/pre-compact.sh +7 -0
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mneme",
|
|
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.
|
|
4
|
+
"version": "0.25.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "hir4ta"
|
|
7
7
|
},
|
package/README.ja.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# mneme
|
|
2
2
|
|
|
3
|
-

|
|
4
4
|

|
|
5
5
|
[](https://www.npmjs.com/package/@hir4ta/mneme)
|
|
6
6
|
[](https://github.com/hir4ta/mneme/blob/main/LICENSE)
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# mneme
|
|
2
2
|
|
|
3
|
-

|
|
4
4
|

|
|
5
5
|
[](https://www.npmjs.com/package/@hir4ta/mneme)
|
|
6
6
|
[](https://github.com/hir4ta/mneme/blob/main/LICENSE)
|
package/bin/mneme.js
CHANGED
|
@@ -100,11 +100,14 @@ function initMneme() {
|
|
|
100
100
|
);
|
|
101
101
|
fs.writeFileSync(path.join(rulesDir, "dev-rules.json"), rulesTemplate);
|
|
102
102
|
|
|
103
|
-
// Create .gitignore for local.db
|
|
103
|
+
// Create .gitignore for local.db and temporary files
|
|
104
104
|
const gitignoreContent = `# Local SQLite database (private interactions)
|
|
105
105
|
local.db
|
|
106
106
|
local.db-wal
|
|
107
107
|
local.db-shm
|
|
108
|
+
|
|
109
|
+
# Temporary files
|
|
110
|
+
.pending-compact.json
|
|
108
111
|
`;
|
|
109
112
|
fs.writeFileSync(gitignorePath, gitignoreContent);
|
|
110
113
|
|
package/dist/lib/save/index.js
CHANGED
|
@@ -417,6 +417,14 @@ function updateSaveState(db, claudeSessionId, lastSavedTimestamp, lastSavedLine)
|
|
|
417
417
|
`);
|
|
418
418
|
stmt.run(lastSavedTimestamp, lastSavedLine, claudeSessionId);
|
|
419
419
|
}
|
|
420
|
+
function updateSaveStateMnemeSessionId(db, claudeSessionId, mnemeSessionId) {
|
|
421
|
+
const stmt = db.prepare(`
|
|
422
|
+
UPDATE session_save_state
|
|
423
|
+
SET mneme_session_id = ?, updated_at = datetime('now')
|
|
424
|
+
WHERE claude_session_id = ?
|
|
425
|
+
`);
|
|
426
|
+
stmt.run(mnemeSessionId, claudeSessionId);
|
|
427
|
+
}
|
|
420
428
|
|
|
421
429
|
// lib/save/parser.ts
|
|
422
430
|
import * as fs4 from "node:fs";
|
|
@@ -523,7 +531,7 @@ async function parseTranscriptIncremental(transcriptPath, lastSavedLine) {
|
|
|
523
531
|
return {
|
|
524
532
|
timestamp: e.timestamp,
|
|
525
533
|
content,
|
|
526
|
-
isCompactSummary: e.isCompactSummary || false,
|
|
534
|
+
isCompactSummary: e.isCompactSummary || !!e.planContent || false,
|
|
527
535
|
slashCommand: extractSlashCommand(content)
|
|
528
536
|
};
|
|
529
537
|
});
|
|
@@ -569,6 +577,35 @@ async function parseTranscriptIncremental(transcriptPath, lastSavedLine) {
|
|
|
569
577
|
return { timestamp: e.timestamp, thinking, text, toolDetails };
|
|
570
578
|
}).filter((m) => m !== null);
|
|
571
579
|
const interactions = [];
|
|
580
|
+
const firstUserTs = userMessages.length > 0 ? userMessages[0].timestamp : "9999-12-31T23:59:59Z";
|
|
581
|
+
const orphanedResponses = assistantMessages.filter(
|
|
582
|
+
(a) => a.timestamp < firstUserTs
|
|
583
|
+
);
|
|
584
|
+
if (orphanedResponses.length > 0) {
|
|
585
|
+
const allToolDetails = orphanedResponses.flatMap((r) => r.toolDetails);
|
|
586
|
+
const orphanedTimeKeys = new Set(
|
|
587
|
+
orphanedResponses.map((r) => r.timestamp.slice(0, 16))
|
|
588
|
+
);
|
|
589
|
+
const allToolResults = [...orphanedTimeKeys].flatMap(
|
|
590
|
+
(k) => toolResultsByTimestamp.get(k) || []
|
|
591
|
+
);
|
|
592
|
+
const allProgressEvents = [...orphanedTimeKeys].flatMap(
|
|
593
|
+
(k) => progressEvents.get(k) || []
|
|
594
|
+
);
|
|
595
|
+
interactions.push({
|
|
596
|
+
timestamp: orphanedResponses[0].timestamp,
|
|
597
|
+
user: "",
|
|
598
|
+
thinking: orphanedResponses.filter((r) => r.thinking).map((r) => r.thinking).join("\n"),
|
|
599
|
+
assistant: orphanedResponses.filter((r) => r.text).map((r) => r.text).join("\n"),
|
|
600
|
+
isCompactSummary: false,
|
|
601
|
+
isContinuation: true,
|
|
602
|
+
toolsUsed: [...new Set(allToolDetails.map((t) => t.name))],
|
|
603
|
+
toolDetails: allToolDetails,
|
|
604
|
+
inPlanMode: isInPlanMode(orphanedResponses[0].timestamp) || void 0,
|
|
605
|
+
toolResults: allToolResults.length > 0 ? allToolResults : void 0,
|
|
606
|
+
progressEvents: allProgressEvents.length > 0 ? allProgressEvents : void 0
|
|
607
|
+
});
|
|
608
|
+
}
|
|
572
609
|
for (let i = 0; i < userMessages.length; i++) {
|
|
573
610
|
const user = userMessages[i];
|
|
574
611
|
const nextUserTs = i + 1 < userMessages.length ? userMessages[i + 1].timestamp : "9999-12-31T23:59:59Z";
|
|
@@ -610,6 +647,61 @@ var IGNORED_PREFIXES = [
|
|
|
610
647
|
".claude/"
|
|
611
648
|
];
|
|
612
649
|
var IGNORED_FILES = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock"];
|
|
650
|
+
function detectCompactContinuation(interactions, projectPath) {
|
|
651
|
+
const compactInteraction = interactions.find((i) => i.isCompactSummary);
|
|
652
|
+
if (!compactInteraction?.user) return null;
|
|
653
|
+
const match = compactInteraction.user.match(
|
|
654
|
+
/read the full transcript at:.*?\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl/
|
|
655
|
+
);
|
|
656
|
+
if (!match) return null;
|
|
657
|
+
const oldClaudeSessionId = match[1];
|
|
658
|
+
return resolveMnemeSessionId(projectPath, oldClaudeSessionId);
|
|
659
|
+
}
|
|
660
|
+
function linkToMasterSession(projectPath, claudeSessionId, masterMnemeSessionId) {
|
|
661
|
+
const sessionLinksDir = path4.join(projectPath, ".mneme", "session-links");
|
|
662
|
+
if (!fs5.existsSync(sessionLinksDir)) {
|
|
663
|
+
fs5.mkdirSync(sessionLinksDir, { recursive: true });
|
|
664
|
+
}
|
|
665
|
+
const linkFile = path4.join(sessionLinksDir, `${claudeSessionId}.json`);
|
|
666
|
+
if (!fs5.existsSync(linkFile)) {
|
|
667
|
+
fs5.writeFileSync(
|
|
668
|
+
linkFile,
|
|
669
|
+
JSON.stringify(
|
|
670
|
+
{
|
|
671
|
+
masterSessionId: masterMnemeSessionId,
|
|
672
|
+
claudeSessionId,
|
|
673
|
+
linkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
674
|
+
},
|
|
675
|
+
null,
|
|
676
|
+
2
|
|
677
|
+
)
|
|
678
|
+
);
|
|
679
|
+
console.error(
|
|
680
|
+
`[mneme] Compact continuation linked: ${claudeSessionId} \u2192 ${masterMnemeSessionId}`
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
const masterFile = findSessionFileById(projectPath, masterMnemeSessionId);
|
|
684
|
+
if (masterFile && fs5.existsSync(masterFile)) {
|
|
685
|
+
try {
|
|
686
|
+
const master = JSON.parse(fs5.readFileSync(masterFile, "utf8"));
|
|
687
|
+
const workPeriods = master.workPeriods || [];
|
|
688
|
+
const alreadyLinked = workPeriods.some(
|
|
689
|
+
(wp) => wp.claudeSessionId === claudeSessionId
|
|
690
|
+
);
|
|
691
|
+
if (!alreadyLinked) {
|
|
692
|
+
workPeriods.push({
|
|
693
|
+
claudeSessionId,
|
|
694
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
695
|
+
endedAt: null
|
|
696
|
+
});
|
|
697
|
+
master.workPeriods = workPeriods;
|
|
698
|
+
master.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
699
|
+
fs5.writeFileSync(masterFile, JSON.stringify(master, null, 2));
|
|
700
|
+
}
|
|
701
|
+
} catch {
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
613
705
|
function isIgnoredPath(relativePath) {
|
|
614
706
|
return IGNORED_PREFIXES.some((p) => relativePath.startsWith(p)) || IGNORED_FILES.includes(relativePath);
|
|
615
707
|
}
|
|
@@ -663,7 +755,7 @@ async function incrementalSave(claudeSessionId, transcriptPath, projectPath) {
|
|
|
663
755
|
}
|
|
664
756
|
const dbPath = path4.join(projectPath, ".mneme", "local.db");
|
|
665
757
|
const db = initDatabase(dbPath);
|
|
666
|
-
|
|
758
|
+
let mnemeSessionId = resolveMnemeSessionId(projectPath, claudeSessionId);
|
|
667
759
|
const saveState = getSaveState(
|
|
668
760
|
db,
|
|
669
761
|
claudeSessionId,
|
|
@@ -674,7 +766,25 @@ async function incrementalSave(claudeSessionId, transcriptPath, projectPath) {
|
|
|
674
766
|
transcriptPath,
|
|
675
767
|
saveState.lastSavedLine
|
|
676
768
|
);
|
|
769
|
+
if (saveState.lastSavedLine === 0 && interactions.length > 0) {
|
|
770
|
+
const masterSessionId = detectCompactContinuation(
|
|
771
|
+
interactions,
|
|
772
|
+
projectPath
|
|
773
|
+
);
|
|
774
|
+
if (masterSessionId && masterSessionId !== mnemeSessionId) {
|
|
775
|
+
linkToMasterSession(projectPath, claudeSessionId, masterSessionId);
|
|
776
|
+
mnemeSessionId = masterSessionId;
|
|
777
|
+
updateSaveStateMnemeSessionId(db, claudeSessionId, masterSessionId);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
677
780
|
if (interactions.length === 0) {
|
|
781
|
+
updateSaveState(
|
|
782
|
+
db,
|
|
783
|
+
claudeSessionId,
|
|
784
|
+
saveState.lastSavedTimestamp || "",
|
|
785
|
+
totalLines
|
|
786
|
+
);
|
|
787
|
+
db.close();
|
|
678
788
|
return {
|
|
679
789
|
success: true,
|
|
680
790
|
savedCount: 0,
|
|
@@ -701,6 +811,7 @@ async function incrementalSave(claudeSessionId, transcriptPath, projectPath) {
|
|
|
701
811
|
toolsUsed: interaction.toolsUsed,
|
|
702
812
|
toolDetails: interaction.toolDetails,
|
|
703
813
|
...interaction.inPlanMode && { inPlanMode: true },
|
|
814
|
+
...interaction.isContinuation && { isContinuation: true },
|
|
704
815
|
...interaction.slashCommand && {
|
|
705
816
|
slashCommand: interaction.slashCommand
|
|
706
817
|
},
|
|
@@ -711,27 +822,30 @@ async function incrementalSave(claudeSessionId, transcriptPath, projectPath) {
|
|
|
711
822
|
progressEvents: interaction.progressEvents
|
|
712
823
|
}
|
|
713
824
|
});
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
825
|
+
if (!interaction.isContinuation) {
|
|
826
|
+
insertStmt.run(
|
|
827
|
+
mnemeSessionId,
|
|
828
|
+
claudeSessionId,
|
|
829
|
+
projectPath,
|
|
830
|
+
repository,
|
|
831
|
+
repositoryUrl,
|
|
832
|
+
repositoryRoot,
|
|
833
|
+
owner,
|
|
834
|
+
"user",
|
|
835
|
+
interaction.user,
|
|
836
|
+
null,
|
|
837
|
+
metadata,
|
|
838
|
+
interaction.timestamp,
|
|
839
|
+
interaction.isCompactSummary ? 1 : 0
|
|
840
|
+
);
|
|
841
|
+
insertedCount++;
|
|
842
|
+
}
|
|
843
|
+
if (interaction.assistant || interaction.thinking) {
|
|
731
844
|
const assistantMetadata = JSON.stringify({
|
|
732
845
|
toolsUsed: interaction.toolsUsed,
|
|
733
846
|
toolDetails: interaction.toolDetails,
|
|
734
847
|
...interaction.inPlanMode && { inPlanMode: true },
|
|
848
|
+
...interaction.isContinuation && { isContinuation: true },
|
|
735
849
|
...interaction.toolResults?.length && {
|
|
736
850
|
toolResults: interaction.toolResults
|
|
737
851
|
},
|
|
@@ -748,7 +862,7 @@ async function incrementalSave(claudeSessionId, transcriptPath, projectPath) {
|
|
|
748
862
|
repositoryRoot,
|
|
749
863
|
owner,
|
|
750
864
|
"assistant",
|
|
751
|
-
interaction.assistant,
|
|
865
|
+
interaction.assistant || "",
|
|
752
866
|
interaction.thinking || null,
|
|
753
867
|
assistantMetadata,
|
|
754
868
|
interaction.timestamp,
|
|
@@ -421,6 +421,14 @@ function updateSaveState(db, claudeSessionId, lastSavedTimestamp, lastSavedLine)
|
|
|
421
421
|
`);
|
|
422
422
|
stmt.run(lastSavedTimestamp, lastSavedLine, claudeSessionId);
|
|
423
423
|
}
|
|
424
|
+
function updateSaveStateMnemeSessionId(db, claudeSessionId, mnemeSessionId) {
|
|
425
|
+
const stmt = db.prepare(`
|
|
426
|
+
UPDATE session_save_state
|
|
427
|
+
SET mneme_session_id = ?, updated_at = datetime('now')
|
|
428
|
+
WHERE claude_session_id = ?
|
|
429
|
+
`);
|
|
430
|
+
stmt.run(mnemeSessionId, claudeSessionId);
|
|
431
|
+
}
|
|
424
432
|
|
|
425
433
|
// lib/save/parser.ts
|
|
426
434
|
import * as fs4 from "node:fs";
|
|
@@ -527,7 +535,7 @@ async function parseTranscriptIncremental(transcriptPath, lastSavedLine) {
|
|
|
527
535
|
return {
|
|
528
536
|
timestamp: e.timestamp,
|
|
529
537
|
content,
|
|
530
|
-
isCompactSummary: e.isCompactSummary || false,
|
|
538
|
+
isCompactSummary: e.isCompactSummary || !!e.planContent || false,
|
|
531
539
|
slashCommand: extractSlashCommand(content)
|
|
532
540
|
};
|
|
533
541
|
});
|
|
@@ -573,6 +581,35 @@ async function parseTranscriptIncremental(transcriptPath, lastSavedLine) {
|
|
|
573
581
|
return { timestamp: e.timestamp, thinking, text, toolDetails };
|
|
574
582
|
}).filter((m) => m !== null);
|
|
575
583
|
const interactions = [];
|
|
584
|
+
const firstUserTs = userMessages.length > 0 ? userMessages[0].timestamp : "9999-12-31T23:59:59Z";
|
|
585
|
+
const orphanedResponses = assistantMessages.filter(
|
|
586
|
+
(a) => a.timestamp < firstUserTs
|
|
587
|
+
);
|
|
588
|
+
if (orphanedResponses.length > 0) {
|
|
589
|
+
const allToolDetails = orphanedResponses.flatMap((r) => r.toolDetails);
|
|
590
|
+
const orphanedTimeKeys = new Set(
|
|
591
|
+
orphanedResponses.map((r) => r.timestamp.slice(0, 16))
|
|
592
|
+
);
|
|
593
|
+
const allToolResults = [...orphanedTimeKeys].flatMap(
|
|
594
|
+
(k) => toolResultsByTimestamp.get(k) || []
|
|
595
|
+
);
|
|
596
|
+
const allProgressEvents = [...orphanedTimeKeys].flatMap(
|
|
597
|
+
(k) => progressEvents.get(k) || []
|
|
598
|
+
);
|
|
599
|
+
interactions.push({
|
|
600
|
+
timestamp: orphanedResponses[0].timestamp,
|
|
601
|
+
user: "",
|
|
602
|
+
thinking: orphanedResponses.filter((r) => r.thinking).map((r) => r.thinking).join("\n"),
|
|
603
|
+
assistant: orphanedResponses.filter((r) => r.text).map((r) => r.text).join("\n"),
|
|
604
|
+
isCompactSummary: false,
|
|
605
|
+
isContinuation: true,
|
|
606
|
+
toolsUsed: [...new Set(allToolDetails.map((t) => t.name))],
|
|
607
|
+
toolDetails: allToolDetails,
|
|
608
|
+
inPlanMode: isInPlanMode(orphanedResponses[0].timestamp) || void 0,
|
|
609
|
+
toolResults: allToolResults.length > 0 ? allToolResults : void 0,
|
|
610
|
+
progressEvents: allProgressEvents.length > 0 ? allProgressEvents : void 0
|
|
611
|
+
});
|
|
612
|
+
}
|
|
576
613
|
for (let i = 0; i < userMessages.length; i++) {
|
|
577
614
|
const user = userMessages[i];
|
|
578
615
|
const nextUserTs = i + 1 < userMessages.length ? userMessages[i + 1].timestamp : "9999-12-31T23:59:59Z";
|
|
@@ -614,6 +651,61 @@ var IGNORED_PREFIXES = [
|
|
|
614
651
|
".claude/"
|
|
615
652
|
];
|
|
616
653
|
var IGNORED_FILES = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock"];
|
|
654
|
+
function detectCompactContinuation(interactions, projectPath) {
|
|
655
|
+
const compactInteraction = interactions.find((i) => i.isCompactSummary);
|
|
656
|
+
if (!compactInteraction?.user) return null;
|
|
657
|
+
const match = compactInteraction.user.match(
|
|
658
|
+
/read the full transcript at:.*?\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl/
|
|
659
|
+
);
|
|
660
|
+
if (!match) return null;
|
|
661
|
+
const oldClaudeSessionId = match[1];
|
|
662
|
+
return resolveMnemeSessionId(projectPath, oldClaudeSessionId);
|
|
663
|
+
}
|
|
664
|
+
function linkToMasterSession(projectPath, claudeSessionId, masterMnemeSessionId) {
|
|
665
|
+
const sessionLinksDir = path4.join(projectPath, ".mneme", "session-links");
|
|
666
|
+
if (!fs5.existsSync(sessionLinksDir)) {
|
|
667
|
+
fs5.mkdirSync(sessionLinksDir, { recursive: true });
|
|
668
|
+
}
|
|
669
|
+
const linkFile = path4.join(sessionLinksDir, `${claudeSessionId}.json`);
|
|
670
|
+
if (!fs5.existsSync(linkFile)) {
|
|
671
|
+
fs5.writeFileSync(
|
|
672
|
+
linkFile,
|
|
673
|
+
JSON.stringify(
|
|
674
|
+
{
|
|
675
|
+
masterSessionId: masterMnemeSessionId,
|
|
676
|
+
claudeSessionId,
|
|
677
|
+
linkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
678
|
+
},
|
|
679
|
+
null,
|
|
680
|
+
2
|
|
681
|
+
)
|
|
682
|
+
);
|
|
683
|
+
console.error(
|
|
684
|
+
`[mneme] Compact continuation linked: ${claudeSessionId} \u2192 ${masterMnemeSessionId}`
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
const masterFile = findSessionFileById(projectPath, masterMnemeSessionId);
|
|
688
|
+
if (masterFile && fs5.existsSync(masterFile)) {
|
|
689
|
+
try {
|
|
690
|
+
const master = JSON.parse(fs5.readFileSync(masterFile, "utf8"));
|
|
691
|
+
const workPeriods = master.workPeriods || [];
|
|
692
|
+
const alreadyLinked = workPeriods.some(
|
|
693
|
+
(wp) => wp.claudeSessionId === claudeSessionId
|
|
694
|
+
);
|
|
695
|
+
if (!alreadyLinked) {
|
|
696
|
+
workPeriods.push({
|
|
697
|
+
claudeSessionId,
|
|
698
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
699
|
+
endedAt: null
|
|
700
|
+
});
|
|
701
|
+
master.workPeriods = workPeriods;
|
|
702
|
+
master.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
703
|
+
fs5.writeFileSync(masterFile, JSON.stringify(master, null, 2));
|
|
704
|
+
}
|
|
705
|
+
} catch {
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
617
709
|
function isIgnoredPath(relativePath) {
|
|
618
710
|
return IGNORED_PREFIXES.some((p) => relativePath.startsWith(p)) || IGNORED_FILES.includes(relativePath);
|
|
619
711
|
}
|
|
@@ -667,7 +759,7 @@ async function incrementalSave(claudeSessionId, transcriptPath, projectPath) {
|
|
|
667
759
|
}
|
|
668
760
|
const dbPath = path4.join(projectPath, ".mneme", "local.db");
|
|
669
761
|
const db = initDatabase(dbPath);
|
|
670
|
-
|
|
762
|
+
let mnemeSessionId = resolveMnemeSessionId(projectPath, claudeSessionId);
|
|
671
763
|
const saveState = getSaveState(
|
|
672
764
|
db,
|
|
673
765
|
claudeSessionId,
|
|
@@ -678,7 +770,25 @@ async function incrementalSave(claudeSessionId, transcriptPath, projectPath) {
|
|
|
678
770
|
transcriptPath,
|
|
679
771
|
saveState.lastSavedLine
|
|
680
772
|
);
|
|
773
|
+
if (saveState.lastSavedLine === 0 && interactions.length > 0) {
|
|
774
|
+
const masterSessionId = detectCompactContinuation(
|
|
775
|
+
interactions,
|
|
776
|
+
projectPath
|
|
777
|
+
);
|
|
778
|
+
if (masterSessionId && masterSessionId !== mnemeSessionId) {
|
|
779
|
+
linkToMasterSession(projectPath, claudeSessionId, masterSessionId);
|
|
780
|
+
mnemeSessionId = masterSessionId;
|
|
781
|
+
updateSaveStateMnemeSessionId(db, claudeSessionId, masterSessionId);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
681
784
|
if (interactions.length === 0) {
|
|
785
|
+
updateSaveState(
|
|
786
|
+
db,
|
|
787
|
+
claudeSessionId,
|
|
788
|
+
saveState.lastSavedTimestamp || "",
|
|
789
|
+
totalLines
|
|
790
|
+
);
|
|
791
|
+
db.close();
|
|
682
792
|
return {
|
|
683
793
|
success: true,
|
|
684
794
|
savedCount: 0,
|
|
@@ -705,6 +815,7 @@ async function incrementalSave(claudeSessionId, transcriptPath, projectPath) {
|
|
|
705
815
|
toolsUsed: interaction.toolsUsed,
|
|
706
816
|
toolDetails: interaction.toolDetails,
|
|
707
817
|
...interaction.inPlanMode && { inPlanMode: true },
|
|
818
|
+
...interaction.isContinuation && { isContinuation: true },
|
|
708
819
|
...interaction.slashCommand && {
|
|
709
820
|
slashCommand: interaction.slashCommand
|
|
710
821
|
},
|
|
@@ -715,27 +826,30 @@ async function incrementalSave(claudeSessionId, transcriptPath, projectPath) {
|
|
|
715
826
|
progressEvents: interaction.progressEvents
|
|
716
827
|
}
|
|
717
828
|
});
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
829
|
+
if (!interaction.isContinuation) {
|
|
830
|
+
insertStmt.run(
|
|
831
|
+
mnemeSessionId,
|
|
832
|
+
claudeSessionId,
|
|
833
|
+
projectPath,
|
|
834
|
+
repository,
|
|
835
|
+
repositoryUrl,
|
|
836
|
+
repositoryRoot,
|
|
837
|
+
owner,
|
|
838
|
+
"user",
|
|
839
|
+
interaction.user,
|
|
840
|
+
null,
|
|
841
|
+
metadata,
|
|
842
|
+
interaction.timestamp,
|
|
843
|
+
interaction.isCompactSummary ? 1 : 0
|
|
844
|
+
);
|
|
845
|
+
insertedCount++;
|
|
846
|
+
}
|
|
847
|
+
if (interaction.assistant || interaction.thinking) {
|
|
735
848
|
const assistantMetadata = JSON.stringify({
|
|
736
849
|
toolsUsed: interaction.toolsUsed,
|
|
737
850
|
toolDetails: interaction.toolDetails,
|
|
738
851
|
...interaction.inPlanMode && { inPlanMode: true },
|
|
852
|
+
...interaction.isContinuation && { isContinuation: true },
|
|
739
853
|
...interaction.toolResults?.length && {
|
|
740
854
|
toolResults: interaction.toolResults
|
|
741
855
|
},
|
|
@@ -752,7 +866,7 @@ async function incrementalSave(claudeSessionId, transcriptPath, projectPath) {
|
|
|
752
866
|
repositoryRoot,
|
|
753
867
|
owner,
|
|
754
868
|
"assistant",
|
|
755
|
-
interaction.assistant,
|
|
869
|
+
interaction.assistant || "",
|
|
756
870
|
interaction.thinking || null,
|
|
757
871
|
assistantMetadata,
|
|
758
872
|
interaction.timestamp,
|
package/dist/lib/session/init.js
CHANGED
|
@@ -209,6 +209,69 @@ function initTags(mnemeDir, pluginRoot) {
|
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
// lib/session/init.ts
|
|
212
|
+
function resolveSessionLink(sessionLinksDir, claudeSessionId) {
|
|
213
|
+
const fullPath = path3.join(sessionLinksDir, `${claudeSessionId}.json`);
|
|
214
|
+
const shortPath = claudeSessionId.length > 8 ? path3.join(sessionLinksDir, `${claudeSessionId.slice(0, 8)}.json`) : fullPath;
|
|
215
|
+
const linkPath = fs3.existsSync(fullPath) ? fullPath : shortPath;
|
|
216
|
+
if (fs3.existsSync(linkPath)) {
|
|
217
|
+
try {
|
|
218
|
+
const link = JSON.parse(fs3.readFileSync(linkPath, "utf8"));
|
|
219
|
+
if (link.masterSessionId) {
|
|
220
|
+
return link.masterSessionId;
|
|
221
|
+
}
|
|
222
|
+
} catch {
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return claudeSessionId;
|
|
226
|
+
}
|
|
227
|
+
function handlePendingCompact(mnemeDir, sessionLinksDir, currentClaudeSessionId) {
|
|
228
|
+
const pendingFile = path3.join(mnemeDir, ".pending-compact.json");
|
|
229
|
+
if (!fs3.existsSync(pendingFile)) return;
|
|
230
|
+
try {
|
|
231
|
+
const pending = JSON.parse(fs3.readFileSync(pendingFile, "utf8"));
|
|
232
|
+
const oldClaudeSessionId = pending.claudeSessionId || "";
|
|
233
|
+
const timestamp = pending.timestamp || "";
|
|
234
|
+
if (timestamp) {
|
|
235
|
+
const age = Date.now() - new Date(timestamp).getTime();
|
|
236
|
+
if (age > 5 * 60 * 1e3) {
|
|
237
|
+
fs3.unlinkSync(pendingFile);
|
|
238
|
+
console.error("[mneme] Stale pending-compact removed");
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (!oldClaudeSessionId || oldClaudeSessionId === currentClaudeSessionId) {
|
|
243
|
+
fs3.unlinkSync(pendingFile);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const masterSessionId = resolveSessionLink(
|
|
247
|
+
sessionLinksDir,
|
|
248
|
+
oldClaudeSessionId
|
|
249
|
+
);
|
|
250
|
+
ensureDir(sessionLinksDir);
|
|
251
|
+
const linkFile = path3.join(
|
|
252
|
+
sessionLinksDir,
|
|
253
|
+
`${currentClaudeSessionId}.json`
|
|
254
|
+
);
|
|
255
|
+
if (!fs3.existsSync(linkFile)) {
|
|
256
|
+
const linkData = {
|
|
257
|
+
masterSessionId,
|
|
258
|
+
claudeSessionId: currentClaudeSessionId,
|
|
259
|
+
linkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
260
|
+
};
|
|
261
|
+
fs3.writeFileSync(linkFile, JSON.stringify(linkData, null, 2));
|
|
262
|
+
console.error(
|
|
263
|
+
`[mneme] Compact continuation linked: ${currentClaudeSessionId} \u2192 ${masterSessionId}`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
fs3.unlinkSync(pendingFile);
|
|
267
|
+
} catch (e) {
|
|
268
|
+
console.error(`[mneme] Error handling pending-compact: ${e}`);
|
|
269
|
+
try {
|
|
270
|
+
fs3.unlinkSync(path3.join(mnemeDir, ".pending-compact.json"));
|
|
271
|
+
} catch {
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
212
275
|
function sessionInit(sessionId, cwd) {
|
|
213
276
|
const pluginRoot = path3.resolve(__dirname, "..", "..");
|
|
214
277
|
const mnemeDir = path3.join(cwd, ".mneme");
|
|
@@ -223,6 +286,7 @@ function sessionInit(sessionId, cwd) {
|
|
|
223
286
|
}
|
|
224
287
|
const now = nowISO();
|
|
225
288
|
const fileId = sessionId || "";
|
|
289
|
+
handlePendingCompact(mnemeDir, sessionLinksDir, fileId);
|
|
226
290
|
const git = getGitInfo(cwd);
|
|
227
291
|
const repoInfo = getRepositoryInfo(cwd);
|
|
228
292
|
const projectName = path3.basename(cwd);
|
package/dist/server.js
CHANGED
package/hooks/pre-compact.sh
CHANGED
|
@@ -70,4 +70,11 @@ else
|
|
|
70
70
|
echo "[mneme:pre-compact] Save result: ${result}" >&2
|
|
71
71
|
fi
|
|
72
72
|
|
|
73
|
+
# Write pending-compact breadcrumb for session linking on next SessionStart
|
|
74
|
+
# The new session (post-compact) will read this to link back to the current mneme session
|
|
75
|
+
pending_compact_file="${cwd}/.mneme/.pending-compact.json"
|
|
76
|
+
printf '{"claudeSessionId":"%s","timestamp":"%s"}\n' \
|
|
77
|
+
"$session_id" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$pending_compact_file"
|
|
78
|
+
echo "[mneme:pre-compact] Wrote pending-compact breadcrumb for session linking" >&2
|
|
79
|
+
|
|
73
80
|
exit 0
|
package/package.json
CHANGED