agent-memory-git 0.1.13__tar.gz → 0.1.15__tar.gz

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.
Files changed (67) hide show
  1. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/PKG-INFO +1 -1
  2. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/pyproject.toml +1 -1
  3. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/pyproject.toml.orig +1 -1
  4. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/PROTOCOL.md +1 -1
  5. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/capture.py +72 -1
  6. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/cli.py +1 -6
  7. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/sessions.py +20 -0
  8. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/ui.py +10 -18
  9. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/webui/app.css +5 -0
  10. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/webui/js/icons.js +1 -0
  11. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/webui/js/views/insights.js +6 -2
  12. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/webui/js/views/list.js +2 -2
  13. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/webui/js/views/settings.js +10 -1
  14. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/LICENSE +0 -0
  15. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/README.md +0 -0
  16. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/__init__.py +0 -0
  17. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/__main__.py +0 -0
  18. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/config.py +0 -0
  19. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/context.py +0 -0
  20. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/demo/personal/2026/08/20260816-blog-publishing-k3qm.md +0 -0
  21. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/demo/personal/2026/09/20260906-push-personal-repos-w8re.md +0 -0
  22. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/demo/personal/2026/09/20260921-home-server-n2vb.md +0 -0
  23. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/demo/personal/2026/10/20261004-options-first-7hcx.md +0 -0
  24. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/demo/personal/2026/10/20261009-thesis-setup-fr6e.md +0 -0
  25. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/demo/work/2026/08/20260804-alex-profile-ujzd.md +0 -0
  26. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/demo/work/2026/08/20260804-reply-language-xdnc.md +0 -0
  27. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/demo/work/2026/08/20260812-ask-before-push-pf3d.md +0 -0
  28. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/demo/work/2026/08/20260820-tests-before-done-odzd.md +0 -0
  29. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/demo/work/2026/08/20260827-etl-orders-export-deadline-pft7.md +0 -0
  30. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/demo/work/2026/08/20260828-etl-dashboard-72ce.md +0 -0
  31. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/demo/work/2026/09/20260902-report-eta-is2j.md +0 -0
  32. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/demo/work/2026/09/20260908-shop-web-stripe-checkout-lgmx.md +0 -0
  33. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/demo/work/2026/09/20260910-shop-web-deploys-dn73.md +0 -0
  34. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/demo/work/2026/09/20260915-etl-parquet-partitioning-seh2.md +0 -0
  35. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/demo/work/2026/09/20260922-design-tokens-source-uvw7.md +0 -0
  36. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/demo/work/2026/10/20261002-shop-web-v3-freeze-5xtp.md +0 -0
  37. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/demo/work/2026/10/20261012-payments-runbook-4syw.md +0 -0
  38. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/demo.py +0 -0
  39. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/embed_worker.py +0 -0
  40. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/frontmatter.py +0 -0
  41. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/gitsync.py +0 -0
  42. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/hooks.py +0 -0
  43. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/importers.py +0 -0
  44. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/install.py +0 -0
  45. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/lint.py +0 -0
  46. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/profiles.py +0 -0
  47. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/select.py +0 -0
  48. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/store.py +0 -0
  49. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/ui_server.py +0 -0
  50. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/update.py +0 -0
  51. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/util.py +0 -0
  52. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/webui/index.html +0 -0
  53. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/webui/js/api.js +0 -0
  54. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/webui/js/avatar.js +0 -0
  55. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/webui/js/filters.js +0 -0
  56. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/webui/js/format.js +0 -0
  57. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/webui/js/h.js +0 -0
  58. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/webui/js/main.js +0 -0
  59. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/webui/js/markdown.js +0 -0
  60. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/webui/js/search.js +0 -0
  61. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/webui/js/store.js +0 -0
  62. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/webui/js/views/memory.js +0 -0
  63. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/webui/js/views/overlays.js +0 -0
  64. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/webui/js/views/profiles.js +0 -0
  65. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/webui/js/views/shell.js +0 -0
  66. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/webui/js/views/ui.js +0 -0
  67. {agent_memory_git-0.1.13 → agent_memory_git-0.1.15}/src/agent_memory/workdir.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-memory-git
3
- Version: 0.1.13
3
+ Version: 0.1.15
4
4
  Summary: Shared, git-backed memory for Claude Code and Codex.
5
5
  Keywords: claude-code,codex,memory,agents,git
6
6
  Author: Shichao Song
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agent-memory-git"
3
- version = "0.1.13"
3
+ version = "0.1.15"
4
4
  description = "Shared, git-backed memory for Claude Code and Codex."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agent-memory-git"
3
- version = "0.1.13"
3
+ version = "0.1.15"
4
4
  description = "Shared, git-backed memory for Claude Code and Codex."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  # Shared memory
2
2
 
3
- You have a persistent memory that Claude Code and Codex share. Every memory is one Markdown file in a git repository. At the start of each session a hook injects a block titled "Shared memory index": it lists the memories relevant to the current folder and repository, and gives the memories directory, this session's staging directory and the path of the `mem` command. If the active profile changes while the session runs, a new index arrives with your next message and replaces the earlier one: from then on use only its memories and paths. If that block is missing, the memory system is not active in this session; ignore this section.
3
+ You have a persistent memory that Claude Code and Codex share. Every memory is one Markdown file in a git repository. At the start of each session a hook injects a block titled "Shared memory index": it lists the memories relevant to the current folder and repository, and gives the memories directory, this session's staging directory and the path of the `mem` command. When the conversation holds more than one index, only the latest counts: use its memories and paths, its staging directory above all. A new one arrives with your next message when the active profile changes, and a session resumed, forked or continued from another starts with that session's index still in view. If that block is missing, the memory system is not active in this session; ignore this section.
4
4
 
5
5
  ## Reading
6
6
 
@@ -7,7 +7,7 @@ from pathlib import Path
7
7
 
8
8
  from . import frontmatter, gitsync, sessions, workdir
9
9
  from .context import detect
10
- from .store import memories_dir, normalize, read, staging_dir
10
+ from .store import load_all, memories_dir, normalize, read, staging_dir
11
11
  from .util import atomic_write, id_path, log, new_id, now, now_iso
12
12
 
13
13
  SESSION_FILE = ".session.json"
@@ -124,6 +124,49 @@ def promote(data_dir, sdir, session=None):
124
124
  return count
125
125
 
126
126
 
127
+ PLACE_KEYS = ("cwd", "original_cwd", "git_root", "git_remote", "git_branch", "git_commit")
128
+
129
+
130
+ def relocated(source, ctx):
131
+ """`source` moved to the project `ctx` points at (a context from relocate()), in the order _source() writes."""
132
+ items = [(k, v) for k, v in source.items() if k not in PLACE_KEYS]
133
+ place = [(k, ctx[k]) for k in PLACE_KEYS if k != "git_commit" and ctx.get(k)]
134
+ at = next((i for i, (k, _) in enumerate(items) if k == "transcript"), len(items))
135
+ return dict(items[:at] + place + items[at:])
136
+
137
+
138
+ def rehome(data_dir, session):
139
+ """Move this session's `here` memories still filed under its throwaway folder to the project it worked in.
140
+
141
+ promote() decides from the transcript so far, which finds nothing when a memory is written before the
142
+ session touches a project folder on this machine; each later turn tries again. Returns how many moved.
143
+ """
144
+ ctx = session.get("ctx") or {}
145
+ tool, sid = session.get("tool"), session.get("session_id")
146
+ if not sid or not workdir.is_throwaway(ctx.get("cwd")):
147
+ return 0
148
+ left = [
149
+ m
150
+ for m in load_all(data_dir)
151
+ if m["scope"] == "here"
152
+ and m["source"].get("session_id") == sid
153
+ and m["source"].get("tool") == tool
154
+ and workdir.is_throwaway(m["source"].get("cwd"))
155
+ and not m["source"].get("original_cwd")
156
+ ]
157
+ if not left:
158
+ return 0
159
+ moved = relocate(ctx, session.get("transcript_path") or sessions.transcript_path(tool, sid))
160
+ if not moved.get("original_cwd"):
161
+ return 0 # still no project to point at
162
+ for m in left:
163
+ path = memories_dir(data_dir) / m["rel"]
164
+ meta, body = read(path)
165
+ meta["source"] = relocated(meta["source"], moved)
166
+ atomic_write(path, frontmatter.join(meta, body))
167
+ return len(left)
168
+
169
+
127
170
  def stamp_updates(data_dir):
128
171
  """Set `updated` on tracked memory files that were edited in place."""
129
172
  r = gitsync.git(data_dir, "status", "--porcelain", "-z", "--", "memories")
@@ -175,6 +218,7 @@ def flush(data_dir, sdir, final=False, lock_timeout=5.0):
175
218
  return False
176
219
  session = read_session(sdir)
177
220
  n = promote(data_dir, sdir, session)
221
+ rehome(data_dir, session)
178
222
  committed = commit(data_dir, session)
179
223
  if final and Path(sdir).is_dir() and not any(Path(sdir).glob("*.md")):
180
224
  shutil.rmtree(sdir, ignore_errors=True)
@@ -183,6 +227,31 @@ def flush(data_dir, sdir, final=False, lock_timeout=5.0):
183
227
  return committed
184
228
 
185
229
 
230
+ TOOLS = ("claude-code", "codex") # the prefixes of staging folder names: <tool>-<session id>
231
+
232
+
233
+ def adopt(sdir):
234
+ """Session info for a staging folder that has none, from its name and that session's transcript.
235
+
236
+ An agent can write into a staging folder that is not its session's own, such as the one named in the older
237
+ index of a session it was continued from; the folder it makes has no .session.json, and without this the
238
+ memories in it are committed with no provenance at all. They are credited to the folder's session.
239
+ """
240
+ name = Path(sdir).name
241
+ tool = next((t for t in TOOLS if name.startswith(t + "-")), None)
242
+ if not tool:
243
+ return
244
+ sid = name[len(tool) + 1 :]
245
+ transcript = sessions.transcript_path(tool, sid)
246
+ info = {"tool": tool, "session_id": sid, "transcript_path": transcript}
247
+ folder = sessions.cwd(tool, sid, transcript)
248
+ if folder:
249
+ ctx = detect(folder)
250
+ ctx.pop("git_commit", None) # HEAD now, not what the session saw
251
+ info["ctx"] = ctx
252
+ write_session(sdir, info)
253
+
254
+
186
255
  def recover_orphans(data_dir, keep=None):
187
256
  """Promote staging dirs left by sessions that ended without a final flush."""
188
257
  root = staging_dir(data_dir)
@@ -196,6 +265,8 @@ def recover_orphans(data_dir, keep=None):
196
265
  age = time.time() - (marker.stat().st_mtime if marker.exists() else sdir.stat().st_mtime)
197
266
  has_files = any(sdir.glob("*.md"))
198
267
  if has_files and age > ORPHAN_AGE:
268
+ if not marker.exists():
269
+ adopt(sdir)
199
270
  committed |= flush(data_dir, sdir, final=True, lock_timeout=1.0)
200
271
  elif not has_files and age > STALE_AGE:
201
272
  shutil.rmtree(sdir, ignore_errors=True)
@@ -129,12 +129,7 @@ def cmd_relocate(args):
129
129
  if args.dry_run:
130
130
  continue
131
131
  meta, body = store.read(mdir / m["rel"])
132
- source = dict(meta.get("source") or {})
133
- for key in ("cwd", "git_root", "git_remote", "git_branch"):
134
- source.pop(key, None)
135
- if ctx.get(key):
136
- source[key] = ctx[key]
137
- source["original_cwd"] = ctx["original_cwd"]
132
+ source = capture.relocated(dict(meta.get("source") or {}), ctx)
138
133
  if transcript:
139
134
  source.setdefault("transcript", transcript)
140
135
  meta["source"] = source
@@ -53,6 +53,26 @@ def transcript_path(tool, session_id):
53
53
  return str(hits[0]) if hits else ""
54
54
 
55
55
 
56
+ def cwd(tool, session_id, transcript=None):
57
+ """The folder a session ran in: Codex's thread record, or the first Claude transcript line that names one."""
58
+ if tool == "codex":
59
+ return _codex_thread(session_id, "cwd")
60
+ try:
61
+ with open(transcript, encoding="utf-8", errors="replace") as f:
62
+ for raw in f:
63
+ if '"cwd"' not in raw:
64
+ continue
65
+ try:
66
+ found = json.loads(raw).get("cwd")
67
+ except ValueError:
68
+ continue
69
+ if found:
70
+ return found
71
+ except (OSError, TypeError):
72
+ pass
73
+ return ""
74
+
75
+
56
76
  def title(tool, session_id, transcript=None):
57
77
  if tool == "codex":
58
78
  name = _codex_title(session_id)
@@ -316,19 +316,15 @@ def embed(data_dir, texts):
316
316
 
317
317
 
318
318
  def closeness(vectors):
319
- """Cosine similarity rescaled so a typical pair is 0 and the closest pair is 1.
319
+ """The cosine similarity of every pair of unit rows, as it is (below 0 reads as 0), and what a typical pair
320
+ scores: the median over pairs of different memories. Pairs at or below it are not listed as similar.
320
321
 
321
- Embedding cosines bunch together (most unrelated pairs still score 0.3-0.5),
322
- so the raw number says little; the rescaled one reads like a percentage.
322
+ It is not rescaled: rescaling made the closest pair 100% however far apart it was.
323
323
  """
324
- raw = vectors @ vectors.T
325
- off = raw[~np.eye(len(raw), dtype=bool)]
326
- if not len(off):
327
- return np.ones_like(raw)
328
- lo, hi = np.percentile(off, 50), off.max()
329
- sim = np.clip((raw - lo) / max(hi - lo, 1e-6), 0, 1)
324
+ sim = np.clip(vectors @ vectors.T, 0, 1)
325
+ off = sim[~np.eye(len(sim), dtype=bool)]
330
326
  np.fill_diagonal(sim, 1)
331
- return sim
327
+ return sim, float(np.median(off)) if len(off) else 0.0
332
328
 
333
329
 
334
330
  # ------------------------------------------------------------ page data
@@ -366,13 +362,9 @@ def build(data_dir, semantic=True, vectors_out=None):
366
362
  x, vocab = vectorize(docs)
367
363
  forms = _display_forms(clean(t + " " + b) for t, b in docs)
368
364
  vectors = embed(data_dir, [prose(t, b) for t, b in docs]) if semantic else None
369
- if vectors is not None:
370
- sim, space = closeness(vectors), vectors
371
- method = f"{MODEL['name']}, local"
372
- else:
373
- sim, space = np.clip(x @ x.T, 0, 1), x
374
- np.fill_diagonal(sim, 1)
375
- method = "keywords only (no local model)"
365
+ space = vectors if vectors is not None else x
366
+ sim, typical = closeness(space)
367
+ method = f"{MODEL['name']}, local" if vectors is not None else "keywords only (no local model)"
376
368
  if vectors_out is not None:
377
369
  vectors_out[:] = [space]
378
370
 
@@ -399,7 +391,7 @@ def build(data_dir, semantic=True, vectors_out=None):
399
391
  "host": src.get("host") or "",
400
392
  "created": m["created"],
401
393
  "updated": m["updated"],
402
- "similar": [{"i": j, "s": round(float(sim[i, j]), 3)} for j in order if sim[i, j] > 0],
394
+ "similar": [{"i": j, "s": round(float(sim[i, j]), 3)} for j in order if sim[i, j] > typical],
403
395
  "keywords": [{"k": vocab[j], "t": forms.get(vocab[j], vocab[j])} for j in picks[i]],
404
396
  "terms": [vocab[j] for j in np.flatnonzero(x[i])],
405
397
  "issues": [message for _, message in lint.check(m, body, names)],
@@ -559,6 +559,11 @@ button.hero-meta-item:hover .copy-hint, button.hero-meta-item:focus-visible .cop
559
559
  .doctor-line .i { width: 13px; height: 13px; }
560
560
  .doctor-sum .chev { transition: transform .15s; }
561
561
  .doctor-sum .chev.turned { transform: rotate(180deg); }
562
+ .about { display: flex; justify-content: center; align-items: center; gap: 2px; margin-top: 4px; font-size: 12.5px; color: var(--ink-3); }
563
+ .about .sep { margin: 0 4px; }
564
+ .about a { display: inline-flex; height: 28px; align-items: center; gap: 4px; padding: 0 8px; border-radius: 999px; color: var(--ink-3); text-decoration: none; transition: background-color .15s, color .15s; }
565
+ .about a:hover { background: var(--hover); color: var(--ink); }
566
+ .about .i { width: 12px; height: 12px; }
562
567
  .checks { width: 100%; margin-top: 12px; padding: 16px 20px 18px; border-radius: var(--r-card); background: var(--panel); animation: rise .15s ease-out; }
563
568
  .checks-note { margin-bottom: 10px; font-size: 12px; color: var(--ink-3); }
564
569
  .checks-note code { font-size: 11.5px; color: var(--ink-2); }
@@ -16,6 +16,7 @@ const PATHS = {
16
16
  "arrow-left": `<path d="m12 19-7-7 7-7"/><path d="M19 12H5"/>`,
17
17
  "arrow-right": `<path d="M5 12h14"/><path d="m12 5 7 7-7 7"/>`,
18
18
  "arrow-up": `<path d="m5 12 7-7 7 7"/><path d="M12 19V5"/>`,
19
+ "arrow-up-right": `<path d="M7 7h10v10"/><path d="M7 17 17 7"/>`,
19
20
  "chart-column": `<path d="M3 3v16a2 2 0 0 0 2 2h16"/><path d="M18 17V9"/><path d="M13 17V5"/><path d="M8 17v-3"/>`,
20
21
  "check": `<path d="M20 6 9 17l-5-5"/>`,
21
22
  "chevron-down": `<path d="m6 9 6 6 6-6"/>`,
@@ -136,7 +136,7 @@ export function insightsPage() {
136
136
  <section class="card span2"><h2>Possible duplicates</h2><p class="card-note">Pairs that read alike; compare them side by side and keep one.</p>
137
137
  ${pairs.length ? pairs.map((p, i) => html`<div class="pair"><div class="pair-text"><span class="truncate">${p[0].description}</span><span class="truncate faint2">${p[1].description}</span></div>
138
138
  <span class="count">${Math.round(p[2] * 100)}%</span><button type="button" class="btn sm" data-act="compare" data-arg="${i}">Compare</button></div>`)
139
- : html`<p class="card-note">No two memories are more than 45% alike.</p>`}
139
+ : html`<p class="card-note">No likely duplicates: no two memories are ${Math.round(DUPLICATE * 100)}% alike or more.</p>`}
140
140
  </section>
141
141
 
142
142
  <section class="card span2"><h2>Changes per week</h2>${chart("changes", changes, WHO, TOOL_COLORS, WHO_LABELS, `No memory changes in the past ${WEEKS} weeks.`)}</section>
@@ -153,9 +153,13 @@ export function insightsPage() {
153
153
  </div>`;
154
154
  }
155
155
 
156
+ /** Cosine similarity at or above which two memories may say the same thing. Set from pairs that were merged as
157
+ * duplicates (0.69-0.70 with the embedding model) and related ones that were not (0.63 and below). */
158
+ const DUPLICATE = 0.65;
159
+
156
160
  /** The pairs shown under Possible duplicates, so Compare can find its pair by position. */
157
161
  export function duplicatePairs() {
158
162
  const M = app.M, pairs = [];
159
- M.forEach((m, i) => m.similar.forEach((s) => { if (s.i > i && s.s >= 0.45 && M[s.i]) pairs.push([m, M[s.i], s.s]); }));
163
+ M.forEach((m, i) => m.similar.forEach((s) => { if (s.i > i && s.s >= DUPLICATE && M[s.i]) pairs.push([m, M[s.i], s.s]); }));
160
164
  return pairs.sort((a, b) => b[2] - a[2]).slice(0, 5);
161
165
  }
@@ -52,7 +52,7 @@ function facet(def) {
52
52
  ${options.map((o) => {
53
53
  const on = values.includes(o.value);
54
54
  return html`<button type="button" role="menuitemcheckbox" class="${cx("menu-item", !o.count && !on && "faint")}"${attrs({ "aria-checked": String(on), "data-act": "toggle", "data-arg": `${def.key}:${o.value}`, "data-key": `${def.key}:${o.value}` })}>
55
- <span class="tick">${on ? icon("check", "", 3) : ""}</span><span class="grow">${o.label}</span><span class="count">${o.count}</span></button>`;
55
+ <span class="tick">${on ? icon("check", "", 3) : ""}</span><span class="grow truncate"${attrs({ title: o.label })}>${o.label}</span><span class="count">${o.count}</span></button>`;
56
56
  })}
57
57
  </div>
58
58
  ${values.length ? html`<div class="menu-foot"><button type="button" class="menu-wide" data-act="setList" data-arg="${def.key}">Clear ${def.label.toLowerCase()}</button></div>` : ""}`;
@@ -73,7 +73,7 @@ function added() {
73
73
  ${opts.map((o) => {
74
74
  const on = v === o.value;
75
75
  return html`<button type="button" role="radio" class="${cx("menu-item", !o.count && !on && "faint")}"${attrs({ "aria-checked": String(on), "data-act": "setAdded", "data-arg": on ? "" : o.value })}>
76
- <span class="radio">${on ? html`<i></i>` : ""}</span><span class="grow">${o.label}</span><span class="count">${o.count}</span></button>`;
76
+ <span class="radio">${on ? html`<i></i>` : ""}</span><span class="grow truncate"${attrs({ title: o.label })}>${o.label}</span><span class="count">${o.count}</span></button>`;
77
77
  })}
78
78
  </div>
79
79
  <div class="menu-range">
@@ -123,6 +123,15 @@ function doctor() {
123
123
  </section>`;
124
124
  }
125
125
 
126
+ // ---------------------------------------------------------------- where agent-memory lives
127
+
128
+ /** The project's site and repository, as [project.urls] in pyproject.toml has them. */
129
+ const LINKS = [["Website", "https://ki-seki.github.io/agent-memory/"], ["GitHub", "https://github.com/Ki-Seki/agent-memory"]];
130
+
131
+ function about() {
132
+ return html`<nav class="about" aria-label="agent-memory">${LINKS.map(([text, href], i) => html`${i ? html`<span class="sep">·</span>` : ""}<a${attrs({ href, target: "_blank", rel: "noopener noreferrer" })}>${text}${icon("arrow-up-right")}</a>`)}</nav>`;
133
+ }
134
+
126
135
  export function settingsPage() {
127
- return html`<div class="settings">${hero()}${prefs()}${doctor()}</div>`;
136
+ return html`<div class="settings">${hero()}${prefs()}${doctor()}${about()}</div>`;
128
137
  }