@hir4ta/mneme 0.24.3 → 0.25.1
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 +144 -19
- package/dist/lib/session/finalize.js +144 -19
- 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.1",
|
|
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";
|
|
@@ -510,9 +518,16 @@ async function parseTranscriptIncremental(transcriptPath, lastSavedLine) {
|
|
|
510
518
|
progressEvents.get(key)?.push(event);
|
|
511
519
|
}
|
|
512
520
|
}
|
|
521
|
+
const planContentEntries = entries.filter(
|
|
522
|
+
(e) => e.type === "user" && e.message?.role === "user" && !!e.planContent && typeof e.message?.content === "string"
|
|
523
|
+
).map((e) => ({
|
|
524
|
+
timestamp: e.timestamp,
|
|
525
|
+
content: e.message?.content
|
|
526
|
+
}));
|
|
513
527
|
const userMessages = entries.filter((e) => {
|
|
514
528
|
if (e.type !== "user" || e.message?.role !== "user") return false;
|
|
515
529
|
if (e.isMeta === true) return false;
|
|
530
|
+
if (e.planContent) return false;
|
|
516
531
|
const content = e.message?.content;
|
|
517
532
|
if (typeof content !== "string") return false;
|
|
518
533
|
if (content.startsWith("<local-command-stdout>")) return false;
|
|
@@ -569,6 +584,39 @@ async function parseTranscriptIncremental(transcriptPath, lastSavedLine) {
|
|
|
569
584
|
return { timestamp: e.timestamp, thinking, text, toolDetails };
|
|
570
585
|
}).filter((m) => m !== null);
|
|
571
586
|
const interactions = [];
|
|
587
|
+
const firstUserTs = userMessages.length > 0 ? userMessages[0].timestamp : "9999-12-31T23:59:59Z";
|
|
588
|
+
const orphanedResponses = assistantMessages.filter(
|
|
589
|
+
(a) => a.timestamp < firstUserTs
|
|
590
|
+
);
|
|
591
|
+
const planEntry = planContentEntries.find((p) => p.timestamp <= firstUserTs);
|
|
592
|
+
if (orphanedResponses.length > 0 || planEntry) {
|
|
593
|
+
const allToolDetails = orphanedResponses.flatMap((r) => r.toolDetails);
|
|
594
|
+
const orphanedTimeKeys = new Set(
|
|
595
|
+
orphanedResponses.map((r) => r.timestamp.slice(0, 16))
|
|
596
|
+
);
|
|
597
|
+
const allToolResults = [...orphanedTimeKeys].flatMap(
|
|
598
|
+
(k) => toolResultsByTimestamp.get(k) || []
|
|
599
|
+
);
|
|
600
|
+
const allProgressEvents = [...orphanedTimeKeys].flatMap(
|
|
601
|
+
(k) => progressEvents.get(k) || []
|
|
602
|
+
);
|
|
603
|
+
interactions.push({
|
|
604
|
+
timestamp: orphanedResponses.length > 0 ? orphanedResponses[0].timestamp : planEntry?.timestamp ?? "",
|
|
605
|
+
// Include plan content for compact detection (UUID extraction)
|
|
606
|
+
user: planEntry?.content || "",
|
|
607
|
+
thinking: orphanedResponses.filter((r) => r.thinking).map((r) => r.thinking).join("\n"),
|
|
608
|
+
assistant: orphanedResponses.filter((r) => r.text).map((r) => r.text).join("\n"),
|
|
609
|
+
isCompactSummary: !!planEntry,
|
|
610
|
+
isContinuation: true,
|
|
611
|
+
toolsUsed: [...new Set(allToolDetails.map((t) => t.name))],
|
|
612
|
+
toolDetails: allToolDetails,
|
|
613
|
+
inPlanMode: isInPlanMode(
|
|
614
|
+
orphanedResponses[0]?.timestamp ?? planEntry?.timestamp ?? ""
|
|
615
|
+
) || void 0,
|
|
616
|
+
toolResults: allToolResults.length > 0 ? allToolResults : void 0,
|
|
617
|
+
progressEvents: allProgressEvents.length > 0 ? allProgressEvents : void 0
|
|
618
|
+
});
|
|
619
|
+
}
|
|
572
620
|
for (let i = 0; i < userMessages.length; i++) {
|
|
573
621
|
const user = userMessages[i];
|
|
574
622
|
const nextUserTs = i + 1 < userMessages.length ? userMessages[i + 1].timestamp : "9999-12-31T23:59:59Z";
|
|
@@ -610,6 +658,61 @@ var IGNORED_PREFIXES = [
|
|
|
610
658
|
".claude/"
|
|
611
659
|
];
|
|
612
660
|
var IGNORED_FILES = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock"];
|
|
661
|
+
function detectCompactContinuation(interactions, projectPath) {
|
|
662
|
+
const compactInteraction = interactions.find((i) => i.isCompactSummary);
|
|
663
|
+
if (!compactInteraction?.user) return null;
|
|
664
|
+
const match = compactInteraction.user.match(
|
|
665
|
+
/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/
|
|
666
|
+
);
|
|
667
|
+
if (!match) return null;
|
|
668
|
+
const oldClaudeSessionId = match[1];
|
|
669
|
+
return resolveMnemeSessionId(projectPath, oldClaudeSessionId);
|
|
670
|
+
}
|
|
671
|
+
function linkToMasterSession(projectPath, claudeSessionId, masterMnemeSessionId) {
|
|
672
|
+
const sessionLinksDir = path4.join(projectPath, ".mneme", "session-links");
|
|
673
|
+
if (!fs5.existsSync(sessionLinksDir)) {
|
|
674
|
+
fs5.mkdirSync(sessionLinksDir, { recursive: true });
|
|
675
|
+
}
|
|
676
|
+
const linkFile = path4.join(sessionLinksDir, `${claudeSessionId}.json`);
|
|
677
|
+
if (!fs5.existsSync(linkFile)) {
|
|
678
|
+
fs5.writeFileSync(
|
|
679
|
+
linkFile,
|
|
680
|
+
JSON.stringify(
|
|
681
|
+
{
|
|
682
|
+
masterSessionId: masterMnemeSessionId,
|
|
683
|
+
claudeSessionId,
|
|
684
|
+
linkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
685
|
+
},
|
|
686
|
+
null,
|
|
687
|
+
2
|
|
688
|
+
)
|
|
689
|
+
);
|
|
690
|
+
console.error(
|
|
691
|
+
`[mneme] Compact continuation linked: ${claudeSessionId} \u2192 ${masterMnemeSessionId}`
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
const masterFile = findSessionFileById(projectPath, masterMnemeSessionId);
|
|
695
|
+
if (masterFile && fs5.existsSync(masterFile)) {
|
|
696
|
+
try {
|
|
697
|
+
const master = JSON.parse(fs5.readFileSync(masterFile, "utf8"));
|
|
698
|
+
const workPeriods = master.workPeriods || [];
|
|
699
|
+
const alreadyLinked = workPeriods.some(
|
|
700
|
+
(wp) => wp.claudeSessionId === claudeSessionId
|
|
701
|
+
);
|
|
702
|
+
if (!alreadyLinked) {
|
|
703
|
+
workPeriods.push({
|
|
704
|
+
claudeSessionId,
|
|
705
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
706
|
+
endedAt: null
|
|
707
|
+
});
|
|
708
|
+
master.workPeriods = workPeriods;
|
|
709
|
+
master.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
710
|
+
fs5.writeFileSync(masterFile, JSON.stringify(master, null, 2));
|
|
711
|
+
}
|
|
712
|
+
} catch {
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
613
716
|
function isIgnoredPath(relativePath) {
|
|
614
717
|
return IGNORED_PREFIXES.some((p) => relativePath.startsWith(p)) || IGNORED_FILES.includes(relativePath);
|
|
615
718
|
}
|
|
@@ -663,7 +766,7 @@ async function incrementalSave(claudeSessionId, transcriptPath, projectPath) {
|
|
|
663
766
|
}
|
|
664
767
|
const dbPath = path4.join(projectPath, ".mneme", "local.db");
|
|
665
768
|
const db = initDatabase(dbPath);
|
|
666
|
-
|
|
769
|
+
let mnemeSessionId = resolveMnemeSessionId(projectPath, claudeSessionId);
|
|
667
770
|
const saveState = getSaveState(
|
|
668
771
|
db,
|
|
669
772
|
claudeSessionId,
|
|
@@ -674,7 +777,25 @@ async function incrementalSave(claudeSessionId, transcriptPath, projectPath) {
|
|
|
674
777
|
transcriptPath,
|
|
675
778
|
saveState.lastSavedLine
|
|
676
779
|
);
|
|
780
|
+
if (saveState.lastSavedLine === 0 && interactions.length > 0) {
|
|
781
|
+
const masterSessionId = detectCompactContinuation(
|
|
782
|
+
interactions,
|
|
783
|
+
projectPath
|
|
784
|
+
);
|
|
785
|
+
if (masterSessionId && masterSessionId !== mnemeSessionId) {
|
|
786
|
+
linkToMasterSession(projectPath, claudeSessionId, masterSessionId);
|
|
787
|
+
mnemeSessionId = masterSessionId;
|
|
788
|
+
updateSaveStateMnemeSessionId(db, claudeSessionId, masterSessionId);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
677
791
|
if (interactions.length === 0) {
|
|
792
|
+
updateSaveState(
|
|
793
|
+
db,
|
|
794
|
+
claudeSessionId,
|
|
795
|
+
saveState.lastSavedTimestamp || "",
|
|
796
|
+
totalLines
|
|
797
|
+
);
|
|
798
|
+
db.close();
|
|
678
799
|
return {
|
|
679
800
|
success: true,
|
|
680
801
|
savedCount: 0,
|
|
@@ -701,6 +822,7 @@ async function incrementalSave(claudeSessionId, transcriptPath, projectPath) {
|
|
|
701
822
|
toolsUsed: interaction.toolsUsed,
|
|
702
823
|
toolDetails: interaction.toolDetails,
|
|
703
824
|
...interaction.inPlanMode && { inPlanMode: true },
|
|
825
|
+
...interaction.isContinuation && { isContinuation: true },
|
|
704
826
|
...interaction.slashCommand && {
|
|
705
827
|
slashCommand: interaction.slashCommand
|
|
706
828
|
},
|
|
@@ -711,27 +833,30 @@ async function incrementalSave(claudeSessionId, transcriptPath, projectPath) {
|
|
|
711
833
|
progressEvents: interaction.progressEvents
|
|
712
834
|
}
|
|
713
835
|
});
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
836
|
+
if (!interaction.isContinuation) {
|
|
837
|
+
insertStmt.run(
|
|
838
|
+
mnemeSessionId,
|
|
839
|
+
claudeSessionId,
|
|
840
|
+
projectPath,
|
|
841
|
+
repository,
|
|
842
|
+
repositoryUrl,
|
|
843
|
+
repositoryRoot,
|
|
844
|
+
owner,
|
|
845
|
+
"user",
|
|
846
|
+
interaction.user,
|
|
847
|
+
null,
|
|
848
|
+
metadata,
|
|
849
|
+
interaction.timestamp,
|
|
850
|
+
interaction.isCompactSummary ? 1 : 0
|
|
851
|
+
);
|
|
852
|
+
insertedCount++;
|
|
853
|
+
}
|
|
854
|
+
if (interaction.assistant || interaction.thinking) {
|
|
731
855
|
const assistantMetadata = JSON.stringify({
|
|
732
856
|
toolsUsed: interaction.toolsUsed,
|
|
733
857
|
toolDetails: interaction.toolDetails,
|
|
734
858
|
...interaction.inPlanMode && { inPlanMode: true },
|
|
859
|
+
...interaction.isContinuation && { isContinuation: true },
|
|
735
860
|
...interaction.toolResults?.length && {
|
|
736
861
|
toolResults: interaction.toolResults
|
|
737
862
|
},
|
|
@@ -748,7 +873,7 @@ async function incrementalSave(claudeSessionId, transcriptPath, projectPath) {
|
|
|
748
873
|
repositoryRoot,
|
|
749
874
|
owner,
|
|
750
875
|
"assistant",
|
|
751
|
-
interaction.assistant,
|
|
876
|
+
interaction.assistant || "",
|
|
752
877
|
interaction.thinking || null,
|
|
753
878
|
assistantMetadata,
|
|
754
879
|
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";
|
|
@@ -514,9 +522,16 @@ async function parseTranscriptIncremental(transcriptPath, lastSavedLine) {
|
|
|
514
522
|
progressEvents.get(key)?.push(event);
|
|
515
523
|
}
|
|
516
524
|
}
|
|
525
|
+
const planContentEntries = entries.filter(
|
|
526
|
+
(e) => e.type === "user" && e.message?.role === "user" && !!e.planContent && typeof e.message?.content === "string"
|
|
527
|
+
).map((e) => ({
|
|
528
|
+
timestamp: e.timestamp,
|
|
529
|
+
content: e.message?.content
|
|
530
|
+
}));
|
|
517
531
|
const userMessages = entries.filter((e) => {
|
|
518
532
|
if (e.type !== "user" || e.message?.role !== "user") return false;
|
|
519
533
|
if (e.isMeta === true) return false;
|
|
534
|
+
if (e.planContent) return false;
|
|
520
535
|
const content = e.message?.content;
|
|
521
536
|
if (typeof content !== "string") return false;
|
|
522
537
|
if (content.startsWith("<local-command-stdout>")) return false;
|
|
@@ -573,6 +588,39 @@ async function parseTranscriptIncremental(transcriptPath, lastSavedLine) {
|
|
|
573
588
|
return { timestamp: e.timestamp, thinking, text, toolDetails };
|
|
574
589
|
}).filter((m) => m !== null);
|
|
575
590
|
const interactions = [];
|
|
591
|
+
const firstUserTs = userMessages.length > 0 ? userMessages[0].timestamp : "9999-12-31T23:59:59Z";
|
|
592
|
+
const orphanedResponses = assistantMessages.filter(
|
|
593
|
+
(a) => a.timestamp < firstUserTs
|
|
594
|
+
);
|
|
595
|
+
const planEntry = planContentEntries.find((p) => p.timestamp <= firstUserTs);
|
|
596
|
+
if (orphanedResponses.length > 0 || planEntry) {
|
|
597
|
+
const allToolDetails = orphanedResponses.flatMap((r) => r.toolDetails);
|
|
598
|
+
const orphanedTimeKeys = new Set(
|
|
599
|
+
orphanedResponses.map((r) => r.timestamp.slice(0, 16))
|
|
600
|
+
);
|
|
601
|
+
const allToolResults = [...orphanedTimeKeys].flatMap(
|
|
602
|
+
(k) => toolResultsByTimestamp.get(k) || []
|
|
603
|
+
);
|
|
604
|
+
const allProgressEvents = [...orphanedTimeKeys].flatMap(
|
|
605
|
+
(k) => progressEvents.get(k) || []
|
|
606
|
+
);
|
|
607
|
+
interactions.push({
|
|
608
|
+
timestamp: orphanedResponses.length > 0 ? orphanedResponses[0].timestamp : planEntry?.timestamp ?? "",
|
|
609
|
+
// Include plan content for compact detection (UUID extraction)
|
|
610
|
+
user: planEntry?.content || "",
|
|
611
|
+
thinking: orphanedResponses.filter((r) => r.thinking).map((r) => r.thinking).join("\n"),
|
|
612
|
+
assistant: orphanedResponses.filter((r) => r.text).map((r) => r.text).join("\n"),
|
|
613
|
+
isCompactSummary: !!planEntry,
|
|
614
|
+
isContinuation: true,
|
|
615
|
+
toolsUsed: [...new Set(allToolDetails.map((t) => t.name))],
|
|
616
|
+
toolDetails: allToolDetails,
|
|
617
|
+
inPlanMode: isInPlanMode(
|
|
618
|
+
orphanedResponses[0]?.timestamp ?? planEntry?.timestamp ?? ""
|
|
619
|
+
) || void 0,
|
|
620
|
+
toolResults: allToolResults.length > 0 ? allToolResults : void 0,
|
|
621
|
+
progressEvents: allProgressEvents.length > 0 ? allProgressEvents : void 0
|
|
622
|
+
});
|
|
623
|
+
}
|
|
576
624
|
for (let i = 0; i < userMessages.length; i++) {
|
|
577
625
|
const user = userMessages[i];
|
|
578
626
|
const nextUserTs = i + 1 < userMessages.length ? userMessages[i + 1].timestamp : "9999-12-31T23:59:59Z";
|
|
@@ -614,6 +662,61 @@ var IGNORED_PREFIXES = [
|
|
|
614
662
|
".claude/"
|
|
615
663
|
];
|
|
616
664
|
var IGNORED_FILES = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock"];
|
|
665
|
+
function detectCompactContinuation(interactions, projectPath) {
|
|
666
|
+
const compactInteraction = interactions.find((i) => i.isCompactSummary);
|
|
667
|
+
if (!compactInteraction?.user) return null;
|
|
668
|
+
const match = compactInteraction.user.match(
|
|
669
|
+
/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/
|
|
670
|
+
);
|
|
671
|
+
if (!match) return null;
|
|
672
|
+
const oldClaudeSessionId = match[1];
|
|
673
|
+
return resolveMnemeSessionId(projectPath, oldClaudeSessionId);
|
|
674
|
+
}
|
|
675
|
+
function linkToMasterSession(projectPath, claudeSessionId, masterMnemeSessionId) {
|
|
676
|
+
const sessionLinksDir = path4.join(projectPath, ".mneme", "session-links");
|
|
677
|
+
if (!fs5.existsSync(sessionLinksDir)) {
|
|
678
|
+
fs5.mkdirSync(sessionLinksDir, { recursive: true });
|
|
679
|
+
}
|
|
680
|
+
const linkFile = path4.join(sessionLinksDir, `${claudeSessionId}.json`);
|
|
681
|
+
if (!fs5.existsSync(linkFile)) {
|
|
682
|
+
fs5.writeFileSync(
|
|
683
|
+
linkFile,
|
|
684
|
+
JSON.stringify(
|
|
685
|
+
{
|
|
686
|
+
masterSessionId: masterMnemeSessionId,
|
|
687
|
+
claudeSessionId,
|
|
688
|
+
linkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
689
|
+
},
|
|
690
|
+
null,
|
|
691
|
+
2
|
|
692
|
+
)
|
|
693
|
+
);
|
|
694
|
+
console.error(
|
|
695
|
+
`[mneme] Compact continuation linked: ${claudeSessionId} \u2192 ${masterMnemeSessionId}`
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
const masterFile = findSessionFileById(projectPath, masterMnemeSessionId);
|
|
699
|
+
if (masterFile && fs5.existsSync(masterFile)) {
|
|
700
|
+
try {
|
|
701
|
+
const master = JSON.parse(fs5.readFileSync(masterFile, "utf8"));
|
|
702
|
+
const workPeriods = master.workPeriods || [];
|
|
703
|
+
const alreadyLinked = workPeriods.some(
|
|
704
|
+
(wp) => wp.claudeSessionId === claudeSessionId
|
|
705
|
+
);
|
|
706
|
+
if (!alreadyLinked) {
|
|
707
|
+
workPeriods.push({
|
|
708
|
+
claudeSessionId,
|
|
709
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
710
|
+
endedAt: null
|
|
711
|
+
});
|
|
712
|
+
master.workPeriods = workPeriods;
|
|
713
|
+
master.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
714
|
+
fs5.writeFileSync(masterFile, JSON.stringify(master, null, 2));
|
|
715
|
+
}
|
|
716
|
+
} catch {
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
617
720
|
function isIgnoredPath(relativePath) {
|
|
618
721
|
return IGNORED_PREFIXES.some((p) => relativePath.startsWith(p)) || IGNORED_FILES.includes(relativePath);
|
|
619
722
|
}
|
|
@@ -667,7 +770,7 @@ async function incrementalSave(claudeSessionId, transcriptPath, projectPath) {
|
|
|
667
770
|
}
|
|
668
771
|
const dbPath = path4.join(projectPath, ".mneme", "local.db");
|
|
669
772
|
const db = initDatabase(dbPath);
|
|
670
|
-
|
|
773
|
+
let mnemeSessionId = resolveMnemeSessionId(projectPath, claudeSessionId);
|
|
671
774
|
const saveState = getSaveState(
|
|
672
775
|
db,
|
|
673
776
|
claudeSessionId,
|
|
@@ -678,7 +781,25 @@ async function incrementalSave(claudeSessionId, transcriptPath, projectPath) {
|
|
|
678
781
|
transcriptPath,
|
|
679
782
|
saveState.lastSavedLine
|
|
680
783
|
);
|
|
784
|
+
if (saveState.lastSavedLine === 0 && interactions.length > 0) {
|
|
785
|
+
const masterSessionId = detectCompactContinuation(
|
|
786
|
+
interactions,
|
|
787
|
+
projectPath
|
|
788
|
+
);
|
|
789
|
+
if (masterSessionId && masterSessionId !== mnemeSessionId) {
|
|
790
|
+
linkToMasterSession(projectPath, claudeSessionId, masterSessionId);
|
|
791
|
+
mnemeSessionId = masterSessionId;
|
|
792
|
+
updateSaveStateMnemeSessionId(db, claudeSessionId, masterSessionId);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
681
795
|
if (interactions.length === 0) {
|
|
796
|
+
updateSaveState(
|
|
797
|
+
db,
|
|
798
|
+
claudeSessionId,
|
|
799
|
+
saveState.lastSavedTimestamp || "",
|
|
800
|
+
totalLines
|
|
801
|
+
);
|
|
802
|
+
db.close();
|
|
682
803
|
return {
|
|
683
804
|
success: true,
|
|
684
805
|
savedCount: 0,
|
|
@@ -705,6 +826,7 @@ async function incrementalSave(claudeSessionId, transcriptPath, projectPath) {
|
|
|
705
826
|
toolsUsed: interaction.toolsUsed,
|
|
706
827
|
toolDetails: interaction.toolDetails,
|
|
707
828
|
...interaction.inPlanMode && { inPlanMode: true },
|
|
829
|
+
...interaction.isContinuation && { isContinuation: true },
|
|
708
830
|
...interaction.slashCommand && {
|
|
709
831
|
slashCommand: interaction.slashCommand
|
|
710
832
|
},
|
|
@@ -715,27 +837,30 @@ async function incrementalSave(claudeSessionId, transcriptPath, projectPath) {
|
|
|
715
837
|
progressEvents: interaction.progressEvents
|
|
716
838
|
}
|
|
717
839
|
});
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
840
|
+
if (!interaction.isContinuation) {
|
|
841
|
+
insertStmt.run(
|
|
842
|
+
mnemeSessionId,
|
|
843
|
+
claudeSessionId,
|
|
844
|
+
projectPath,
|
|
845
|
+
repository,
|
|
846
|
+
repositoryUrl,
|
|
847
|
+
repositoryRoot,
|
|
848
|
+
owner,
|
|
849
|
+
"user",
|
|
850
|
+
interaction.user,
|
|
851
|
+
null,
|
|
852
|
+
metadata,
|
|
853
|
+
interaction.timestamp,
|
|
854
|
+
interaction.isCompactSummary ? 1 : 0
|
|
855
|
+
);
|
|
856
|
+
insertedCount++;
|
|
857
|
+
}
|
|
858
|
+
if (interaction.assistant || interaction.thinking) {
|
|
735
859
|
const assistantMetadata = JSON.stringify({
|
|
736
860
|
toolsUsed: interaction.toolsUsed,
|
|
737
861
|
toolDetails: interaction.toolDetails,
|
|
738
862
|
...interaction.inPlanMode && { inPlanMode: true },
|
|
863
|
+
...interaction.isContinuation && { isContinuation: true },
|
|
739
864
|
...interaction.toolResults?.length && {
|
|
740
865
|
toolResults: interaction.toolResults
|
|
741
866
|
},
|
|
@@ -752,7 +877,7 @@ async function incrementalSave(claudeSessionId, transcriptPath, projectPath) {
|
|
|
752
877
|
repositoryRoot,
|
|
753
878
|
owner,
|
|
754
879
|
"assistant",
|
|
755
|
-
interaction.assistant,
|
|
880
|
+
interaction.assistant || "",
|
|
756
881
|
interaction.thinking || null,
|
|
757
882
|
assistantMetadata,
|
|
758
883
|
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