kyp-mem 0.7.3 → 0.7.4

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/bin/cli.mjs CHANGED
@@ -26,13 +26,25 @@ function run(command, cmdArgs, stdio = "ignore") {
26
26
  if (args[0] === "hook") {
27
27
  const hookType = args[1];
28
28
  const sessionDir = join(homedir(), ".kyp-mem", "sessions");
29
- const sessionFile = join(sessionDir, "current.jsonl");
30
29
 
31
30
  const chunks = [];
32
31
  process.stdin.on("data", (chunk) => chunks.push(chunk));
33
32
  await new Promise((r) => process.stdin.on("end", r));
34
33
  const raw = Buffer.concat(chunks).toString();
35
34
 
35
+ // Partition the activity log per Claude session id. Previously every session
36
+ // (across all projects) appended to one shared current.jsonl, so concurrent
37
+ // sessions interleaved and the Stop hook filed the whole batch under whichever
38
+ // project logged first — leaking foreign summaries into unrelated projects.
39
+ let sessionId = "";
40
+ try {
41
+ sessionId = ((JSON.parse(raw) || {}).session_id || "").toString();
42
+ } catch (_) {}
43
+ const safeId = sessionId.replace(/[^A-Za-z0-9_-]/g, "");
44
+ const sessionFile = safeId
45
+ ? join(sessionDir, `current-${safeId}.jsonl`)
46
+ : join(sessionDir, "current.jsonl");
47
+
36
48
  if (hookType === "user-prompt") {
37
49
  try {
38
50
  const data = JSON.parse(raw);
@@ -59,7 +71,7 @@ if (args[0] === "hook") {
59
71
  const input = data.tool_input || {};
60
72
  const rawResp = data.tool_response || "";
61
73
  const resp = (typeof rawResp === "string" ? rawResp : JSON.stringify(rawResp)).slice(0, 2000);
62
- const entry = { ts: new Date().toISOString(), tool, cwd: process.cwd() };
74
+ const entry = { ts: new Date().toISOString(), tool, cwd: process.env.CLAUDE_PROJECT_DIR || process.cwd() };
63
75
 
64
76
  if (tool === "Edit" || tool === "Write") {
65
77
  entry.file = input.file_path || "";
@@ -93,7 +105,7 @@ if (args[0] === "hook") {
93
105
  const py = resolvePython();
94
106
  if (py) {
95
107
  const [cmd, pre] = py;
96
- const r = run(cmd, [...pre, "-m", "kyp_mem.hooks", "stop"], "inherit");
108
+ const r = run(cmd, [...pre, "-m", "kyp_mem.hooks", "stop", safeId], "inherit");
97
109
  process.exit(r.status ?? 0);
98
110
  }
99
111
  process.exit(0);
package/kyp_mem/hooks.py CHANGED
@@ -10,6 +10,37 @@ from pathlib import Path
10
10
  SESSION_DIR = Path.home() / ".kyp-mem" / "sessions"
11
11
  CURRENT_SESSION = SESSION_DIR / "current.jsonl"
12
12
 
13
+
14
+ def _session_file(session_id):
15
+ """Per-Claude-session activity log path.
16
+
17
+ Each Claude session gets its own ``current-<session_id>.jsonl`` so that
18
+ concurrent sessions in different projects never share one file. Falls back
19
+ to the legacy shared ``current.jsonl`` when no session id is available.
20
+ """
21
+ if session_id:
22
+ safe = "".join(c for c in str(session_id) if c.isalnum() or c in "_-")
23
+ if safe:
24
+ return SESSION_DIR / f"current-{safe}.jsonl"
25
+ return CURRENT_SESSION
26
+
27
+
28
+ def _prune_stale_logs(max_age_days=3):
29
+ """Remove orphaned activity logs left by sessions that never fired Stop
30
+ (crashes, kills) plus the legacy shared current.jsonl once it goes idle."""
31
+ import time
32
+
33
+ cutoff = time.time() - max_age_days * 86400
34
+ try:
35
+ for f in SESSION_DIR.glob("current*.jsonl"):
36
+ try:
37
+ if f.stat().st_mtime < cutoff:
38
+ f.unlink(missing_ok=True)
39
+ except OSError:
40
+ pass
41
+ except Exception:
42
+ pass
43
+
13
44
  MIN_ACTIONS = 5
14
45
  CHARS_PER_TOKEN = 4
15
46
 
@@ -123,20 +154,26 @@ def handle_session_start():
123
154
  cwd = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
124
155
  project_name = Path(cwd).name
125
156
 
157
+ _prune_stale_logs()
158
+
126
159
  try:
127
160
  from .config import get_vault_path
128
161
  from .vault import Vault
129
162
 
130
163
  vault = Vault(get_vault_path())
131
164
 
132
- project_notes = [p for p in vault.index.notes if p.startswith(f"{project_name}/")]
165
+ # Match case-insensitively: the project dir may be stored in the vault
166
+ # with different casing than the cwd basename (e.g. on case-insensitive
167
+ # filesystems "KYP-MEM" and "kyp-mem" are the same directory).
168
+ prefix = f"{project_name}/".lower()
169
+ project_notes = [p for p in vault.index.notes if p.lower().startswith(prefix)]
133
170
  if not project_notes:
134
171
  return
135
172
 
136
173
  sessions = sorted(
137
- (p for p in project_notes if "/Sessions/" in p),
174
+ (p for p in project_notes if "/sessions/" in p.lower()),
138
175
  reverse=True,
139
- )[:3]
176
+ )[:10]
140
177
  if not sessions:
141
178
  return
142
179
 
@@ -193,7 +230,7 @@ def handle_user_prompt():
193
230
  }
194
231
 
195
232
  SESSION_DIR.mkdir(parents=True, exist_ok=True)
196
- with open(CURRENT_SESSION, "a") as f:
233
+ with open(_session_file(data.get("session_id", "")), "a") as f:
197
234
  f.write(json.dumps(entry) + "\n")
198
235
 
199
236
 
@@ -255,7 +292,7 @@ def handle_post_tool_use():
255
292
  return
256
293
 
257
294
  SESSION_DIR.mkdir(parents=True, exist_ok=True)
258
- with open(CURRENT_SESSION, "a") as f:
295
+ with open(_session_file(data.get("session_id", "")), "a") as f:
259
296
  f.write(json.dumps(entry) + "\n")
260
297
 
261
298
 
@@ -513,13 +550,14 @@ Raw session data:
513
550
  return None
514
551
 
515
552
 
516
- def handle_stop():
517
- if _is_subprocess() or not CURRENT_SESSION.exists():
553
+ def handle_stop(session_id=""):
554
+ session_file = _session_file(session_id)
555
+ if _is_subprocess() or not session_file.exists():
518
556
  return
519
557
 
520
- text = CURRENT_SESSION.read_text().strip()
558
+ text = session_file.read_text().strip()
521
559
  if not text:
522
- CURRENT_SESSION.unlink(missing_ok=True)
560
+ session_file.unlink(missing_ok=True)
523
561
  return
524
562
 
525
563
  entries = []
@@ -530,12 +568,12 @@ def handle_stop():
530
568
  continue
531
569
 
532
570
  if not entries:
533
- CURRENT_SESSION.unlink(missing_ok=True)
571
+ session_file.unlink(missing_ok=True)
534
572
  return
535
573
 
536
574
  write_actions = [e for e in entries if e.get("action") in ("edit", "create", "command")]
537
575
  if len(write_actions) < MIN_ACTIONS:
538
- CURRENT_SESSION.unlink(missing_ok=True)
576
+ session_file.unlink(missing_ok=True)
539
577
  return
540
578
 
541
579
  project_dir = entries[0].get("cwd", "unknown")
@@ -691,8 +729,8 @@ def handle_stop():
691
729
  pass
692
730
 
693
731
  # Delete session file BEFORE summarization so the spawned claude subprocess
694
- # doesn't pollute it via hooks writing back into current.jsonl
695
- CURRENT_SESSION.unlink(missing_ok=True)
732
+ # doesn't pollute it via hooks writing back into the session log
733
+ session_file.unlink(missing_ok=True)
696
734
 
697
735
  # Try Claude summarization, fall back to raw sections
698
736
  summarized = _summarize_with_claude(raw_note, project_name)
@@ -765,7 +803,8 @@ def handle_stop():
765
803
 
766
804
  def main():
767
805
  if len(sys.argv) > 1 and sys.argv[1] == "stop":
768
- handle_stop()
806
+ session_id = sys.argv[2] if len(sys.argv) > 2 else ""
807
+ handle_stop(session_id)
769
808
  else:
770
809
  raw = sys.stdin.read().strip()
771
810
  if not raw:
@@ -775,7 +814,7 @@ def main():
775
814
  except json.JSONDecodeError:
776
815
  return
777
816
  if "stop_reason" in data:
778
- handle_stop()
817
+ handle_stop(data.get("session_id", ""))
779
818
 
780
819
 
781
820
  if __name__ == "__main__":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kyp-mem",
3
- "version": "0.7.3",
3
+ "version": "0.7.4",
4
4
  "description": "Know Your Project — Persistent & Session level knowledge base for AI agents. MCP-powered with wikilinks, backlinks, auto-learning, and neon web UI.",
5
5
  "bin": {
6
6
  "kyp-mem": "bin/cli.mjs"
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "kyp-mem"
7
- version = "0.7.3"
7
+ version = "0.7.4"
8
8
  description = "Know Your Project — Persistent knowledge base for AI agents. MCP-powered with wikilinks, backlinks, auto-learning, and neon web UI."
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}