@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.
@@ -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.24.3",
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
- ![Version](https://img.shields.io/badge/version-0.24.3-blue)
3
+ ![Version](https://img.shields.io/badge/version-0.25.1-blue)
4
4
  ![Node.js](https://img.shields.io/badge/node-%3E%3D22.5.0-brightgreen)
5
5
  [![NPM Version](https://img.shields.io/npm/v/%40hir4ta%2Fmneme)](https://www.npmjs.com/package/@hir4ta/mneme)
6
6
  [![MIT License](https://img.shields.io/npm/l/%40hir4ta%2Fmneme)](https://github.com/hir4ta/mneme/blob/main/LICENSE)
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # mneme
2
2
 
3
- ![Version](https://img.shields.io/badge/version-0.24.3-blue)
3
+ ![Version](https://img.shields.io/badge/version-0.25.1-blue)
4
4
  ![Node.js](https://img.shields.io/badge/node-%3E%3D22.5.0-brightgreen)
5
5
  [![NPM Version](https://img.shields.io/npm/v/%40hir4ta%2Fmneme)](https://www.npmjs.com/package/@hir4ta/mneme)
6
6
  [![MIT License](https://img.shields.io/npm/l/%40hir4ta%2Fmneme)](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
 
@@ -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
- const mnemeSessionId = resolveMnemeSessionId(projectPath, claudeSessionId);
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
- insertStmt.run(
715
- mnemeSessionId,
716
- claudeSessionId,
717
- projectPath,
718
- repository,
719
- repositoryUrl,
720
- repositoryRoot,
721
- owner,
722
- "user",
723
- interaction.user,
724
- null,
725
- metadata,
726
- interaction.timestamp,
727
- interaction.isCompactSummary ? 1 : 0
728
- );
729
- insertedCount++;
730
- if (interaction.assistant) {
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
- const mnemeSessionId = resolveMnemeSessionId(projectPath, claudeSessionId);
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
- insertStmt.run(
719
- mnemeSessionId,
720
- claudeSessionId,
721
- projectPath,
722
- repository,
723
- repositoryUrl,
724
- repositoryRoot,
725
- owner,
726
- "user",
727
- interaction.user,
728
- null,
729
- metadata,
730
- interaction.timestamp,
731
- interaction.isCompactSummary ? 1 : 0
732
- );
733
- insertedCount++;
734
- if (interaction.assistant) {
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,
@@ -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
@@ -4493,7 +4493,7 @@ misc.get("/project", (c) => {
4493
4493
  }
4494
4494
  } catch {
4495
4495
  }
4496
- const version = "0.24.3";
4496
+ const version = "0.25.1";
4497
4497
  return c.json({
4498
4498
  name: projectName,
4499
4499
  path: projectRoot,
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hir4ta/mneme",
3
- "version": "0.24.3",
3
+ "version": "0.25.1",
4
4
  "description": "Long-term memory plugin for Claude Code - automated session saving, recording technical decisions, and web dashboard",
5
5
  "keywords": [
6
6
  "claude",