@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.
@@ -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.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
- ![Version](https://img.shields.io/badge/version-0.24.3-blue)
3
+ ![Version](https://img.shields.io/badge/version-0.25.0-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.0-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";
@@ -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
- const mnemeSessionId = resolveMnemeSessionId(projectPath, claudeSessionId);
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
- 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) {
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
- const mnemeSessionId = resolveMnemeSessionId(projectPath, claudeSessionId);
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
- 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) {
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,
@@ -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.0";
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.0",
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",