Update logbook: Reproduction: Memora: A Harmonic Memory Representation Balancing Abstraction and Specificity
Browse files- README.md +14 -7
- bucket-icon.svg +5 -0
- index.html +82 -17
- logbook.css +2142 -0
- logbook.js +0 -0
- logbook.json +74 -0
- pages/claim-1-locomo-policy-retriever-reaches-0-863-llm-as-a-judge-and-exceeds-full-context/page.md +2440 -0
- pages/claim-2-locomo-semantic-retriever-reaches-0-849-llm-as-a-judge-and-exceeds-full-context/page.md +752 -0
- pages/claim-3-longmemeval-reaches-87-4-average-accuracy-with-up-to-98-token-reduction/page.md +8 -0
- pages/claim-4-harmonic-memory-uses-a-primary-abstraction-concrete-memory-value-and-cue-anchors/page.md +8 -0
- pages/claim-5-rag-and-kg-retrieval-are-special-cases-of-the-memora-dual-layer-design/page.md +8 -0
- pages/conclusion/page.md +8 -0
- pages/executive-summary/page.md +0 -0
- pages/index.md +13 -0
- trackio-logo-light.png +0 -0
- trackio-logo.png +0 -0
- trackio-wordmark-dark.png +0 -0
- workspace.json +8 -0
README.md
CHANGED
|
@@ -1,12 +1,19 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
colorFrom: pink
|
| 7 |
-
colorTo: indigo
|
| 8 |
sdk: static
|
| 9 |
pinned: false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: "Reproduction: Memora: A Harmonic Memory Representation Balancing Abstraction and Specificity"
|
| 3 |
+
emoji: 🎯
|
| 4 |
+
colorFrom: yellow
|
| 5 |
+
colorTo: red
|
|
|
|
|
|
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
| 8 |
+
tags:
|
| 9 |
+
- trackio
|
| 10 |
+
- trackio-logbook
|
| 11 |
+
- open-experiment
|
| 12 |
+
- icml2026-repro
|
| 13 |
+
- paper-zSrvkj0ers
|
| 14 |
+
- arxiv:2602.03315
|
| 15 |
---
|
| 16 |
|
| 17 |
+
# Reproduction: Memora: A Harmonic Memory Representation Balancing Abstraction and Specificity
|
| 18 |
+
|
| 19 |
+
An open experiment logbook, published with [Trackio](https://github.com/gradio-app/trackio).
|
bucket-icon.svg
ADDED
|
|
index.html
CHANGED
|
@@ -1,19 +1,84 @@
|
|
| 1 |
<!doctype html>
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
</html>
|
|
|
|
| 1 |
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 6 |
+
<title>Reproduction: Memora: A Harmonic Memory Representation Balancing Abstraction and Specificity</title>
|
| 7 |
+
<link rel="stylesheet" href="./logbook.css" />
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div id="app">
|
| 11 |
+
<aside id="sidebar">
|
| 12 |
+
<div id="book-head">
|
| 13 |
+
<img id="book-wordmark" src="./trackio-wordmark-dark.png" alt="" />
|
| 14 |
+
<div id="book-title" class="sr-only">Logbook</div>
|
| 15 |
+
</div>
|
| 16 |
+
<nav id="tree"></nav>
|
| 17 |
+
<div id="sidebar-foot" hidden>
|
| 18 |
+
<button id="connect-btn" type="button">
|
| 19 |
+
<span class="ico">ⓘ</span> Collaborate with your agent
|
| 20 |
+
</button>
|
| 21 |
+
</div>
|
| 22 |
+
</aside>
|
| 23 |
+
<main id="content">
|
| 24 |
+
<nav id="view-tabs" aria-label="Logbook views">
|
| 25 |
+
<a data-view="code" href="#/view/code/index">
|
| 26 |
+
<svg viewBox="0 0 24 24" aria-hidden="true">
|
| 27 |
+
<path d="m18 16 4-4-4-4" />
|
| 28 |
+
<path d="m6 8-4 4 4 4" />
|
| 29 |
+
<path d="m14.5 4-5 16" />
|
| 30 |
+
</svg>
|
| 31 |
+
<span>Logbook</span>
|
| 32 |
+
</a>
|
| 33 |
+
<a data-view="trace" href="#/view/trace">
|
| 34 |
+
<svg viewBox="0 0 24 24" aria-hidden="true">
|
| 35 |
+
<path d="M8 5h13" />
|
| 36 |
+
<path d="M13 12h8" />
|
| 37 |
+
<path d="M13 19h8" />
|
| 38 |
+
<path d="M3 10a2 2 0 0 0 2 2h3" />
|
| 39 |
+
<path d="M3 5v12a2 2 0 0 0 2 2h3" />
|
| 40 |
+
</svg>
|
| 41 |
+
<span>Traces</span>
|
| 42 |
+
</a>
|
| 43 |
+
<a data-view="workspace" href="#/view/workspace">
|
| 44 |
+
<svg viewBox="0 0 24 24" aria-hidden="true">
|
| 45 |
+
<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" />
|
| 46 |
+
</svg>
|
| 47 |
+
<span>Workspace</span>
|
| 48 |
+
</a>
|
| 49 |
+
</nav>
|
| 50 |
+
<header id="logbook-header">
|
| 51 |
+
<h1 id="logbook-title"></h1>
|
| 52 |
+
<div id="logbook-cli"></div>
|
| 53 |
+
</header>
|
| 54 |
+
<div id="page"></div>
|
| 55 |
+
</main>
|
| 56 |
+
</div>
|
| 57 |
+
|
| 58 |
+
<div id="modal" hidden>
|
| 59 |
+
<div class="modal-backdrop"></div>
|
| 60 |
+
<div class="modal-card" role="dialog" aria-modal="true">
|
| 61 |
+
<div class="modal-head">
|
| 62 |
+
<div class="modal-title">
|
| 63 |
+
<img class="modal-logo" src="./trackio-logo.png" alt="" />
|
| 64 |
+
Collaborate with your agent
|
| 65 |
+
</div>
|
| 66 |
+
<div class="modal-actions">
|
| 67 |
+
<button id="copy-agent" class="btn">Copy for agent</button>
|
| 68 |
+
<button id="modal-close" class="btn icon" aria-label="Close">×</button>
|
| 69 |
+
</div>
|
| 70 |
+
</div>
|
| 71 |
+
<div class="modal-body">
|
| 72 |
+
<p class="modal-intro">
|
| 73 |
+
Point your coding agent at this logbook. It reads a compact,
|
| 74 |
+
token-efficient version — and if you've given it write access to this
|
| 75 |
+
Space, it can add findings that sync back automatically.
|
| 76 |
+
</p>
|
| 77 |
+
<ol id="connect-steps"></ol>
|
| 78 |
+
</div>
|
| 79 |
+
</div>
|
| 80 |
+
</div>
|
| 81 |
+
|
| 82 |
+
<script src="./logbook.js"></script>
|
| 83 |
+
</body>
|
| 84 |
</html>
|
logbook.css
ADDED
|
@@ -0,0 +1,2142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
--bg: #ffffff;
|
| 3 |
+
--paper: #ffffff;
|
| 4 |
+
--panel: #ffffff;
|
| 5 |
+
--ink: #1f2937;
|
| 6 |
+
--muted: #6b7280;
|
| 7 |
+
--line: #e5e7eb;
|
| 8 |
+
--accent: #f97316;
|
| 9 |
+
--accent-strong: #ea580c;
|
| 10 |
+
--accent-soft: #fff7ed;
|
| 11 |
+
--accent-line: rgba(249, 115, 22, 0.16);
|
| 12 |
+
--grid-line: rgba(31, 41, 55, 0.02);
|
| 13 |
+
--code-bg: #f3f4f6;
|
| 14 |
+
--radius: 12px;
|
| 15 |
+
--sidebar-width: 280px;
|
| 16 |
+
--content-gutter: 40px;
|
| 17 |
+
--serif: ui-serif, "Iowan Old Style", "Palatino Linotype", Georgia, serif;
|
| 18 |
+
--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial,
|
| 19 |
+
sans-serif;
|
| 20 |
+
--mono: "SFMono-Regular", "Cascadia Mono", "JetBrains Mono", Menlo, Consolas,
|
| 21 |
+
ui-monospace, monospace;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
* {
|
| 25 |
+
box-sizing: border-box;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
html,
|
| 29 |
+
body {
|
| 30 |
+
margin: 0;
|
| 31 |
+
padding: 0;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
html {
|
| 35 |
+
scroll-behavior: smooth;
|
| 36 |
+
scrollbar-gutter: stable;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
body {
|
| 40 |
+
background: var(--bg);
|
| 41 |
+
color: var(--ink);
|
| 42 |
+
font-family: var(--sans);
|
| 43 |
+
font-size: 13px;
|
| 44 |
+
line-height: 1.65;
|
| 45 |
+
-webkit-font-smoothing: antialiased;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
#app {
|
| 49 |
+
display: flex;
|
| 50 |
+
min-height: 100vh;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
body[data-view="trace"] #sidebar-foot,
|
| 54 |
+
body[data-view="workspace"] #sidebar-foot {
|
| 55 |
+
display: none;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
/* ---- sidebar (composition-book cover) ---- */
|
| 59 |
+
#sidebar {
|
| 60 |
+
width: var(--sidebar-width);
|
| 61 |
+
flex: 0 0 var(--sidebar-width);
|
| 62 |
+
background: #17181c;
|
| 63 |
+
color: #e7e7ea;
|
| 64 |
+
position: sticky;
|
| 65 |
+
top: 0;
|
| 66 |
+
height: 100vh;
|
| 67 |
+
overflow-y: auto;
|
| 68 |
+
padding: 22px 16px;
|
| 69 |
+
display: flex;
|
| 70 |
+
flex-direction: column;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
#book-head {
|
| 74 |
+
display: flex;
|
| 75 |
+
align-items: center;
|
| 76 |
+
gap: 10px;
|
| 77 |
+
padding: 8px;
|
| 78 |
+
margin-bottom: 12px;
|
| 79 |
+
border-radius: 10px;
|
| 80 |
+
cursor: pointer;
|
| 81 |
+
transition: background 0.12s;
|
| 82 |
+
}
|
| 83 |
+
#book-head:hover {
|
| 84 |
+
background: rgba(255, 255, 255, 0.05);
|
| 85 |
+
}
|
| 86 |
+
#book-wordmark {
|
| 87 |
+
width: 154px;
|
| 88 |
+
height: auto;
|
| 89 |
+
object-fit: contain;
|
| 90 |
+
}
|
| 91 |
+
.sr-only {
|
| 92 |
+
position: absolute;
|
| 93 |
+
width: 1px;
|
| 94 |
+
height: 1px;
|
| 95 |
+
padding: 0;
|
| 96 |
+
margin: -1px;
|
| 97 |
+
overflow: hidden;
|
| 98 |
+
clip: rect(0, 0, 0, 0);
|
| 99 |
+
white-space: nowrap;
|
| 100 |
+
border: 0;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
#tree {
|
| 104 |
+
flex: 1;
|
| 105 |
+
padding-top: 8px;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
#tree .tree-label {
|
| 109 |
+
padding: 6px 10px 8px;
|
| 110 |
+
color: #777a83;
|
| 111 |
+
font-size: 10px;
|
| 112 |
+
font-weight: 700;
|
| 113 |
+
letter-spacing: 0.12em;
|
| 114 |
+
text-transform: uppercase;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
#tree a {
|
| 118 |
+
display: block;
|
| 119 |
+
padding: 6px 10px;
|
| 120 |
+
border-radius: 8px;
|
| 121 |
+
color: #c3c4cb;
|
| 122 |
+
text-decoration: none;
|
| 123 |
+
font-size: 14px;
|
| 124 |
+
transition: background 0.12s, color 0.12s;
|
| 125 |
+
overflow: hidden;
|
| 126 |
+
text-overflow: ellipsis;
|
| 127 |
+
white-space: nowrap;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
#tree a:hover {
|
| 131 |
+
background: rgba(255, 255, 255, 0.06);
|
| 132 |
+
color: #ffffff;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
#tree a.active {
|
| 136 |
+
background: rgba(249, 115, 22, 0.16);
|
| 137 |
+
color: #fdba74;
|
| 138 |
+
font-weight: 600;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
#tree a .tree-mark {
|
| 142 |
+
color: #6b6d76;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
#tree a:hover .tree-mark,
|
| 146 |
+
#tree a.active .tree-mark {
|
| 147 |
+
color: inherit;
|
| 148 |
+
opacity: 0.6;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
#tree .depth-1 {
|
| 152 |
+
padding-left: 22px;
|
| 153 |
+
}
|
| 154 |
+
#tree .depth-2 {
|
| 155 |
+
padding-left: 34px;
|
| 156 |
+
}
|
| 157 |
+
#tree .depth-3 {
|
| 158 |
+
padding-left: 46px;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
/* ---- content ---- */
|
| 163 |
+
#content {
|
| 164 |
+
flex: 1;
|
| 165 |
+
min-width: 0;
|
| 166 |
+
padding: 24px
|
| 167 |
+
clamp(
|
| 168 |
+
var(--content-gutter),
|
| 169 |
+
calc(100vw - 960px),
|
| 170 |
+
calc(var(--sidebar-width) + var(--content-gutter))
|
| 171 |
+
)
|
| 172 |
+
120px var(--content-gutter);
|
| 173 |
+
background-color: var(--paper);
|
| 174 |
+
background-image:
|
| 175 |
+
linear-gradient(var(--grid-line) 1px, transparent 1px),
|
| 176 |
+
linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
|
| 177 |
+
background-size: 26px 26px;
|
| 178 |
+
background-position: center top;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
#logbook-header {
|
| 182 |
+
width: 100%;
|
| 183 |
+
max-width: 1080px;
|
| 184 |
+
margin: 0 auto 20px;
|
| 185 |
+
}
|
| 186 |
+
#logbook-title {
|
| 187 |
+
font-family: var(--serif);
|
| 188 |
+
font-size: 34px;
|
| 189 |
+
line-height: 1.15;
|
| 190 |
+
letter-spacing: -0.02em;
|
| 191 |
+
margin: 0 0 10px;
|
| 192 |
+
overflow-wrap: anywhere;
|
| 193 |
+
}
|
| 194 |
+
#logbook-cli {
|
| 195 |
+
display: grid;
|
| 196 |
+
gap: 7px;
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
#page {
|
| 200 |
+
width: 100%;
|
| 201 |
+
min-width: 0;
|
| 202 |
+
max-width: 1080px;
|
| 203 |
+
margin: 0 auto;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
.page-section {
|
| 207 |
+
scroll-margin-top: 40px;
|
| 208 |
+
padding: 0 0 35px;
|
| 209 |
+
margin: 0 0 32px;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
.page-section:last-child {
|
| 213 |
+
margin-bottom: 0;
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
.page-layout {
|
| 217 |
+
display: block;
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
.page-body {
|
| 221 |
+
min-width: 0;
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
.resource-anchor {
|
| 225 |
+
display: block;
|
| 226 |
+
height: 0;
|
| 227 |
+
overflow: hidden;
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
/* ---- pinned notes ---- */
|
| 231 |
+
.pinned-notes {
|
| 232 |
+
margin: 30px 0 32px;
|
| 233 |
+
}
|
| 234 |
+
.pinned-notes-list .cell {
|
| 235 |
+
margin: 0;
|
| 236 |
+
}
|
| 237 |
+
.pinned-notes-list .cell-title {
|
| 238 |
+
display: flex;
|
| 239 |
+
align-items: center;
|
| 240 |
+
gap: 7px;
|
| 241 |
+
}
|
| 242 |
+
.pin-ico {
|
| 243 |
+
flex: 0 0 auto;
|
| 244 |
+
width: 14px;
|
| 245 |
+
height: 14px;
|
| 246 |
+
fill: var(--accent);
|
| 247 |
+
stroke: none;
|
| 248 |
+
}
|
| 249 |
+
.pinned-notes-list .cell + .cell {
|
| 250 |
+
margin-top: 12px;
|
| 251 |
+
}
|
| 252 |
+
.book-intro.has-pinned-notes {
|
| 253 |
+
border-bottom: none;
|
| 254 |
+
padding-bottom: 22px;
|
| 255 |
+
margin-bottom: 30px;
|
| 256 |
+
}
|
| 257 |
+
.book-intro.book-intro-tight {
|
| 258 |
+
border-bottom: none;
|
| 259 |
+
padding-bottom: 4px;
|
| 260 |
+
margin-bottom: 20px;
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
#page h1 {
|
| 264 |
+
font-family: var(--serif);
|
| 265 |
+
font-size: 34px;
|
| 266 |
+
line-height: 1.15;
|
| 267 |
+
letter-spacing: -0.02em;
|
| 268 |
+
margin: 0 0 8px;
|
| 269 |
+
overflow-wrap: anywhere;
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
#page .page-section:not(.book-intro) h1 {
|
| 273 |
+
font-size: 26px;
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
#page h2 {
|
| 277 |
+
font-family: var(--serif);
|
| 278 |
+
font-size: 24px;
|
| 279 |
+
margin: 36px 0 10px;
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
#page h3 {
|
| 283 |
+
font-size: 17px;
|
| 284 |
+
font-weight: 700;
|
| 285 |
+
margin: 26px 0 2px;
|
| 286 |
+
letter-spacing: -0.01em;
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
#page h3::before {
|
| 290 |
+
content: "";
|
| 291 |
+
display: inline-block;
|
| 292 |
+
width: 7px;
|
| 293 |
+
height: 7px;
|
| 294 |
+
border-radius: 2px;
|
| 295 |
+
background: var(--accent);
|
| 296 |
+
margin-right: 10px;
|
| 297 |
+
vertical-align: middle;
|
| 298 |
+
transform: translateY(-1px);
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
#page p {
|
| 302 |
+
margin: 10px 0;
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
#page blockquote {
|
| 306 |
+
margin: 14px 0;
|
| 307 |
+
padding: 2px 16px;
|
| 308 |
+
border-left: 3px solid #fdba74;
|
| 309 |
+
color: var(--muted);
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
#page hr {
|
| 313 |
+
display: none;
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
#page code {
|
| 317 |
+
font-family: var(--mono);
|
| 318 |
+
font-size: 0.86em;
|
| 319 |
+
background: var(--code-bg);
|
| 320 |
+
padding: 2px 6px;
|
| 321 |
+
border-radius: 6px;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
#page pre {
|
| 325 |
+
max-width: 100%;
|
| 326 |
+
background: var(--code-bg);
|
| 327 |
+
border: 1px solid var(--line);
|
| 328 |
+
border-radius: var(--radius);
|
| 329 |
+
padding: 14px 16px;
|
| 330 |
+
overflow-x: auto;
|
| 331 |
+
}
|
| 332 |
+
#page pre code {
|
| 333 |
+
background: none;
|
| 334 |
+
padding: 0;
|
| 335 |
+
font-size: 11.5px;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
/* ---- code blocks + collapsible accordion ---- */
|
| 339 |
+
#page pre.hl {
|
| 340 |
+
background: #17181c;
|
| 341 |
+
border: none;
|
| 342 |
+
color: #e7e7ea;
|
| 343 |
+
font-size: 13px;
|
| 344 |
+
line-height: 1.58;
|
| 345 |
+
}
|
| 346 |
+
#page pre.hl code {
|
| 347 |
+
color: inherit;
|
| 348 |
+
font-family: var(--mono);
|
| 349 |
+
}
|
| 350 |
+
.code-accordion {
|
| 351 |
+
border: 1px solid rgba(249, 115, 22, 0.2);
|
| 352 |
+
border-radius: 8px;
|
| 353 |
+
overflow: hidden;
|
| 354 |
+
margin: 12px 0;
|
| 355 |
+
background: #17181c;
|
| 356 |
+
}
|
| 357 |
+
.code-accordion summary {
|
| 358 |
+
list-style: none;
|
| 359 |
+
cursor: pointer;
|
| 360 |
+
display: flex;
|
| 361 |
+
align-items: center;
|
| 362 |
+
gap: 9px;
|
| 363 |
+
padding: 9px 12px;
|
| 364 |
+
font-family: var(--mono);
|
| 365 |
+
font-size: 11.5px;
|
| 366 |
+
font-weight: 700;
|
| 367 |
+
color: #e7e7ea;
|
| 368 |
+
background: #1e2027;
|
| 369 |
+
user-select: none;
|
| 370 |
+
overflow-wrap: anywhere;
|
| 371 |
+
}
|
| 372 |
+
.code-accordion summary::-webkit-details-marker {
|
| 373 |
+
display: none;
|
| 374 |
+
}
|
| 375 |
+
.code-accordion summary::after {
|
| 376 |
+
content: "▸";
|
| 377 |
+
margin-left: auto;
|
| 378 |
+
color: var(--accent);
|
| 379 |
+
transition: transform 0.12s;
|
| 380 |
+
transform: rotate(180deg);
|
| 381 |
+
}
|
| 382 |
+
.code-accordion[open] summary::after {
|
| 383 |
+
transform: rotate(90deg);
|
| 384 |
+
}
|
| 385 |
+
.code-accordion .code-ico {
|
| 386 |
+
color: var(--accent);
|
| 387 |
+
font-weight: 700;
|
| 388 |
+
}
|
| 389 |
+
.code-accordion pre.hl {
|
| 390 |
+
margin: 0;
|
| 391 |
+
border-radius: 0;
|
| 392 |
+
border: none;
|
| 393 |
+
border-top: 1px solid rgba(249, 115, 22, 0.16);
|
| 394 |
+
}
|
| 395 |
+
.tok-comment {
|
| 396 |
+
color: #7a7d87;
|
| 397 |
+
font-style: italic;
|
| 398 |
+
}
|
| 399 |
+
.tok-string {
|
| 400 |
+
color: #a5d6a7;
|
| 401 |
+
}
|
| 402 |
+
.tok-keyword {
|
| 403 |
+
color: #fdba74;
|
| 404 |
+
}
|
| 405 |
+
.tok-number {
|
| 406 |
+
color: #7fd0e0;
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
#page a {
|
| 410 |
+
color: var(--accent);
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
#page ul {
|
| 414 |
+
padding-left: 20px;
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
.ts {
|
| 418 |
+
font-family: var(--mono);
|
| 419 |
+
font-size: 12px;
|
| 420 |
+
color: var(--muted);
|
| 421 |
+
background: none;
|
| 422 |
+
padding: 0;
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
/* ---- notebook-style cells ---- */
|
| 426 |
+
.cell {
|
| 427 |
+
max-width: 100%;
|
| 428 |
+
margin: 0 0 32px;
|
| 429 |
+
background: none;
|
| 430 |
+
border: none;
|
| 431 |
+
border-radius: 0;
|
| 432 |
+
box-shadow: none;
|
| 433 |
+
overflow: visible;
|
| 434 |
+
}
|
| 435 |
+
.cell-head {
|
| 436 |
+
display: flex;
|
| 437 |
+
justify-content: space-between;
|
| 438 |
+
gap: 16px;
|
| 439 |
+
align-items: baseline;
|
| 440 |
+
padding: 0 0 5px;
|
| 441 |
+
background: none;
|
| 442 |
+
border-bottom: none;
|
| 443 |
+
}
|
| 444 |
+
.cell-head.no-title {
|
| 445 |
+
justify-content: flex-end;
|
| 446 |
+
padding: 0 0 3px;
|
| 447 |
+
}
|
| 448 |
+
.cell-title {
|
| 449 |
+
flex: 1;
|
| 450 |
+
min-width: 0;
|
| 451 |
+
font-size: 13px;
|
| 452 |
+
font-weight: 650;
|
| 453 |
+
color: var(--ink);
|
| 454 |
+
line-height: 1.35;
|
| 455 |
+
overflow-wrap: anywhere;
|
| 456 |
+
}
|
| 457 |
+
.cell-meta {
|
| 458 |
+
flex: 0 0 auto;
|
| 459 |
+
display: flex;
|
| 460 |
+
align-items: center;
|
| 461 |
+
gap: 10px;
|
| 462 |
+
font-family: var(--sans);
|
| 463 |
+
font-size: 13px;
|
| 464 |
+
color: var(--muted);
|
| 465 |
+
}
|
| 466 |
+
.cell-open {
|
| 467 |
+
flex: 0 0 auto;
|
| 468 |
+
font-family: var(--mono);
|
| 469 |
+
font-size: 12px;
|
| 470 |
+
color: var(--accent);
|
| 471 |
+
text-decoration: none;
|
| 472 |
+
}
|
| 473 |
+
.cell-open:hover {
|
| 474 |
+
color: var(--accent-strong);
|
| 475 |
+
}
|
| 476 |
+
.cell-body {
|
| 477 |
+
min-width: 0;
|
| 478 |
+
padding: 0;
|
| 479 |
+
}
|
| 480 |
+
.cell.dashboard .cell-body {
|
| 481 |
+
padding: 0;
|
| 482 |
+
}
|
| 483 |
+
#page .cell-body h1,
|
| 484 |
+
#page .cell-body h2 {
|
| 485 |
+
font-family: var(--sans);
|
| 486 |
+
font-size: 17px;
|
| 487 |
+
font-weight: 700;
|
| 488 |
+
letter-spacing: -0.01em;
|
| 489 |
+
line-height: 1.35;
|
| 490 |
+
margin: 22px 0 6px;
|
| 491 |
+
}
|
| 492 |
+
#page .cell-body > :first-child {
|
| 493 |
+
margin-top: 0;
|
| 494 |
+
}
|
| 495 |
+
#page .cell-body > :last-child {
|
| 496 |
+
margin-bottom: 0;
|
| 497 |
+
}
|
| 498 |
+
.figure-fit {
|
| 499 |
+
position: relative;
|
| 500 |
+
overflow: hidden;
|
| 501 |
+
min-height: 160px;
|
| 502 |
+
border: 1px solid var(--line);
|
| 503 |
+
border-radius: 8px;
|
| 504 |
+
background: #fff;
|
| 505 |
+
}
|
| 506 |
+
.figure-fit[hidden] {
|
| 507 |
+
display: none;
|
| 508 |
+
}
|
| 509 |
+
.figure-fit:fullscreen,
|
| 510 |
+
.figure-fit:-webkit-full-screen {
|
| 511 |
+
width: 100%;
|
| 512 |
+
height: 100%;
|
| 513 |
+
border: none;
|
| 514 |
+
border-radius: 0;
|
| 515 |
+
}
|
| 516 |
+
.figure-frame {
|
| 517 |
+
display: block;
|
| 518 |
+
width: 100%;
|
| 519 |
+
min-height: 160px;
|
| 520 |
+
border: none;
|
| 521 |
+
background: #fff;
|
| 522 |
+
}
|
| 523 |
+
.figure-frame[hidden],
|
| 524 |
+
.figure-raw[hidden] {
|
| 525 |
+
display: none;
|
| 526 |
+
}
|
| 527 |
+
.fig-switch {
|
| 528 |
+
position: relative;
|
| 529 |
+
display: inline-flex;
|
| 530 |
+
flex: 0 0 auto;
|
| 531 |
+
border: 1px solid var(--line);
|
| 532 |
+
border-radius: 999px;
|
| 533 |
+
background: var(--code-bg);
|
| 534 |
+
padding: 2px;
|
| 535 |
+
}
|
| 536 |
+
.fig-switch button {
|
| 537 |
+
position: relative;
|
| 538 |
+
z-index: 1;
|
| 539 |
+
flex: 1;
|
| 540 |
+
min-width: 62px;
|
| 541 |
+
border: none;
|
| 542 |
+
background: none;
|
| 543 |
+
font-family: var(--sans);
|
| 544 |
+
font-size: 12px;
|
| 545 |
+
font-weight: 600;
|
| 546 |
+
color: var(--muted);
|
| 547 |
+
padding: 3px 12px;
|
| 548 |
+
border-radius: 999px;
|
| 549 |
+
cursor: pointer;
|
| 550 |
+
transition: color 0.15s;
|
| 551 |
+
}
|
| 552 |
+
.fig-switch button.active {
|
| 553 |
+
color: var(--accent-strong);
|
| 554 |
+
}
|
| 555 |
+
.fig-switch-thumb {
|
| 556 |
+
position: absolute;
|
| 557 |
+
top: 2px;
|
| 558 |
+
bottom: 2px;
|
| 559 |
+
left: 2px;
|
| 560 |
+
width: calc(50% - 2px);
|
| 561 |
+
border-radius: 999px;
|
| 562 |
+
background: var(--panel);
|
| 563 |
+
border: 1px solid rgba(249, 115, 22, 0.35);
|
| 564 |
+
box-shadow: 0 1px 4px rgba(31, 41, 55, 0.08);
|
| 565 |
+
transition: transform 0.18s ease;
|
| 566 |
+
}
|
| 567 |
+
.fig-switch.raw .fig-switch-thumb {
|
| 568 |
+
transform: translateX(100%);
|
| 569 |
+
}
|
| 570 |
+
#page .figure-raw pre {
|
| 571 |
+
margin: 0;
|
| 572 |
+
max-height: 420px;
|
| 573 |
+
overflow: auto;
|
| 574 |
+
font-family: var(--mono);
|
| 575 |
+
font-size: 13px;
|
| 576 |
+
line-height: 1.55;
|
| 577 |
+
background: var(--code-bg);
|
| 578 |
+
border: 1px solid var(--line);
|
| 579 |
+
border-radius: 8px;
|
| 580 |
+
padding: 12px 14px;
|
| 581 |
+
}
|
| 582 |
+
/* ---- figure fullscreen ---- */
|
| 583 |
+
.cell-fullscreen {
|
| 584 |
+
position: relative;
|
| 585 |
+
display: inline-flex;
|
| 586 |
+
flex: 0 0 auto;
|
| 587 |
+
}
|
| 588 |
+
.cell-fullscreen-btn {
|
| 589 |
+
display: inline-flex;
|
| 590 |
+
align-items: center;
|
| 591 |
+
justify-content: center;
|
| 592 |
+
width: 26px;
|
| 593 |
+
height: 26px;
|
| 594 |
+
padding: 0;
|
| 595 |
+
border: 1px solid var(--line);
|
| 596 |
+
border-radius: 999px;
|
| 597 |
+
background: var(--code-bg);
|
| 598 |
+
color: var(--muted);
|
| 599 |
+
cursor: pointer;
|
| 600 |
+
transition: color 0.15s, border-color 0.15s, background 0.15s;
|
| 601 |
+
}
|
| 602 |
+
.cell-fullscreen-btn:hover {
|
| 603 |
+
color: var(--accent-strong);
|
| 604 |
+
border-color: rgba(249, 115, 22, 0.35);
|
| 605 |
+
background: var(--accent-soft);
|
| 606 |
+
}
|
| 607 |
+
.cell-fullscreen-btn svg {
|
| 608 |
+
width: 14px;
|
| 609 |
+
height: 14px;
|
| 610 |
+
}
|
| 611 |
+
/* ---- copyable snippets ---- */
|
| 612 |
+
.snippet {
|
| 613 |
+
position: relative;
|
| 614 |
+
}
|
| 615 |
+
.copy-snippet {
|
| 616 |
+
position: absolute;
|
| 617 |
+
top: 7px;
|
| 618 |
+
right: 8px;
|
| 619 |
+
width: 24px;
|
| 620 |
+
height: 24px;
|
| 621 |
+
border: none;
|
| 622 |
+
border-radius: 6px;
|
| 623 |
+
background: rgba(255, 255, 255, 0.08);
|
| 624 |
+
color: #9a9da8;
|
| 625 |
+
font-size: 12px;
|
| 626 |
+
line-height: 1;
|
| 627 |
+
cursor: pointer;
|
| 628 |
+
opacity: 0;
|
| 629 |
+
transition: opacity 0.12s, color 0.12s, background 0.12s;
|
| 630 |
+
}
|
| 631 |
+
.snippet:hover .copy-snippet,
|
| 632 |
+
.jp-out:hover .copy-snippet,
|
| 633 |
+
.figure-raw:hover .copy-snippet,
|
| 634 |
+
.code-accordion summary:hover .copy-snippet {
|
| 635 |
+
opacity: 1;
|
| 636 |
+
}
|
| 637 |
+
.copy-snippet:hover {
|
| 638 |
+
color: #ffffff;
|
| 639 |
+
background: rgba(255, 255, 255, 0.16);
|
| 640 |
+
}
|
| 641 |
+
.copy-snippet.copied {
|
| 642 |
+
color: #52d08a;
|
| 643 |
+
opacity: 1;
|
| 644 |
+
}
|
| 645 |
+
.code-accordion .code-name {
|
| 646 |
+
user-select: text;
|
| 647 |
+
cursor: text;
|
| 648 |
+
}
|
| 649 |
+
.jp-out,
|
| 650 |
+
.figure-raw {
|
| 651 |
+
position: relative;
|
| 652 |
+
}
|
| 653 |
+
.jp-out .copy-snippet,
|
| 654 |
+
.figure-raw .copy-snippet {
|
| 655 |
+
background: var(--code-bg);
|
| 656 |
+
color: var(--muted);
|
| 657 |
+
border: 1px solid var(--line);
|
| 658 |
+
}
|
| 659 |
+
.jp-out .copy-snippet:hover,
|
| 660 |
+
.figure-raw .copy-snippet:hover {
|
| 661 |
+
color: var(--accent-strong);
|
| 662 |
+
background: var(--panel);
|
| 663 |
+
}
|
| 664 |
+
|
| 665 |
+
/* ---- jupyter-style code cells ---- */
|
| 666 |
+
.jp {
|
| 667 |
+
border: 1px solid var(--line);
|
| 668 |
+
border-radius: 10px;
|
| 669 |
+
overflow: hidden;
|
| 670 |
+
margin: 0;
|
| 671 |
+
background: var(--panel);
|
| 672 |
+
}
|
| 673 |
+
.jp-cmd {
|
| 674 |
+
display: flex;
|
| 675 |
+
align-items: baseline;
|
| 676 |
+
gap: 9px;
|
| 677 |
+
position: relative;
|
| 678 |
+
padding: 10px 16px 10px 0;
|
| 679 |
+
font-family: var(--mono);
|
| 680 |
+
font-size: 12px;
|
| 681 |
+
color: #8b8e98;
|
| 682 |
+
}
|
| 683 |
+
.jp-cmd-prompt {
|
| 684 |
+
color: var(--accent);
|
| 685 |
+
font-weight: 700;
|
| 686 |
+
}
|
| 687 |
+
#page .jp-cmd code {
|
| 688 |
+
min-width: 0;
|
| 689 |
+
color: #b6b9c2;
|
| 690 |
+
font-family: var(--mono);
|
| 691 |
+
font-size: 12px;
|
| 692 |
+
background: none;
|
| 693 |
+
padding: 0;
|
| 694 |
+
border-radius: 0;
|
| 695 |
+
overflow-wrap: anywhere;
|
| 696 |
+
}
|
| 697 |
+
.jp-cmd:hover .copy-snippet {
|
| 698 |
+
opacity: 1;
|
| 699 |
+
}
|
| 700 |
+
.jp-in-body .jp-cmd + .code-accordion,
|
| 701 |
+
.jp-in-body .jp-cmd + .snippet {
|
| 702 |
+
border-top: 1px solid rgba(255, 255, 255, 0.09);
|
| 703 |
+
}
|
| 704 |
+
.jp-gutter {
|
| 705 |
+
flex: 0 0 46px;
|
| 706 |
+
padding: 13px 0 0 13px;
|
| 707 |
+
font-family: var(--mono);
|
| 708 |
+
font-size: 10.5px;
|
| 709 |
+
letter-spacing: 0.07em;
|
| 710 |
+
text-transform: uppercase;
|
| 711 |
+
font-weight: 600;
|
| 712 |
+
user-select: none;
|
| 713 |
+
}
|
| 714 |
+
.jp-in {
|
| 715 |
+
display: flex;
|
| 716 |
+
background: #17181c;
|
| 717 |
+
}
|
| 718 |
+
.jp-in .jp-gutter {
|
| 719 |
+
color: #6f727d;
|
| 720 |
+
}
|
| 721 |
+
.jp-in-body {
|
| 722 |
+
flex: 1;
|
| 723 |
+
min-width: 0;
|
| 724 |
+
}
|
| 725 |
+
#page .jp-in-body pre.hl {
|
| 726 |
+
margin: 0;
|
| 727 |
+
border: none;
|
| 728 |
+
border-radius: 0;
|
| 729 |
+
background: none;
|
| 730 |
+
padding: 12px 16px 12px 0;
|
| 731 |
+
overflow-y: auto;
|
| 732 |
+
max-height: 26em;
|
| 733 |
+
}
|
| 734 |
+
.jp-in-body .code-accordion {
|
| 735 |
+
margin: 0;
|
| 736 |
+
border: none;
|
| 737 |
+
border-top: 1px solid rgba(255, 255, 255, 0.09);
|
| 738 |
+
border-radius: 0;
|
| 739 |
+
background: none;
|
| 740 |
+
}
|
| 741 |
+
.jp-in-body .code-accordion summary {
|
| 742 |
+
background: none;
|
| 743 |
+
padding: 9px 16px 9px 0;
|
| 744 |
+
}
|
| 745 |
+
.jp-in-body .code-accordion pre.hl {
|
| 746 |
+
border-top: 1px solid rgba(255, 255, 255, 0.09);
|
| 747 |
+
}
|
| 748 |
+
.jp-meta {
|
| 749 |
+
padding: 5px 14px;
|
| 750 |
+
font-family: var(--mono);
|
| 751 |
+
font-size: 11.5px;
|
| 752 |
+
color: var(--muted);
|
| 753 |
+
background: #fbfbfc;
|
| 754 |
+
border-top: 1px solid var(--line);
|
| 755 |
+
}
|
| 756 |
+
.jp-out {
|
| 757 |
+
display: flex;
|
| 758 |
+
border-top: 1px solid var(--line);
|
| 759 |
+
background: var(--panel);
|
| 760 |
+
}
|
| 761 |
+
.jp-out .jp-gutter {
|
| 762 |
+
color: var(--accent-strong);
|
| 763 |
+
}
|
| 764 |
+
.jp-out-body {
|
| 765 |
+
flex: 1;
|
| 766 |
+
min-width: 0;
|
| 767 |
+
}
|
| 768 |
+
#page .jp-out-pre {
|
| 769 |
+
min-width: 0;
|
| 770 |
+
margin: 0;
|
| 771 |
+
border: none;
|
| 772 |
+
border-radius: 0;
|
| 773 |
+
background: none;
|
| 774 |
+
color: var(--ink);
|
| 775 |
+
font-family: var(--mono);
|
| 776 |
+
font-size: 13px;
|
| 777 |
+
line-height: 1.55;
|
| 778 |
+
padding: 12px 16px 12px 0;
|
| 779 |
+
white-space: pre;
|
| 780 |
+
overflow-x: auto;
|
| 781 |
+
overflow-y: auto;
|
| 782 |
+
max-height: 26em;
|
| 783 |
+
}
|
| 784 |
+
.jp-artifacts {
|
| 785 |
+
display: flex;
|
| 786 |
+
flex-direction: column;
|
| 787 |
+
}
|
| 788 |
+
.jp-out-body .jp-out-pre + .jp-artifacts {
|
| 789 |
+
border-top: 1px solid var(--line);
|
| 790 |
+
}
|
| 791 |
+
.out-artifact {
|
| 792 |
+
display: flex;
|
| 793 |
+
align-items: baseline;
|
| 794 |
+
gap: 8px;
|
| 795 |
+
padding: 9px 16px 9px 0;
|
| 796 |
+
text-decoration: none;
|
| 797 |
+
color: inherit;
|
| 798 |
+
}
|
| 799 |
+
.out-artifact + .out-artifact {
|
| 800 |
+
border-top: 1px solid var(--line);
|
| 801 |
+
}
|
| 802 |
+
a.out-artifact:hover .out-artifact-name {
|
| 803 |
+
color: var(--accent-strong);
|
| 804 |
+
}
|
| 805 |
+
.out-artifact-ico {
|
| 806 |
+
flex: 0 0 auto;
|
| 807 |
+
font-size: 13px;
|
| 808 |
+
}
|
| 809 |
+
.out-artifact-name {
|
| 810 |
+
font-family: var(--mono);
|
| 811 |
+
font-size: 12.5px;
|
| 812 |
+
font-weight: 600;
|
| 813 |
+
color: var(--ink);
|
| 814 |
+
overflow: hidden;
|
| 815 |
+
text-overflow: ellipsis;
|
| 816 |
+
white-space: nowrap;
|
| 817 |
+
}
|
| 818 |
+
.out-artifact-meta {
|
| 819 |
+
flex: 0 0 auto;
|
| 820 |
+
margin-left: auto;
|
| 821 |
+
padding-left: 12px;
|
| 822 |
+
font-size: 12px;
|
| 823 |
+
color: var(--muted);
|
| 824 |
+
white-space: nowrap;
|
| 825 |
+
}
|
| 826 |
+
.out-artifact-state.open {
|
| 827 |
+
color: var(--accent);
|
| 828 |
+
font-weight: 600;
|
| 829 |
+
}
|
| 830 |
+
.trackio-embed {
|
| 831 |
+
border: 1px solid var(--line);
|
| 832 |
+
border-radius: var(--radius);
|
| 833 |
+
overflow: hidden;
|
| 834 |
+
background: var(--panel);
|
| 835 |
+
}
|
| 836 |
+
.trackio-cell-meta {
|
| 837 |
+
display: flex;
|
| 838 |
+
gap: 6px;
|
| 839 |
+
flex-wrap: wrap;
|
| 840 |
+
justify-content: flex-end;
|
| 841 |
+
}
|
| 842 |
+
|
| 843 |
+
/* ---- unfurl cards ---- */
|
| 844 |
+
.unfurl {
|
| 845 |
+
display: block;
|
| 846 |
+
border: 1px solid var(--line);
|
| 847 |
+
border-radius: var(--radius);
|
| 848 |
+
background: var(--panel);
|
| 849 |
+
margin: 12px 0;
|
| 850 |
+
overflow: hidden;
|
| 851 |
+
text-decoration: none;
|
| 852 |
+
color: inherit;
|
| 853 |
+
transition: border-color 0.14s, box-shadow 0.14s;
|
| 854 |
+
}
|
| 855 |
+
.unfurl:hover {
|
| 856 |
+
border-color: #cfcbe6;
|
| 857 |
+
box-shadow: 0 4px 18px rgba(30, 20, 80, 0.06);
|
| 858 |
+
}
|
| 859 |
+
|
| 860 |
+
.unfurl-body {
|
| 861 |
+
padding: 13px 16px;
|
| 862 |
+
display: flex;
|
| 863 |
+
gap: 12px;
|
| 864 |
+
align-items: flex-start;
|
| 865 |
+
}
|
| 866 |
+
|
| 867 |
+
.unfurl-ico {
|
| 868 |
+
font-size: 20px;
|
| 869 |
+
line-height: 1.3;
|
| 870 |
+
flex: 0 0 auto;
|
| 871 |
+
}
|
| 872 |
+
|
| 873 |
+
.unfurl-main {
|
| 874 |
+
min-width: 0;
|
| 875 |
+
flex: 1;
|
| 876 |
+
}
|
| 877 |
+
|
| 878 |
+
.unfurl-kind {
|
| 879 |
+
font-family: var(--mono);
|
| 880 |
+
font-size: 10.5px;
|
| 881 |
+
text-transform: uppercase;
|
| 882 |
+
letter-spacing: 0.08em;
|
| 883 |
+
color: var(--accent);
|
| 884 |
+
font-weight: 600;
|
| 885 |
+
}
|
| 886 |
+
|
| 887 |
+
.unfurl-title {
|
| 888 |
+
font-weight: 650;
|
| 889 |
+
font-size: 15px;
|
| 890 |
+
margin: 1px 0 2px;
|
| 891 |
+
white-space: nowrap;
|
| 892 |
+
overflow: hidden;
|
| 893 |
+
text-overflow: ellipsis;
|
| 894 |
+
}
|
| 895 |
+
|
| 896 |
+
.unfurl-desc {
|
| 897 |
+
color: var(--muted);
|
| 898 |
+
font-size: 13.5px;
|
| 899 |
+
line-height: 1.45;
|
| 900 |
+
}
|
| 901 |
+
|
| 902 |
+
.unfurl-meta {
|
| 903 |
+
margin-top: 6px;
|
| 904 |
+
display: flex;
|
| 905 |
+
flex-wrap: wrap;
|
| 906 |
+
gap: 6px;
|
| 907 |
+
}
|
| 908 |
+
|
| 909 |
+
.chip {
|
| 910 |
+
font-size: 11.5px;
|
| 911 |
+
background: var(--code-bg);
|
| 912 |
+
border-radius: 999px;
|
| 913 |
+
padding: 2px 9px;
|
| 914 |
+
color: var(--muted);
|
| 915 |
+
font-family: var(--mono);
|
| 916 |
+
}
|
| 917 |
+
|
| 918 |
+
.unfurl-raw {
|
| 919 |
+
font-family: var(--mono);
|
| 920 |
+
font-size: 11px;
|
| 921 |
+
color: var(--muted);
|
| 922 |
+
border-top: 1px solid var(--line);
|
| 923 |
+
padding: 7px 16px;
|
| 924 |
+
white-space: nowrap;
|
| 925 |
+
overflow: hidden;
|
| 926 |
+
text-overflow: ellipsis;
|
| 927 |
+
}
|
| 928 |
+
|
| 929 |
+
.unfurl.embed {
|
| 930 |
+
padding: 0;
|
| 931 |
+
overflow: hidden;
|
| 932 |
+
}
|
| 933 |
+
.embed-head {
|
| 934 |
+
display: flex;
|
| 935 |
+
align-items: center;
|
| 936 |
+
gap: 10px;
|
| 937 |
+
padding: 10px 14px;
|
| 938 |
+
border-bottom: 1px solid var(--line);
|
| 939 |
+
}
|
| 940 |
+
.embed-head .unfurl-kind {
|
| 941 |
+
flex: 0 0 auto;
|
| 942 |
+
}
|
| 943 |
+
.embed-title {
|
| 944 |
+
flex: 1;
|
| 945 |
+
min-width: 0;
|
| 946 |
+
font-weight: 650;
|
| 947 |
+
font-size: 14px;
|
| 948 |
+
color: var(--ink);
|
| 949 |
+
text-decoration: none;
|
| 950 |
+
white-space: nowrap;
|
| 951 |
+
overflow: hidden;
|
| 952 |
+
text-overflow: ellipsis;
|
| 953 |
+
}
|
| 954 |
+
.embed-title:hover {
|
| 955 |
+
color: var(--accent);
|
| 956 |
+
}
|
| 957 |
+
.embed-open {
|
| 958 |
+
flex: 0 0 auto;
|
| 959 |
+
font-family: var(--mono);
|
| 960 |
+
font-size: 12px;
|
| 961 |
+
color: var(--accent);
|
| 962 |
+
text-decoration: none;
|
| 963 |
+
}
|
| 964 |
+
.embed-frame {
|
| 965 |
+
display: block;
|
| 966 |
+
width: 100%;
|
| 967 |
+
height: 560px;
|
| 968 |
+
border: 0;
|
| 969 |
+
background: var(--code-bg);
|
| 970 |
+
}
|
| 971 |
+
|
| 972 |
+
.dashboard-shell {
|
| 973 |
+
display: block;
|
| 974 |
+
}
|
| 975 |
+
.dashboard-shell .dashboard-frame {
|
| 976 |
+
display: block;
|
| 977 |
+
width: 100%;
|
| 978 |
+
height: 900px;
|
| 979 |
+
border: 0;
|
| 980 |
+
background: var(--code-bg);
|
| 981 |
+
}
|
| 982 |
+
|
| 983 |
+
.unfurl.image {
|
| 984 |
+
padding: 0;
|
| 985 |
+
}
|
| 986 |
+
.unfurl.image img {
|
| 987 |
+
display: block;
|
| 988 |
+
width: 100%;
|
| 989 |
+
height: auto;
|
| 990 |
+
max-height: 460px;
|
| 991 |
+
object-fit: contain;
|
| 992 |
+
background: var(--code-bg);
|
| 993 |
+
}
|
| 994 |
+
|
| 995 |
+
.artifact-chip {
|
| 996 |
+
border: 1px solid var(--line);
|
| 997 |
+
background: var(--panel);
|
| 998 |
+
border-radius: var(--radius);
|
| 999 |
+
padding: 10px 14px;
|
| 1000 |
+
margin: 8px 0;
|
| 1001 |
+
font-size: 14px;
|
| 1002 |
+
}
|
| 1003 |
+
.cell.dashboard .artifact-chip {
|
| 1004 |
+
margin: 14px 18px 18px;
|
| 1005 |
+
}
|
| 1006 |
+
.artifact-chip code {
|
| 1007 |
+
color: var(--accent);
|
| 1008 |
+
}
|
| 1009 |
+
|
| 1010 |
+
/* ---- task board ---- */
|
| 1011 |
+
.board-wrap {
|
| 1012 |
+
overflow-x: auto;
|
| 1013 |
+
border: 1px solid var(--line);
|
| 1014 |
+
border-radius: var(--radius);
|
| 1015 |
+
margin: 12px 0 20px;
|
| 1016 |
+
background: var(--panel);
|
| 1017 |
+
}
|
| 1018 |
+
table.board {
|
| 1019 |
+
border-collapse: collapse;
|
| 1020 |
+
width: 100%;
|
| 1021 |
+
font-size: 14px;
|
| 1022 |
+
}
|
| 1023 |
+
table.board th,
|
| 1024 |
+
table.board td {
|
| 1025 |
+
text-align: left;
|
| 1026 |
+
padding: 9px 14px;
|
| 1027 |
+
border-bottom: 1px solid var(--line);
|
| 1028 |
+
vertical-align: top;
|
| 1029 |
+
}
|
| 1030 |
+
table.board thead th {
|
| 1031 |
+
background: var(--accent-soft);
|
| 1032 |
+
font-size: 12px;
|
| 1033 |
+
text-transform: uppercase;
|
| 1034 |
+
letter-spacing: 0.05em;
|
| 1035 |
+
color: #9a4a12;
|
| 1036 |
+
font-weight: 600;
|
| 1037 |
+
border-bottom: 1px solid var(--line);
|
| 1038 |
+
}
|
| 1039 |
+
table.board tbody tr:last-child td {
|
| 1040 |
+
border-bottom: none;
|
| 1041 |
+
}
|
| 1042 |
+
table.board .col-check {
|
| 1043 |
+
text-align: center;
|
| 1044 |
+
width: 92px;
|
| 1045 |
+
white-space: nowrap;
|
| 1046 |
+
}
|
| 1047 |
+
table.board tr.section-row td {
|
| 1048 |
+
background: var(--accent-soft);
|
| 1049 |
+
text-align: center;
|
| 1050 |
+
font-weight: 700;
|
| 1051 |
+
font-size: 13px;
|
| 1052 |
+
color: var(--accent-strong);
|
| 1053 |
+
padding: 7px 14px;
|
| 1054 |
+
letter-spacing: 0.02em;
|
| 1055 |
+
}
|
| 1056 |
+
.box {
|
| 1057 |
+
display: inline-flex;
|
| 1058 |
+
align-items: center;
|
| 1059 |
+
justify-content: center;
|
| 1060 |
+
width: 18px;
|
| 1061 |
+
height: 18px;
|
| 1062 |
+
border: 1.5px solid #cfcbe0;
|
| 1063 |
+
border-radius: 5px;
|
| 1064 |
+
font-size: 12px;
|
| 1065 |
+
color: #fff;
|
| 1066 |
+
line-height: 1;
|
| 1067 |
+
}
|
| 1068 |
+
.box.on {
|
| 1069 |
+
background: var(--accent);
|
| 1070 |
+
border-color: var(--accent);
|
| 1071 |
+
}
|
| 1072 |
+
.who-chip {
|
| 1073 |
+
display: inline-block;
|
| 1074 |
+
padding: 3px 12px;
|
| 1075 |
+
border-radius: 999px;
|
| 1076 |
+
font-size: 12.5px;
|
| 1077 |
+
font-weight: 600;
|
| 1078 |
+
white-space: nowrap;
|
| 1079 |
+
}
|
| 1080 |
+
.who-chip.muted {
|
| 1081 |
+
background: var(--code-bg);
|
| 1082 |
+
color: var(--muted);
|
| 1083 |
+
font-weight: 500;
|
| 1084 |
+
}
|
| 1085 |
+
|
| 1086 |
+
/* ---- status badges + clickable rows ---- */
|
| 1087 |
+
table.board .col-status {
|
| 1088 |
+
width: 130px;
|
| 1089 |
+
white-space: nowrap;
|
| 1090 |
+
}
|
| 1091 |
+
.badge {
|
| 1092 |
+
display: inline-block;
|
| 1093 |
+
padding: 3px 11px;
|
| 1094 |
+
border-radius: 999px;
|
| 1095 |
+
font-size: 12px;
|
| 1096 |
+
font-weight: 600;
|
| 1097 |
+
letter-spacing: 0.01em;
|
| 1098 |
+
}
|
| 1099 |
+
.badge.gray {
|
| 1100 |
+
background: var(--code-bg);
|
| 1101 |
+
color: var(--muted);
|
| 1102 |
+
}
|
| 1103 |
+
.badge.amber {
|
| 1104 |
+
background: var(--accent-soft);
|
| 1105 |
+
color: #b45309;
|
| 1106 |
+
}
|
| 1107 |
+
.badge.green {
|
| 1108 |
+
background: #e6f7ee;
|
| 1109 |
+
color: #1a8a55;
|
| 1110 |
+
}
|
| 1111 |
+
.badge.red {
|
| 1112 |
+
background: #fde8ec;
|
| 1113 |
+
color: #c62a4b;
|
| 1114 |
+
}
|
| 1115 |
+
table.board tr.linked-row {
|
| 1116 |
+
cursor: pointer;
|
| 1117 |
+
}
|
| 1118 |
+
table.board tr.linked-row:hover td {
|
| 1119 |
+
background: var(--accent-soft);
|
| 1120 |
+
}
|
| 1121 |
+
table.board tr.linked-row a {
|
| 1122 |
+
color: var(--ink);
|
| 1123 |
+
font-weight: 600;
|
| 1124 |
+
text-decoration: none;
|
| 1125 |
+
}
|
| 1126 |
+
table.board tr.linked-row:hover a {
|
| 1127 |
+
color: var(--accent-strong);
|
| 1128 |
+
}
|
| 1129 |
+
|
| 1130 |
+
/* ---- agent read hint ---- */
|
| 1131 |
+
.agent-hint {
|
| 1132 |
+
display: flex;
|
| 1133 |
+
align-items: center;
|
| 1134 |
+
flex-wrap: wrap;
|
| 1135 |
+
gap: 8px;
|
| 1136 |
+
margin: 0;
|
| 1137 |
+
font-size: 12.5px;
|
| 1138 |
+
color: var(--muted);
|
| 1139 |
+
}
|
| 1140 |
+
.agent-hint code {
|
| 1141 |
+
flex: 1 1 18rem;
|
| 1142 |
+
min-width: 0;
|
| 1143 |
+
background: var(--code-bg);
|
| 1144 |
+
padding: 2px 9px;
|
| 1145 |
+
border-radius: 6px;
|
| 1146 |
+
font-family: var(--mono);
|
| 1147 |
+
font-size: 12px;
|
| 1148 |
+
font-weight: 500;
|
| 1149 |
+
color: var(--ink);
|
| 1150 |
+
overflow: hidden;
|
| 1151 |
+
text-overflow: ellipsis;
|
| 1152 |
+
white-space: nowrap;
|
| 1153 |
+
}
|
| 1154 |
+
.agent-hint .copy {
|
| 1155 |
+
flex: 0 0 auto;
|
| 1156 |
+
background: none;
|
| 1157 |
+
color: var(--muted);
|
| 1158 |
+
border: 1px solid var(--line);
|
| 1159 |
+
border-radius: 6px;
|
| 1160 |
+
width: 22px;
|
| 1161 |
+
height: 22px;
|
| 1162 |
+
font-size: 11px;
|
| 1163 |
+
line-height: 1;
|
| 1164 |
+
cursor: pointer;
|
| 1165 |
+
transition: color 0.12s, border-color 0.12s;
|
| 1166 |
+
}
|
| 1167 |
+
.agent-hint .copy:hover {
|
| 1168 |
+
color: var(--accent-strong);
|
| 1169 |
+
border-color: var(--accent);
|
| 1170 |
+
}
|
| 1171 |
+
.agent-hint .copy.copied {
|
| 1172 |
+
color: #1a8a55;
|
| 1173 |
+
border-color: #1a8a55;
|
| 1174 |
+
}
|
| 1175 |
+
.agent-hint-note {
|
| 1176 |
+
margin-left: auto;
|
| 1177 |
+
font-size: 12px;
|
| 1178 |
+
color: var(--muted);
|
| 1179 |
+
}
|
| 1180 |
+
.hub-destination {
|
| 1181 |
+
display: flex;
|
| 1182 |
+
align-items: center;
|
| 1183 |
+
flex-wrap: wrap;
|
| 1184 |
+
gap: 8px;
|
| 1185 |
+
color: var(--muted);
|
| 1186 |
+
font-size: 12.5px;
|
| 1187 |
+
}
|
| 1188 |
+
.hub-destination a {
|
| 1189 |
+
display: inline-flex;
|
| 1190 |
+
align-items: center;
|
| 1191 |
+
gap: 6px;
|
| 1192 |
+
max-width: 100%;
|
| 1193 |
+
padding: 3px 9px;
|
| 1194 |
+
border: 1px solid var(--accent-line);
|
| 1195 |
+
border-radius: 999px;
|
| 1196 |
+
background: var(--accent-soft);
|
| 1197 |
+
color: var(--accent-strong);
|
| 1198 |
+
font-family: var(--mono);
|
| 1199 |
+
font-size: 12px;
|
| 1200 |
+
font-weight: 650;
|
| 1201 |
+
line-height: 1.5;
|
| 1202 |
+
text-decoration: none;
|
| 1203 |
+
overflow-wrap: anywhere;
|
| 1204 |
+
transition: border-color 0.12s, background 0.12s, color 0.12s;
|
| 1205 |
+
}
|
| 1206 |
+
.hub-destination a:hover {
|
| 1207 |
+
border-color: var(--accent);
|
| 1208 |
+
background: #ffedd5;
|
| 1209 |
+
color: #c2410c;
|
| 1210 |
+
}
|
| 1211 |
+
.hub-destination svg {
|
| 1212 |
+
width: 13px;
|
| 1213 |
+
height: 13px;
|
| 1214 |
+
flex: 0 0 auto;
|
| 1215 |
+
fill: none;
|
| 1216 |
+
stroke: currentColor;
|
| 1217 |
+
stroke-width: 1.8;
|
| 1218 |
+
stroke-linecap: round;
|
| 1219 |
+
stroke-linejoin: round;
|
| 1220 |
+
}
|
| 1221 |
+
|
| 1222 |
+
.index-paper-link {
|
| 1223 |
+
margin: 14px 0 30px;
|
| 1224 |
+
font-size: 19px;
|
| 1225 |
+
line-height: 1.35;
|
| 1226 |
+
font-weight: 700;
|
| 1227 |
+
}
|
| 1228 |
+
.index-paper-link a {
|
| 1229 |
+
text-underline-offset: 4px;
|
| 1230 |
+
text-decoration-thickness: 2px;
|
| 1231 |
+
}
|
| 1232 |
+
.art-ico {
|
| 1233 |
+
width: 1em;
|
| 1234 |
+
height: 1em;
|
| 1235 |
+
object-fit: contain;
|
| 1236 |
+
vertical-align: -0.15em;
|
| 1237 |
+
}
|
| 1238 |
+
.art-file-ico {
|
| 1239 |
+
width: 15px;
|
| 1240 |
+
height: 15px;
|
| 1241 |
+
flex: 0 0 auto;
|
| 1242 |
+
fill: none;
|
| 1243 |
+
stroke: currentColor;
|
| 1244 |
+
stroke-width: 1.7;
|
| 1245 |
+
stroke-linecap: round;
|
| 1246 |
+
stroke-linejoin: round;
|
| 1247 |
+
vertical-align: -0.2em;
|
| 1248 |
+
}
|
| 1249 |
+
.out-artifact-ico .art-file-ico {
|
| 1250 |
+
color: var(--muted);
|
| 1251 |
+
}
|
| 1252 |
+
|
| 1253 |
+
/* ---- scroll-to-resource highlight ---- */
|
| 1254 |
+
.res-flash {
|
| 1255 |
+
animation: res-flash 1.5s ease;
|
| 1256 |
+
border-radius: 8px;
|
| 1257 |
+
}
|
| 1258 |
+
@keyframes res-flash {
|
| 1259 |
+
0%,
|
| 1260 |
+
25% {
|
| 1261 |
+
box-shadow: 0 0 0 3px var(--accent);
|
| 1262 |
+
}
|
| 1263 |
+
100% {
|
| 1264 |
+
box-shadow: 0 0 0 3px rgba(249, 115, 22, 0);
|
| 1265 |
+
}
|
| 1266 |
+
}
|
| 1267 |
+
|
| 1268 |
+
/* ---- inline resource chips ---- */
|
| 1269 |
+
#page .res-chip {
|
| 1270 |
+
display: inline-flex;
|
| 1271 |
+
align-items: center;
|
| 1272 |
+
gap: 5px;
|
| 1273 |
+
max-width: 100%;
|
| 1274 |
+
padding: 0 9px 0 6px;
|
| 1275 |
+
margin: 0 1px;
|
| 1276 |
+
border: 1px solid var(--line);
|
| 1277 |
+
border-radius: 999px;
|
| 1278 |
+
background: var(--panel);
|
| 1279 |
+
font-family: var(--mono);
|
| 1280 |
+
font-size: 0.78em;
|
| 1281 |
+
font-weight: 600;
|
| 1282 |
+
color: var(--ink);
|
| 1283 |
+
text-decoration: none;
|
| 1284 |
+
white-space: nowrap;
|
| 1285 |
+
overflow: hidden;
|
| 1286 |
+
text-overflow: ellipsis;
|
| 1287 |
+
vertical-align: middle;
|
| 1288 |
+
line-height: 1.65;
|
| 1289 |
+
transform: translateY(-0.08em);
|
| 1290 |
+
transition: border-color 0.12s, background 0.12s, color 0.12s;
|
| 1291 |
+
}
|
| 1292 |
+
.res-chip-ico {
|
| 1293 |
+
font-size: 1.05em;
|
| 1294 |
+
line-height: 1;
|
| 1295 |
+
}
|
| 1296 |
+
#page .res-chip:hover {
|
| 1297 |
+
border-color: var(--accent);
|
| 1298 |
+
background: var(--accent-soft);
|
| 1299 |
+
color: var(--accent-strong);
|
| 1300 |
+
}
|
| 1301 |
+
|
| 1302 |
+
/* ---- connect footer + modal ---- */
|
| 1303 |
+
#sidebar-foot {
|
| 1304 |
+
margin-top: auto;
|
| 1305 |
+
padding-top: 14px;
|
| 1306 |
+
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
| 1307 |
+
}
|
| 1308 |
+
|
| 1309 |
+
#connect-btn {
|
| 1310 |
+
width: 100%;
|
| 1311 |
+
display: flex;
|
| 1312 |
+
align-items: center;
|
| 1313 |
+
gap: 8px;
|
| 1314 |
+
background: rgba(255, 255, 255, 0.05);
|
| 1315 |
+
color: #c3c4cb;
|
| 1316 |
+
border: 1px solid rgba(255, 255, 255, 0.12);
|
| 1317 |
+
border-radius: 9px;
|
| 1318 |
+
padding: 9px 12px;
|
| 1319 |
+
font-size: 13.5px;
|
| 1320 |
+
font-family: var(--sans);
|
| 1321 |
+
cursor: pointer;
|
| 1322 |
+
transition: background 0.12s, color 0.12s, border-color 0.12s;
|
| 1323 |
+
}
|
| 1324 |
+
#connect-btn:hover {
|
| 1325 |
+
background: rgba(249, 115, 22, 0.14);
|
| 1326 |
+
border-color: rgba(249, 115, 22, 0.4);
|
| 1327 |
+
color: #fdba74;
|
| 1328 |
+
}
|
| 1329 |
+
#connect-btn .ico {
|
| 1330 |
+
font-size: 15px;
|
| 1331 |
+
}
|
| 1332 |
+
|
| 1333 |
+
#modal[hidden] {
|
| 1334 |
+
display: none;
|
| 1335 |
+
}
|
| 1336 |
+
#modal {
|
| 1337 |
+
position: fixed;
|
| 1338 |
+
inset: 0;
|
| 1339 |
+
z-index: 100;
|
| 1340 |
+
display: flex;
|
| 1341 |
+
align-items: center;
|
| 1342 |
+
justify-content: center;
|
| 1343 |
+
padding: 24px;
|
| 1344 |
+
}
|
| 1345 |
+
.modal-backdrop {
|
| 1346 |
+
position: absolute;
|
| 1347 |
+
inset: 0;
|
| 1348 |
+
background: rgba(20, 18, 30, 0.5);
|
| 1349 |
+
backdrop-filter: blur(2px);
|
| 1350 |
+
}
|
| 1351 |
+
.modal-card {
|
| 1352 |
+
position: relative;
|
| 1353 |
+
background: var(--panel);
|
| 1354 |
+
border-radius: 16px;
|
| 1355 |
+
width: 100%;
|
| 1356 |
+
max-width: 620px;
|
| 1357 |
+
max-height: 85vh;
|
| 1358 |
+
overflow-y: auto;
|
| 1359 |
+
box-shadow: 0 24px 70px rgba(20, 15, 50, 0.28);
|
| 1360 |
+
}
|
| 1361 |
+
.modal-head {
|
| 1362 |
+
display: flex;
|
| 1363 |
+
align-items: center;
|
| 1364 |
+
justify-content: space-between;
|
| 1365 |
+
gap: 12px;
|
| 1366 |
+
padding: 18px 22px;
|
| 1367 |
+
border-bottom: 1px solid var(--line);
|
| 1368 |
+
position: sticky;
|
| 1369 |
+
top: 0;
|
| 1370 |
+
background: var(--panel);
|
| 1371 |
+
}
|
| 1372 |
+
.modal-title {
|
| 1373 |
+
display: flex;
|
| 1374 |
+
align-items: center;
|
| 1375 |
+
gap: 10px;
|
| 1376 |
+
font-family: var(--serif);
|
| 1377 |
+
font-size: 21px;
|
| 1378 |
+
letter-spacing: -0.01em;
|
| 1379 |
+
}
|
| 1380 |
+
.modal-logo {
|
| 1381 |
+
width: 26px;
|
| 1382 |
+
height: 26px;
|
| 1383 |
+
object-fit: contain;
|
| 1384 |
+
}
|
| 1385 |
+
.modal-actions {
|
| 1386 |
+
display: flex;
|
| 1387 |
+
align-items: center;
|
| 1388 |
+
gap: 8px;
|
| 1389 |
+
}
|
| 1390 |
+
.btn {
|
| 1391 |
+
font-family: var(--sans);
|
| 1392 |
+
font-size: 13.5px;
|
| 1393 |
+
font-weight: 600;
|
| 1394 |
+
border: 1px solid var(--line);
|
| 1395 |
+
background: var(--panel);
|
| 1396 |
+
color: var(--ink);
|
| 1397 |
+
border-radius: 9px;
|
| 1398 |
+
padding: 8px 13px;
|
| 1399 |
+
cursor: pointer;
|
| 1400 |
+
transition: background 0.12s, border-color 0.12s, color 0.12s;
|
| 1401 |
+
}
|
| 1402 |
+
.btn:hover {
|
| 1403 |
+
border-color: var(--accent);
|
| 1404 |
+
color: var(--accent-strong);
|
| 1405 |
+
}
|
| 1406 |
+
.btn.copied {
|
| 1407 |
+
border-color: #1a8a55;
|
| 1408 |
+
color: #1a8a55;
|
| 1409 |
+
}
|
| 1410 |
+
.btn.icon {
|
| 1411 |
+
font-size: 18px;
|
| 1412 |
+
line-height: 1;
|
| 1413 |
+
padding: 6px 11px;
|
| 1414 |
+
font-weight: 400;
|
| 1415 |
+
}
|
| 1416 |
+
.modal-body {
|
| 1417 |
+
padding: 20px 22px 26px;
|
| 1418 |
+
}
|
| 1419 |
+
.modal-intro {
|
| 1420 |
+
margin: 0 0 20px;
|
| 1421 |
+
color: var(--muted);
|
| 1422 |
+
line-height: 1.55;
|
| 1423 |
+
}
|
| 1424 |
+
#connect-steps {
|
| 1425 |
+
list-style: none;
|
| 1426 |
+
margin: 0;
|
| 1427 |
+
padding: 0;
|
| 1428 |
+
}
|
| 1429 |
+
#connect-steps li {
|
| 1430 |
+
margin-bottom: 18px;
|
| 1431 |
+
}
|
| 1432 |
+
.step-title {
|
| 1433 |
+
font-weight: 600;
|
| 1434 |
+
font-size: 14.5px;
|
| 1435 |
+
margin-bottom: 8px;
|
| 1436 |
+
}
|
| 1437 |
+
.codeblock {
|
| 1438 |
+
display: flex;
|
| 1439 |
+
align-items: center;
|
| 1440 |
+
gap: 8px;
|
| 1441 |
+
background: #17181c;
|
| 1442 |
+
border-radius: 10px;
|
| 1443 |
+
padding: 11px 12px 11px 15px;
|
| 1444 |
+
}
|
| 1445 |
+
.codeblock code {
|
| 1446 |
+
flex: 1;
|
| 1447 |
+
min-width: 0;
|
| 1448 |
+
overflow-x: auto;
|
| 1449 |
+
white-space: nowrap;
|
| 1450 |
+
font-family: var(--mono);
|
| 1451 |
+
font-size: 13px;
|
| 1452 |
+
color: #f0efff;
|
| 1453 |
+
background: none;
|
| 1454 |
+
padding: 0;
|
| 1455 |
+
}
|
| 1456 |
+
.codeblock .copy {
|
| 1457 |
+
flex: 0 0 auto;
|
| 1458 |
+
background: rgba(255, 255, 255, 0.08);
|
| 1459 |
+
color: #c3c4cb;
|
| 1460 |
+
border: 1px solid rgba(255, 255, 255, 0.14);
|
| 1461 |
+
border-radius: 7px;
|
| 1462 |
+
width: 30px;
|
| 1463 |
+
height: 30px;
|
| 1464 |
+
font-size: 14px;
|
| 1465 |
+
cursor: pointer;
|
| 1466 |
+
transition: background 0.12s, color 0.12s;
|
| 1467 |
+
}
|
| 1468 |
+
.codeblock .copy:hover {
|
| 1469 |
+
background: rgba(249, 115, 22, 0.2);
|
| 1470 |
+
color: #fdba74;
|
| 1471 |
+
}
|
| 1472 |
+
.codeblock .copy.copied {
|
| 1473 |
+
color: #52d08a;
|
| 1474 |
+
}
|
| 1475 |
+
|
| 1476 |
+
/* ---- top-level logbook views ---- */
|
| 1477 |
+
#view-tabs {
|
| 1478 |
+
position: sticky;
|
| 1479 |
+
top: 0;
|
| 1480 |
+
z-index: 30;
|
| 1481 |
+
width: 100%;
|
| 1482 |
+
max-width: 1080px;
|
| 1483 |
+
margin: 0 auto 24px;
|
| 1484 |
+
padding-top: 10px;
|
| 1485 |
+
display: flex;
|
| 1486 |
+
align-items: center;
|
| 1487 |
+
justify-content: flex-start;
|
| 1488 |
+
gap: 26px;
|
| 1489 |
+
border-bottom: 1px solid var(--line);
|
| 1490 |
+
background: var(--paper);
|
| 1491 |
+
}
|
| 1492 |
+
#view-tabs a {
|
| 1493 |
+
display: inline-flex;
|
| 1494 |
+
align-items: center;
|
| 1495 |
+
gap: 8px;
|
| 1496 |
+
min-height: 44px;
|
| 1497 |
+
margin-bottom: -1px;
|
| 1498 |
+
color: var(--muted);
|
| 1499 |
+
border-bottom: 2px solid transparent;
|
| 1500 |
+
text-decoration: none;
|
| 1501 |
+
font-size: 13.5px;
|
| 1502 |
+
font-weight: 600;
|
| 1503 |
+
transition: color 0.12s, border-color 0.12s;
|
| 1504 |
+
}
|
| 1505 |
+
#view-tabs a:hover {
|
| 1506 |
+
color: var(--ink);
|
| 1507 |
+
}
|
| 1508 |
+
#view-tabs a.active {
|
| 1509 |
+
color: var(--accent-strong);
|
| 1510 |
+
border-bottom-color: var(--accent);
|
| 1511 |
+
}
|
| 1512 |
+
#view-tabs svg {
|
| 1513 |
+
width: 18px;
|
| 1514 |
+
height: 18px;
|
| 1515 |
+
flex: 0 0 auto;
|
| 1516 |
+
fill: none;
|
| 1517 |
+
stroke: currentColor;
|
| 1518 |
+
stroke-width: 2;
|
| 1519 |
+
stroke-linecap: round;
|
| 1520 |
+
stroke-linejoin: round;
|
| 1521 |
+
}
|
| 1522 |
+
.workspace-file svg,
|
| 1523 |
+
.workspace-folder summary svg,
|
| 1524 |
+
.workspace-download svg {
|
| 1525 |
+
width: 17px;
|
| 1526 |
+
height: 17px;
|
| 1527 |
+
flex: 0 0 auto;
|
| 1528 |
+
fill: none;
|
| 1529 |
+
stroke: currentColor;
|
| 1530 |
+
stroke-width: 1.7;
|
| 1531 |
+
stroke-linecap: round;
|
| 1532 |
+
stroke-linejoin: round;
|
| 1533 |
+
}
|
| 1534 |
+
|
| 1535 |
+
#page.trace-page,
|
| 1536 |
+
#page.workspace-page {
|
| 1537 |
+
max-width: 1080px;
|
| 1538 |
+
}
|
| 1539 |
+
.view-loading {
|
| 1540 |
+
padding: 72px 0;
|
| 1541 |
+
color: var(--muted);
|
| 1542 |
+
text-align: center;
|
| 1543 |
+
}
|
| 1544 |
+
.view-empty {
|
| 1545 |
+
margin: 48px 0;
|
| 1546 |
+
padding: 44px 28px;
|
| 1547 |
+
border: 1px dashed #d8dbe1;
|
| 1548 |
+
border-radius: var(--radius);
|
| 1549 |
+
background: rgba(255, 255, 255, 0.72);
|
| 1550 |
+
text-align: center;
|
| 1551 |
+
}
|
| 1552 |
+
.view-empty h2 {
|
| 1553 |
+
margin: 0 0 7px;
|
| 1554 |
+
font-size: 18px;
|
| 1555 |
+
}
|
| 1556 |
+
.view-empty p {
|
| 1557 |
+
max-width: 560px;
|
| 1558 |
+
margin: 0 auto;
|
| 1559 |
+
color: var(--muted);
|
| 1560 |
+
}
|
| 1561 |
+
.view-empty code {
|
| 1562 |
+
display: inline-block;
|
| 1563 |
+
margin-top: 18px;
|
| 1564 |
+
padding: 7px 10px;
|
| 1565 |
+
border-radius: 7px;
|
| 1566 |
+
background: var(--code-bg);
|
| 1567 |
+
font-family: var(--mono);
|
| 1568 |
+
font-size: 12px;
|
| 1569 |
+
}
|
| 1570 |
+
#page .repo-ref-link {
|
| 1571 |
+
display: inline-block;
|
| 1572 |
+
margin-top: 18px;
|
| 1573 |
+
padding: 8px 14px;
|
| 1574 |
+
border-radius: 8px;
|
| 1575 |
+
background: var(--accent-strong, #2158d0);
|
| 1576 |
+
color: #fff;
|
| 1577 |
+
font-weight: 600;
|
| 1578 |
+
text-decoration: none;
|
| 1579 |
+
}
|
| 1580 |
+
#page .repo-ref-link:hover,
|
| 1581 |
+
#page .repo-ref-link:focus-visible {
|
| 1582 |
+
color: #fff;
|
| 1583 |
+
filter: brightness(0.95);
|
| 1584 |
+
}
|
| 1585 |
+
.view-eyebrow {
|
| 1586 |
+
margin-bottom: 4px;
|
| 1587 |
+
color: var(--accent-strong);
|
| 1588 |
+
font-family: var(--mono);
|
| 1589 |
+
font-size: 11px;
|
| 1590 |
+
font-weight: 700;
|
| 1591 |
+
letter-spacing: 0.12em;
|
| 1592 |
+
text-transform: uppercase;
|
| 1593 |
+
}
|
| 1594 |
+
|
| 1595 |
+
/* ---- trace ---- */
|
| 1596 |
+
.trace-session {
|
| 1597 |
+
scroll-margin-top: 24px;
|
| 1598 |
+
}
|
| 1599 |
+
.trace-session + .trace-session {
|
| 1600 |
+
margin-top: 44px;
|
| 1601 |
+
padding-top: 40px;
|
| 1602 |
+
border-top: 1px solid var(--line);
|
| 1603 |
+
}
|
| 1604 |
+
.trace-session-title {
|
| 1605 |
+
margin: 0 0 14px;
|
| 1606 |
+
color: var(--ink);
|
| 1607 |
+
font-family: var(--serif);
|
| 1608 |
+
font-size: 22px;
|
| 1609 |
+
line-height: 1.2;
|
| 1610 |
+
letter-spacing: -0.02em;
|
| 1611 |
+
overflow-wrap: anywhere;
|
| 1612 |
+
}
|
| 1613 |
+
.workspace-header h1 {
|
| 1614 |
+
margin: 0;
|
| 1615 |
+
color: var(--ink);
|
| 1616 |
+
font-size: 30px;
|
| 1617 |
+
line-height: 1.2;
|
| 1618 |
+
letter-spacing: -0.025em;
|
| 1619 |
+
}
|
| 1620 |
+
.trace-meta {
|
| 1621 |
+
display: flex;
|
| 1622 |
+
flex-wrap: wrap;
|
| 1623 |
+
gap: 9px 20px;
|
| 1624 |
+
margin-bottom: 34px;
|
| 1625 |
+
padding: 14px 16px;
|
| 1626 |
+
border: 1px solid var(--line);
|
| 1627 |
+
border-radius: 10px;
|
| 1628 |
+
background: rgba(255, 255, 255, 0.78);
|
| 1629 |
+
color: var(--muted);
|
| 1630 |
+
font-family: var(--mono);
|
| 1631 |
+
font-size: 11px;
|
| 1632 |
+
}
|
| 1633 |
+
.trace-meta strong {
|
| 1634 |
+
color: var(--ink);
|
| 1635 |
+
font-weight: 650;
|
| 1636 |
+
}
|
| 1637 |
+
.trace-source-missing {
|
| 1638 |
+
color: #b45309;
|
| 1639 |
+
}
|
| 1640 |
+
.trace-timeline {
|
| 1641 |
+
position: relative;
|
| 1642 |
+
}
|
| 1643 |
+
.trace-timeline::before {
|
| 1644 |
+
content: "";
|
| 1645 |
+
position: absolute;
|
| 1646 |
+
top: 0;
|
| 1647 |
+
bottom: 0;
|
| 1648 |
+
left: 82px;
|
| 1649 |
+
width: 1px;
|
| 1650 |
+
background: #dedfe3;
|
| 1651 |
+
}
|
| 1652 |
+
.trace-load-controls {
|
| 1653 |
+
display: flex;
|
| 1654 |
+
align-items: center;
|
| 1655 |
+
justify-content: space-between;
|
| 1656 |
+
gap: 16px;
|
| 1657 |
+
margin: 22px 0 0 100px;
|
| 1658 |
+
padding-top: 16px;
|
| 1659 |
+
border-top: 1px solid var(--line);
|
| 1660 |
+
}
|
| 1661 |
+
.trace-load-progress {
|
| 1662 |
+
color: var(--muted);
|
| 1663 |
+
font-family: var(--mono);
|
| 1664 |
+
font-size: 11px;
|
| 1665 |
+
}
|
| 1666 |
+
.trace-load-more {
|
| 1667 |
+
padding: 7px 12px;
|
| 1668 |
+
border: 1px solid var(--line-strong);
|
| 1669 |
+
border-radius: 7px;
|
| 1670 |
+
background: var(--paper);
|
| 1671 |
+
color: var(--ink);
|
| 1672 |
+
cursor: pointer;
|
| 1673 |
+
font: 650 12px/1.2 var(--sans);
|
| 1674 |
+
}
|
| 1675 |
+
.trace-load-more:hover:not(:disabled) {
|
| 1676 |
+
border-color: var(--accent);
|
| 1677 |
+
color: var(--accent-strong);
|
| 1678 |
+
}
|
| 1679 |
+
.trace-load-more:disabled {
|
| 1680 |
+
cursor: default;
|
| 1681 |
+
opacity: 0.65;
|
| 1682 |
+
}
|
| 1683 |
+
.trace-entry {
|
| 1684 |
+
--trace-depth: 0;
|
| 1685 |
+
position: relative;
|
| 1686 |
+
display: grid;
|
| 1687 |
+
grid-template-columns: 100px minmax(0, 1fr);
|
| 1688 |
+
margin: 0 0 18px calc(var(--trace-depth) * 24px);
|
| 1689 |
+
}
|
| 1690 |
+
.trace-rail {
|
| 1691 |
+
position: relative;
|
| 1692 |
+
min-height: 36px;
|
| 1693 |
+
padding: 4px 28px 0 0;
|
| 1694 |
+
color: #8a8d95;
|
| 1695 |
+
text-align: right;
|
| 1696 |
+
font-family: var(--mono);
|
| 1697 |
+
}
|
| 1698 |
+
.trace-number,
|
| 1699 |
+
.trace-elapsed {
|
| 1700 |
+
display: block;
|
| 1701 |
+
white-space: nowrap;
|
| 1702 |
+
}
|
| 1703 |
+
.trace-number {
|
| 1704 |
+
font-size: 12px;
|
| 1705 |
+
font-weight: 650;
|
| 1706 |
+
}
|
| 1707 |
+
.trace-elapsed {
|
| 1708 |
+
margin-top: 3px;
|
| 1709 |
+
font-size: 10px;
|
| 1710 |
+
}
|
| 1711 |
+
.trace-dot {
|
| 1712 |
+
position: absolute;
|
| 1713 |
+
top: 10px;
|
| 1714 |
+
right: 11px;
|
| 1715 |
+
width: 11px;
|
| 1716 |
+
height: 11px;
|
| 1717 |
+
border: 2px solid var(--paper);
|
| 1718 |
+
border-radius: 50%;
|
| 1719 |
+
background: var(--accent);
|
| 1720 |
+
box-shadow: 0 0 0 1px #d7d9de;
|
| 1721 |
+
}
|
| 1722 |
+
.trace-card {
|
| 1723 |
+
min-width: 0;
|
| 1724 |
+
overflow: hidden;
|
| 1725 |
+
border: 1px solid #dddfe4;
|
| 1726 |
+
border-radius: 11px;
|
| 1727 |
+
background: rgba(255, 255, 255, 0.92);
|
| 1728 |
+
}
|
| 1729 |
+
.trace-card > header {
|
| 1730 |
+
display: flex;
|
| 1731 |
+
align-items: center;
|
| 1732 |
+
gap: 10px;
|
| 1733 |
+
min-height: 37px;
|
| 1734 |
+
padding: 8px 13px;
|
| 1735 |
+
border-bottom: 1px solid #eceef1;
|
| 1736 |
+
}
|
| 1737 |
+
.trace-status .trace-card > header {
|
| 1738 |
+
border-bottom: 0;
|
| 1739 |
+
padding-bottom: 5px;
|
| 1740 |
+
}
|
| 1741 |
+
.trace-kind {
|
| 1742 |
+
font-family: var(--mono);
|
| 1743 |
+
font-size: 10.5px;
|
| 1744 |
+
font-weight: 750;
|
| 1745 |
+
letter-spacing: 0.08em;
|
| 1746 |
+
text-transform: uppercase;
|
| 1747 |
+
}
|
| 1748 |
+
.trace-turn {
|
| 1749 |
+
color: var(--muted);
|
| 1750 |
+
font: 10px var(--mono);
|
| 1751 |
+
}
|
| 1752 |
+
.trace-status-badge {
|
| 1753 |
+
margin-left: auto;
|
| 1754 |
+
padding: 1px 6px;
|
| 1755 |
+
border-radius: 999px;
|
| 1756 |
+
background: #eef0f3;
|
| 1757 |
+
color: var(--muted);
|
| 1758 |
+
font: 9.5px var(--mono);
|
| 1759 |
+
text-transform: uppercase;
|
| 1760 |
+
}
|
| 1761 |
+
.trace-status-badge-error,
|
| 1762 |
+
.trace-status-badge-failed {
|
| 1763 |
+
background: #fef2f2;
|
| 1764 |
+
color: #b91c1c;
|
| 1765 |
+
}
|
| 1766 |
+
.trace-body {
|
| 1767 |
+
margin: 0;
|
| 1768 |
+
padding: 15px 17px 17px;
|
| 1769 |
+
overflow-wrap: anywhere;
|
| 1770 |
+
white-space: pre-wrap;
|
| 1771 |
+
font-family: var(--sans);
|
| 1772 |
+
font-size: 13px;
|
| 1773 |
+
line-height: 1.65;
|
| 1774 |
+
}
|
| 1775 |
+
.trace-reasoning .trace-card {
|
| 1776 |
+
border-style: dashed;
|
| 1777 |
+
border-color: #d7b98a;
|
| 1778 |
+
background: #fffdf8;
|
| 1779 |
+
}
|
| 1780 |
+
.trace-reasoning .trace-kind {
|
| 1781 |
+
color: #9a6b22;
|
| 1782 |
+
}
|
| 1783 |
+
.trace-reasoning .trace-body {
|
| 1784 |
+
font-style: italic;
|
| 1785 |
+
}
|
| 1786 |
+
.trace-user .trace-card {
|
| 1787 |
+
border-left: 3px solid #f3a66d;
|
| 1788 |
+
}
|
| 1789 |
+
.trace-tool_call .trace-card,
|
| 1790 |
+
.trace-tool_result .trace-card {
|
| 1791 |
+
border-color: #2d3036;
|
| 1792 |
+
background: #191a1e;
|
| 1793 |
+
color: #ececf0;
|
| 1794 |
+
}
|
| 1795 |
+
.trace-tool_call .trace-card > header,
|
| 1796 |
+
.trace-tool_result .trace-card > header {
|
| 1797 |
+
border-bottom-color: rgba(255, 255, 255, 0.1);
|
| 1798 |
+
}
|
| 1799 |
+
.trace-tool_call .trace-kind,
|
| 1800 |
+
.trace-tool_result .trace-kind {
|
| 1801 |
+
color: #f5a66d;
|
| 1802 |
+
}
|
| 1803 |
+
.trace-tool_call .trace-turn,
|
| 1804 |
+
.trace-tool_result .trace-turn {
|
| 1805 |
+
color: #979aa3;
|
| 1806 |
+
}
|
| 1807 |
+
.trace-tool_call .trace-body,
|
| 1808 |
+
.trace-tool_result .trace-body,
|
| 1809 |
+
.trace-output pre {
|
| 1810 |
+
font-family: var(--mono);
|
| 1811 |
+
font-size: 11.5px;
|
| 1812 |
+
line-height: 1.6;
|
| 1813 |
+
}
|
| 1814 |
+
#page .trace-tool_call pre.trace-body,
|
| 1815 |
+
#page .trace-tool_result pre.trace-body {
|
| 1816 |
+
margin: 0;
|
| 1817 |
+
padding: 15px 17px 17px;
|
| 1818 |
+
border: 0;
|
| 1819 |
+
border-radius: 0;
|
| 1820 |
+
background: transparent;
|
| 1821 |
+
color: #ececf0;
|
| 1822 |
+
}
|
| 1823 |
+
.trace-output {
|
| 1824 |
+
border-top: 1px dashed rgba(255, 255, 255, 0.14);
|
| 1825 |
+
}
|
| 1826 |
+
.trace-output summary {
|
| 1827 |
+
padding: 9px 14px;
|
| 1828 |
+
color: #aaaeb7;
|
| 1829 |
+
cursor: pointer;
|
| 1830 |
+
font: 700 10px var(--mono);
|
| 1831 |
+
letter-spacing: 0.06em;
|
| 1832 |
+
text-transform: uppercase;
|
| 1833 |
+
}
|
| 1834 |
+
#page .trace-output pre {
|
| 1835 |
+
max-height: 480px;
|
| 1836 |
+
margin: 0;
|
| 1837 |
+
padding: 0 16px 16px;
|
| 1838 |
+
border: 0;
|
| 1839 |
+
border-radius: 0;
|
| 1840 |
+
background: transparent;
|
| 1841 |
+
overflow: auto;
|
| 1842 |
+
color: #d7d8dd;
|
| 1843 |
+
white-space: pre-wrap;
|
| 1844 |
+
}
|
| 1845 |
+
|
| 1846 |
+
/* ---- workspace ---- */
|
| 1847 |
+
.workspace-header {
|
| 1848 |
+
padding-bottom: 24px;
|
| 1849 |
+
}
|
| 1850 |
+
.workspace-header p {
|
| 1851 |
+
margin: 0;
|
| 1852 |
+
color: var(--muted);
|
| 1853 |
+
font-family: var(--mono);
|
| 1854 |
+
font-size: 11px;
|
| 1855 |
+
}
|
| 1856 |
+
.workspace-inventory {
|
| 1857 |
+
overflow: hidden;
|
| 1858 |
+
border: 1px solid var(--line);
|
| 1859 |
+
border-radius: 11px;
|
| 1860 |
+
background: rgba(255, 255, 255, 0.92);
|
| 1861 |
+
}
|
| 1862 |
+
.workspace-folder > summary {
|
| 1863 |
+
display: flex;
|
| 1864 |
+
align-items: center;
|
| 1865 |
+
gap: 8px;
|
| 1866 |
+
min-height: 39px;
|
| 1867 |
+
padding: 8px 13px;
|
| 1868 |
+
background: #fafafa;
|
| 1869 |
+
cursor: pointer;
|
| 1870 |
+
font-weight: 650;
|
| 1871 |
+
list-style: none;
|
| 1872 |
+
}
|
| 1873 |
+
.workspace-folder > summary::-webkit-details-marker {
|
| 1874 |
+
display: none;
|
| 1875 |
+
}
|
| 1876 |
+
.workspace-folder > summary::after {
|
| 1877 |
+
content: "›";
|
| 1878 |
+
margin-left: auto;
|
| 1879 |
+
color: #989ba2;
|
| 1880 |
+
transform: rotate(90deg);
|
| 1881 |
+
}
|
| 1882 |
+
.workspace-folder:not([open]) > summary::after {
|
| 1883 |
+
transform: rotate(0);
|
| 1884 |
+
}
|
| 1885 |
+
.workspace-folder-children {
|
| 1886 |
+
padding-left: 20px;
|
| 1887 |
+
}
|
| 1888 |
+
.workspace-file {
|
| 1889 |
+
display: grid;
|
| 1890 |
+
grid-template-columns: minmax(180px, 1fr) 72px 78px 180px 36px;
|
| 1891 |
+
align-items: center;
|
| 1892 |
+
min-height: 44px;
|
| 1893 |
+
padding: 7px 10px 7px 13px;
|
| 1894 |
+
color: var(--muted);
|
| 1895 |
+
font-family: var(--mono);
|
| 1896 |
+
font-size: 10.5px;
|
| 1897 |
+
}
|
| 1898 |
+
.workspace-file-name {
|
| 1899 |
+
display: flex;
|
| 1900 |
+
align-items: center;
|
| 1901 |
+
min-width: 0;
|
| 1902 |
+
gap: 8px;
|
| 1903 |
+
color: var(--ink);
|
| 1904 |
+
font-family: var(--sans);
|
| 1905 |
+
font-size: 12.5px;
|
| 1906 |
+
font-weight: 550;
|
| 1907 |
+
}
|
| 1908 |
+
.workspace-file-name span {
|
| 1909 |
+
overflow: hidden;
|
| 1910 |
+
text-overflow: ellipsis;
|
| 1911 |
+
white-space: nowrap;
|
| 1912 |
+
}
|
| 1913 |
+
.workspace-file-type {
|
| 1914 |
+
width: fit-content;
|
| 1915 |
+
padding: 1px 6px;
|
| 1916 |
+
border-radius: 999px;
|
| 1917 |
+
background: var(--accent-soft);
|
| 1918 |
+
color: var(--accent-strong);
|
| 1919 |
+
text-transform: uppercase;
|
| 1920 |
+
}
|
| 1921 |
+
.workspace-download {
|
| 1922 |
+
display: inline-flex;
|
| 1923 |
+
align-items: center;
|
| 1924 |
+
justify-content: center;
|
| 1925 |
+
width: 30px;
|
| 1926 |
+
height: 30px;
|
| 1927 |
+
border-radius: 7px;
|
| 1928 |
+
color: var(--muted);
|
| 1929 |
+
}
|
| 1930 |
+
.workspace-download:hover {
|
| 1931 |
+
background: var(--accent-soft);
|
| 1932 |
+
color: var(--accent-strong);
|
| 1933 |
+
}
|
| 1934 |
+
.workspace-unpublished {
|
| 1935 |
+
color: #9ca3af;
|
| 1936 |
+
text-align: center;
|
| 1937 |
+
}
|
| 1938 |
+
|
| 1939 |
+
.workspace-header {
|
| 1940 |
+
display: flex;
|
| 1941 |
+
align-items: center;
|
| 1942 |
+
justify-content: space-between;
|
| 1943 |
+
gap: 16px;
|
| 1944 |
+
flex-wrap: wrap;
|
| 1945 |
+
}
|
| 1946 |
+
.workspace-toggle {
|
| 1947 |
+
display: inline-flex;
|
| 1948 |
+
align-items: center;
|
| 1949 |
+
padding: 2px;
|
| 1950 |
+
border: 1px solid var(--line);
|
| 1951 |
+
border-radius: 999px;
|
| 1952 |
+
background: #fafafa;
|
| 1953 |
+
}
|
| 1954 |
+
.workspace-toggle-btn {
|
| 1955 |
+
padding: 4px 13px;
|
| 1956 |
+
border: 0;
|
| 1957 |
+
border-radius: 999px;
|
| 1958 |
+
background: transparent;
|
| 1959 |
+
color: var(--muted);
|
| 1960 |
+
font-family: var(--sans);
|
| 1961 |
+
font-size: 12px;
|
| 1962 |
+
font-weight: 600;
|
| 1963 |
+
cursor: pointer;
|
| 1964 |
+
}
|
| 1965 |
+
.workspace-toggle-btn:hover {
|
| 1966 |
+
color: var(--accent-strong);
|
| 1967 |
+
}
|
| 1968 |
+
.workspace-toggle-btn.is-active {
|
| 1969 |
+
background: var(--accent);
|
| 1970 |
+
color: #ffffff;
|
| 1971 |
+
}
|
| 1972 |
+
.workspace-group + .workspace-group {
|
| 1973 |
+
margin-top: 18px;
|
| 1974 |
+
}
|
| 1975 |
+
.workspace-group-head,
|
| 1976 |
+
.workspace-hub-group-head {
|
| 1977 |
+
display: flex;
|
| 1978 |
+
align-items: center;
|
| 1979 |
+
gap: 8px;
|
| 1980 |
+
margin: 0;
|
| 1981 |
+
padding: 8px 13px;
|
| 1982 |
+
background: #fafafa;
|
| 1983 |
+
border-bottom: 1px solid var(--line);
|
| 1984 |
+
color: var(--ink);
|
| 1985 |
+
font-family: var(--sans);
|
| 1986 |
+
font-size: 12px;
|
| 1987 |
+
font-weight: 650;
|
| 1988 |
+
text-transform: capitalize;
|
| 1989 |
+
}
|
| 1990 |
+
.workspace-group-count,
|
| 1991 |
+
.workspace-hub-count {
|
| 1992 |
+
padding: 0 7px;
|
| 1993 |
+
border-radius: 999px;
|
| 1994 |
+
background: var(--accent-soft);
|
| 1995 |
+
color: var(--accent-strong);
|
| 1996 |
+
font-family: var(--mono);
|
| 1997 |
+
font-size: 10.5px;
|
| 1998 |
+
}
|
| 1999 |
+
.workspace-group {
|
| 2000 |
+
overflow: hidden;
|
| 2001 |
+
border: 1px solid var(--line);
|
| 2002 |
+
border-radius: 11px;
|
| 2003 |
+
background: rgba(255, 255, 255, 0.92);
|
| 2004 |
+
}
|
| 2005 |
+
|
| 2006 |
+
.workspace-hub {
|
| 2007 |
+
margin-top: 28px;
|
| 2008 |
+
}
|
| 2009 |
+
.workspace-hub-title {
|
| 2010 |
+
margin: 0 0 14px;
|
| 2011 |
+
font-family: var(--sans);
|
| 2012 |
+
font-size: 16px;
|
| 2013 |
+
font-weight: 700;
|
| 2014 |
+
color: var(--ink);
|
| 2015 |
+
}
|
| 2016 |
+
.workspace-hub-group {
|
| 2017 |
+
overflow: hidden;
|
| 2018 |
+
border: 1px solid var(--line);
|
| 2019 |
+
border-radius: 11px;
|
| 2020 |
+
background: rgba(255, 255, 255, 0.92);
|
| 2021 |
+
}
|
| 2022 |
+
.workspace-hub-group + .workspace-hub-group {
|
| 2023 |
+
margin-top: 14px;
|
| 2024 |
+
}
|
| 2025 |
+
.workspace-hub-list {
|
| 2026 |
+
display: flex;
|
| 2027 |
+
flex-direction: column;
|
| 2028 |
+
}
|
| 2029 |
+
.workspace-hub-link {
|
| 2030 |
+
padding: 9px 13px;
|
| 2031 |
+
color: var(--accent-strong);
|
| 2032 |
+
font-family: var(--mono);
|
| 2033 |
+
font-size: 12px;
|
| 2034 |
+
text-decoration: none;
|
| 2035 |
+
overflow: hidden;
|
| 2036 |
+
text-overflow: ellipsis;
|
| 2037 |
+
white-space: nowrap;
|
| 2038 |
+
}
|
| 2039 |
+
.workspace-hub-link + .workspace-hub-link {
|
| 2040 |
+
border-top: 1px solid var(--line);
|
| 2041 |
+
}
|
| 2042 |
+
.workspace-hub-link:hover {
|
| 2043 |
+
background: var(--accent-soft);
|
| 2044 |
+
text-decoration: underline;
|
| 2045 |
+
}
|
| 2046 |
+
|
| 2047 |
+
/* --- UI nits --- */
|
| 2048 |
+
/* Flush group headers: #page h3/h2 (ID selectors) otherwise inject a top margin
|
| 2049 |
+
that, with overflow:hidden on the card, shows as whitespace above "Jobs" etc. */
|
| 2050 |
+
#page .workspace-hub-title {
|
| 2051 |
+
margin: 0 0 14px;
|
| 2052 |
+
}
|
| 2053 |
+
#page .workspace-hub-group-head,
|
| 2054 |
+
#page .workspace-group-head {
|
| 2055 |
+
margin: 0;
|
| 2056 |
+
}
|
| 2057 |
+
/* HF brand logo before the "Hugging Face artifacts" heading */
|
| 2058 |
+
.workspace-hub-title {
|
| 2059 |
+
display: flex;
|
| 2060 |
+
align-items: center;
|
| 2061 |
+
gap: 9px;
|
| 2062 |
+
}
|
| 2063 |
+
.workspace-hub-logo {
|
| 2064 |
+
width: 22px;
|
| 2065 |
+
height: 22px;
|
| 2066 |
+
flex: none;
|
| 2067 |
+
}
|
| 2068 |
+
/* Center empty-state placeholders (heading, body, command) */
|
| 2069 |
+
.view-empty {
|
| 2070 |
+
display: flex;
|
| 2071 |
+
flex-direction: column;
|
| 2072 |
+
align-items: center;
|
| 2073 |
+
}
|
| 2074 |
+
#page .view-empty h2,
|
| 2075 |
+
#page .view-empty p {
|
| 2076 |
+
text-align: center;
|
| 2077 |
+
}
|
| 2078 |
+
|
| 2079 |
+
@media (max-width: 720px) {
|
| 2080 |
+
#app {
|
| 2081 |
+
flex-direction: column;
|
| 2082 |
+
}
|
| 2083 |
+
#sidebar {
|
| 2084 |
+
width: 100%;
|
| 2085 |
+
flex: none;
|
| 2086 |
+
height: auto;
|
| 2087 |
+
position: static;
|
| 2088 |
+
}
|
| 2089 |
+
#content {
|
| 2090 |
+
display: block;
|
| 2091 |
+
width: 100%;
|
| 2092 |
+
padding: 28px 20px 80px;
|
| 2093 |
+
overflow-x: hidden;
|
| 2094 |
+
}
|
| 2095 |
+
#view-tabs {
|
| 2096 |
+
margin: 0 0 20px;
|
| 2097 |
+
gap: 18px;
|
| 2098 |
+
justify-content: flex-start;
|
| 2099 |
+
overflow-x: auto;
|
| 2100 |
+
}
|
| 2101 |
+
#view-tabs a {
|
| 2102 |
+
flex: 0 0 auto;
|
| 2103 |
+
}
|
| 2104 |
+
.trace-timeline::before {
|
| 2105 |
+
left: 16px;
|
| 2106 |
+
}
|
| 2107 |
+
.trace-entry {
|
| 2108 |
+
grid-template-columns: 32px minmax(0, 1fr);
|
| 2109 |
+
margin-left: calc(var(--trace-depth) * 10px);
|
| 2110 |
+
}
|
| 2111 |
+
.trace-rail {
|
| 2112 |
+
padding: 0;
|
| 2113 |
+
}
|
| 2114 |
+
.trace-number,
|
| 2115 |
+
.trace-elapsed {
|
| 2116 |
+
display: none;
|
| 2117 |
+
}
|
| 2118 |
+
.trace-dot {
|
| 2119 |
+
top: 10px;
|
| 2120 |
+
right: 10px;
|
| 2121 |
+
}
|
| 2122 |
+
.workspace-file {
|
| 2123 |
+
grid-template-columns: minmax(150px, 1fr) 66px 34px;
|
| 2124 |
+
}
|
| 2125 |
+
.workspace-file-size,
|
| 2126 |
+
.workspace-file-time {
|
| 2127 |
+
display: none;
|
| 2128 |
+
}
|
| 2129 |
+
#page {
|
| 2130 |
+
width: 100%;
|
| 2131 |
+
max-width: 100%;
|
| 2132 |
+
}
|
| 2133 |
+
#page h1,
|
| 2134 |
+
#logbook-title {
|
| 2135 |
+
font-size: 30px;
|
| 2136 |
+
}
|
| 2137 |
+
.cell-head {
|
| 2138 |
+
align-items: flex-start;
|
| 2139 |
+
flex-direction: column;
|
| 2140 |
+
gap: 4px;
|
| 2141 |
+
}
|
| 2142 |
+
}
|
logbook.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
logbook.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"schema_version": 2,
|
| 3 |
+
"title": "Reproduction: Memora: A Harmonic Memory Representation Balancing Abstraction and Specificity",
|
| 4 |
+
"emoji": "🎯",
|
| 5 |
+
"space_id": "JUNGU/repro-memora-a-harmonic-memory-representation-balancing-abstraction-and-specificity",
|
| 6 |
+
"paper": {
|
| 7 |
+
"arxiv_id": "2602.03315"
|
| 8 |
+
},
|
| 9 |
+
"tags": [
|
| 10 |
+
"icml2026-repro",
|
| 11 |
+
"paper-zSrvkj0ers"
|
| 12 |
+
],
|
| 13 |
+
"updated_at": "2026-07-24T14:43:13+00:00",
|
| 14 |
+
"root": {
|
| 15 |
+
"slug": "index",
|
| 16 |
+
"title": "Reproduction: Memora: A Harmonic Memory Representation Balancing Abstraction and Specificity",
|
| 17 |
+
"file": "pages/index.md",
|
| 18 |
+
"children": [
|
| 19 |
+
{
|
| 20 |
+
"slug": "executive-summary",
|
| 21 |
+
"title": "Executive summary",
|
| 22 |
+
"file": "pages/executive-summary/page.md",
|
| 23 |
+
"children": []
|
| 24 |
+
},
|
| 25 |
+
{
|
| 26 |
+
"slug": "claim-1-locomo-policy-retriever-reaches-0-863-llm-as-a-judge-and-exceeds-full-context",
|
| 27 |
+
"title": "Claim 1: LoCoMo Policy Retriever reaches 0.863 LLM-as-a-Judge and exceeds full-context.",
|
| 28 |
+
"file": "pages/claim-1-locomo-policy-retriever-reaches-0-863-llm-as-a-judge-and-exceeds-full-context/page.md",
|
| 29 |
+
"children": []
|
| 30 |
+
},
|
| 31 |
+
{
|
| 32 |
+
"slug": "claim-2-locomo-semantic-retriever-reaches-0-849-llm-as-a-judge-and-exceeds-full-context",
|
| 33 |
+
"title": "Claim 2: LoCoMo Semantic Retriever reaches 0.849 LLM-as-a-Judge and exceeds full-context.",
|
| 34 |
+
"file": "pages/claim-2-locomo-semantic-retriever-reaches-0-849-llm-as-a-judge-and-exceeds-full-context/page.md",
|
| 35 |
+
"children": []
|
| 36 |
+
},
|
| 37 |
+
{
|
| 38 |
+
"slug": "claim-3-longmemeval-reaches-87-4-average-accuracy-with-up-to-98-token-reduction",
|
| 39 |
+
"title": "Claim 3: LongMemEval reaches 87.4% average accuracy with up to 98% token reduction.",
|
| 40 |
+
"file": "pages/claim-3-longmemeval-reaches-87-4-average-accuracy-with-up-to-98-token-reduction/page.md",
|
| 41 |
+
"children": []
|
| 42 |
+
},
|
| 43 |
+
{
|
| 44 |
+
"slug": "claim-4-harmonic-memory-uses-a-primary-abstraction-concrete-memory-value-and-cue-anchors",
|
| 45 |
+
"title": "Claim 4: Harmonic memory uses a primary abstraction, concrete memory value, and cue anchors.",
|
| 46 |
+
"file": "pages/claim-4-harmonic-memory-uses-a-primary-abstraction-concrete-memory-value-and-cue-anchors/page.md",
|
| 47 |
+
"children": []
|
| 48 |
+
},
|
| 49 |
+
{
|
| 50 |
+
"slug": "claim-5-rag-and-kg-retrieval-are-special-cases-of-the-memora-dual-layer-design",
|
| 51 |
+
"title": "Claim 5: RAG and KG retrieval are special cases of the Memora dual-layer design.",
|
| 52 |
+
"file": "pages/claim-5-rag-and-kg-retrieval-are-special-cases-of-the-memora-dual-layer-design/page.md",
|
| 53 |
+
"children": []
|
| 54 |
+
},
|
| 55 |
+
{
|
| 56 |
+
"slug": "conclusion",
|
| 57 |
+
"title": "Conclusion",
|
| 58 |
+
"file": "pages/conclusion/page.md",
|
| 59 |
+
"children": []
|
| 60 |
+
}
|
| 61 |
+
]
|
| 62 |
+
},
|
| 63 |
+
"traces": [],
|
| 64 |
+
"workspace": {
|
| 65 |
+
"file": "workspace.json",
|
| 66 |
+
"file_count": 0,
|
| 67 |
+
"total_size": 0,
|
| 68 |
+
"bucket_id": null
|
| 69 |
+
},
|
| 70 |
+
"agent_view_tokens": 3880,
|
| 71 |
+
"trace_view_tokens": 10,
|
| 72 |
+
"workspace_view_tokens": 8,
|
| 73 |
+
"revision": "feab0879a55f521b8739"
|
| 74 |
+
}
|
pages/claim-1-locomo-policy-retriever-reaches-0-863-llm-as-a-judge-and-exceeds-full-context/page.md
ADDED
|
@@ -0,0 +1,2440 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Claim 1: LoCoMo Policy Retriever reaches 0.863 LLM-as-a-Judge and exceeds full-context.
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
---
|
| 5 |
+
<!-- trackio-cell
|
| 6 |
+
{"type": "markdown", "id": "cell_f017fe994a2e", "created_at": "2026-07-24T05:17:39+00:00", "title": "Claim 1: LoCoMo Policy Retriever reaches 0.863 LLM-as-a-Judge and exceeds full-context."}
|
| 7 |
+
-->
|
| 8 |
+
Document setup, runs, and results for **Claim 1: LoCoMo Policy Retriever reaches 0.863 LLM-as-a-Judge and exceeds full-context.**.
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
---
|
| 12 |
+
<!-- trackio-cell
|
| 13 |
+
{"type": "markdown", "id": "cell_026153fd1ee3", "created_at": "2026-07-24T05:17:48+00:00", "title": "Protocol and model substitution"}
|
| 14 |
+
-->
|
| 15 |
+
We test the official Memora Policy Retriever on the complete LoCoMo dataset: all 10 conversations, 272 sessions, and 1,540 evaluated questions after the repository-standard exclusion of category 5. The memory construction, cue index, raw-segment episodic memory, factual memory, BM25 hybrid retrieval, top-k=30, and four-step prompted policy are preserved. Because the original GPT-4.1-mini and OpenAI embedding endpoints are unavailable, this is a full-scale portability reproduction using GLM-5.2 through an OpenAI-compatible API and local BAAI/bge-small-en-v1.5 embeddings. We therefore compare against the reported 0.863 Policy score and 0.825 full-context baseline while treating any difference as model-stack-sensitive rather than an exact same-model replication.
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
---
|
| 19 |
+
<!-- trackio-cell
|
| 20 |
+
{"type": "code", "id": "cell_dece326842f3", "created_at": "2026-07-24T05:17:56+00:00", "title": "Run: python run_locomo_reproduction.py (exit 1)", "command": [".venv/bin/python", "scripts/run_locomo_reproduction.py", "--strategy", "prompt", "--run-prefix", "glm52-full", "--max-parallel", "3", "--retries", "3", "--env-file", "/Users/conanssam-m4/.ouroboros/worktrees/studymemory/orch_d223290f8ae3/.env", "--trackio"], "exit_code": 1, "duration_s": 1.758}
|
| 21 |
+
-->
|
| 22 |
+
````bash
|
| 23 |
+
$ .venv/bin/python scripts/run_locomo_reproduction.py --strategy prompt --run-prefix glm52-full --max-parallel 3 --retries 3 --env-file /Users/conanssam-m4/.ouroboros/worktrees/studymemory/orch_d223290f8ae3/.env --trackio
|
| 24 |
+
````
|
| 25 |
+
|
| 26 |
+
exit 1 · 1.8s
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
````python title=run_locomo_reproduction.py
|
| 30 |
+
#!/usr/bin/env python3
|
| 31 |
+
"""Run the full LoCoMo Memora reproduction as restartable conversation splits.
|
| 32 |
+
|
| 33 |
+
Each LoCoMo conversation has an isolated Memora user namespace. Running the ten
|
| 34 |
+
conversations in separate processes therefore preserves the official method
|
| 35 |
+
while avoiding result-directory collisions and making multi-hour API runs
|
| 36 |
+
restartable.
|
| 37 |
+
"""
|
| 38 |
+
|
| 39 |
+
from __future__ import annotations
|
| 40 |
+
|
| 41 |
+
import argparse
|
| 42 |
+
import concurrent.futures
|
| 43 |
+
import hashlib
|
| 44 |
+
import json
|
| 45 |
+
import os
|
| 46 |
+
import subprocess
|
| 47 |
+
import sys
|
| 48 |
+
import time
|
| 49 |
+
from collections import defaultdict
|
| 50 |
+
from datetime import datetime, timezone
|
| 51 |
+
from pathlib import Path
|
| 52 |
+
from statistics import fmean
|
| 53 |
+
from typing import Any
|
| 54 |
+
|
| 55 |
+
from dotenv import load_dotenv
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
| 59 |
+
LOCOMO_ROOT = REPO_ROOT / "app" / "locomo"
|
| 60 |
+
DATA_PATH = LOCOMO_ROOT / "data" / "locomo10.json"
|
| 61 |
+
DEFAULT_PROJECT = "memora-icml2026-repro"
|
| 62 |
+
PAPER_TARGETS = {
|
| 63 |
+
"prompt": {"llm_score": 0.863, "f1_score": 0.553, "bleu_score": 0.466},
|
| 64 |
+
"semantic": {"llm_score": 0.849, "f1_score": 0.552, "bleu_score": 0.464},
|
| 65 |
+
"full_context": {"llm_score": 0.825},
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def parse_args() -> argparse.Namespace:
|
| 70 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 71 |
+
parser.add_argument(
|
| 72 |
+
"--strategy",
|
| 73 |
+
choices=("prompt", "semantic"),
|
| 74 |
+
required=True,
|
| 75 |
+
help="Memora retrieval strategy to reproduce.",
|
| 76 |
+
)
|
| 77 |
+
parser.add_argument(
|
| 78 |
+
"--run-prefix",
|
| 79 |
+
default="glm52-full",
|
| 80 |
+
help="Stable campaign name used for result and memory directories.",
|
| 81 |
+
)
|
| 82 |
+
parser.add_argument(
|
| 83 |
+
"--splits",
|
| 84 |
+
nargs="+",
|
| 85 |
+
type=int,
|
| 86 |
+
default=list(range(1, 11)),
|
| 87 |
+
help="One-based LoCoMo conversation indices (default: all 1..10).",
|
| 88 |
+
)
|
| 89 |
+
parser.add_argument(
|
| 90 |
+
"--max-parallel",
|
| 91 |
+
type=int,
|
| 92 |
+
default=3,
|
| 93 |
+
help="Maximum concurrent GLM-backed split processes.",
|
| 94 |
+
)
|
| 95 |
+
parser.add_argument(
|
| 96 |
+
"--retries",
|
| 97 |
+
type=int,
|
| 98 |
+
default=2,
|
| 99 |
+
help="Process-level attempts per split.",
|
| 100 |
+
)
|
| 101 |
+
parser.add_argument(
|
| 102 |
+
"--env-file",
|
| 103 |
+
type=Path,
|
| 104 |
+
help="Optional dotenv file containing GLM_API_KEY and GLM_MODEL.",
|
| 105 |
+
)
|
| 106 |
+
parser.add_argument(
|
| 107 |
+
"--python",
|
| 108 |
+
type=Path,
|
| 109 |
+
default=REPO_ROOT / ".venv" / "bin" / "python",
|
| 110 |
+
help="Python interpreter for split processes.",
|
| 111 |
+
)
|
| 112 |
+
parser.add_argument(
|
| 113 |
+
"--trackio",
|
| 114 |
+
action="store_true",
|
| 115 |
+
help="Log split completion and aggregate metrics to Trackio.",
|
| 116 |
+
)
|
| 117 |
+
parser.add_argument(
|
| 118 |
+
"--dry-run",
|
| 119 |
+
action="store_true",
|
| 120 |
+
help="Print commands without running the experiment.",
|
| 121 |
+
)
|
| 122 |
+
return parser.parse_args()
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def validate_splits(splits: list[int]) -> list[int]:
|
| 126 |
+
normalized = sorted(set(splits))
|
| 127 |
+
if not normalized or normalized[0] < 1 or normalized[-1] > 10:
|
| 128 |
+
raise ValueError("--splits must contain one-based indices between 1 and 10")
|
| 129 |
+
return normalized
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def load_dataset(path: Path = DATA_PATH) -> list[dict[str, Any]]:
|
| 133 |
+
with path.open() as f:
|
| 134 |
+
data = json.load(f)
|
| 135 |
+
if len(data) != 10:
|
| 136 |
+
raise ValueError(f"Expected 10 LoCoMo conversations, found {len(data)}")
|
| 137 |
+
return data
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def dataset_summary(data: list[dict[str, Any]]) -> dict[str, Any]:
|
| 141 |
+
category_counts: dict[str, int] = defaultdict(int)
|
| 142 |
+
sessions = 0
|
| 143 |
+
question_counts = []
|
| 144 |
+
evaluated_counts = []
|
| 145 |
+
for item in data:
|
| 146 |
+
conversation = item["conversation"]
|
| 147 |
+
sessions += sum(
|
| 148 |
+
1
|
| 149 |
+
for key in conversation
|
| 150 |
+
if key.startswith("session_") and not key.endswith("_date_time")
|
| 151 |
+
)
|
| 152 |
+
qa = item["qa"]
|
| 153 |
+
question_counts.append(len(qa))
|
| 154 |
+
evaluated = 0
|
| 155 |
+
for question in qa:
|
| 156 |
+
category = str(question["category"])
|
| 157 |
+
category_counts[category] += 1
|
| 158 |
+
if category != "5":
|
| 159 |
+
evaluated += 1
|
| 160 |
+
evaluated_counts.append(evaluated)
|
| 161 |
+
return {
|
| 162 |
+
"conversations": len(data),
|
| 163 |
+
"sessions": sessions,
|
| 164 |
+
"questions": sum(question_counts),
|
| 165 |
+
"evaluated_questions": sum(evaluated_counts),
|
| 166 |
+
"question_counts": question_counts,
|
| 167 |
+
"evaluated_counts": evaluated_counts,
|
| 168 |
+
"category_counts": dict(sorted(category_counts.items())),
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def file_sha256(path: Path) -> str:
|
| 173 |
+
digest = hashlib.sha256()
|
| 174 |
+
with path.open("rb") as f:
|
| 175 |
+
for chunk in iter(lambda: f.read(1024 * 1024), b""):
|
| 176 |
+
digest.update(chunk)
|
| 177 |
+
return digest.hexdigest()
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def campaign_root(run_prefix: str) -> Path:
|
| 181 |
+
return LOCOMO_ROOT / "results" / "reproduction" / run_prefix
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def split_output_dir(run_prefix: str, strategy: str, split: int) -> Path:
|
| 185 |
+
return (
|
| 186 |
+
campaign_root(run_prefix)
|
| 187 |
+
/ "runs"
|
| 188 |
+
/ f"memora_{strategy}_split{split:02d}"
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def split_paths(
|
| 193 |
+
run_prefix: str,
|
| 194 |
+
strategy: str,
|
| 195 |
+
split: int,
|
| 196 |
+
) -> dict[str, Path]:
|
| 197 |
+
method = f"memora_{strategy}"
|
| 198 |
+
output_dir = split_output_dir(run_prefix, strategy, split)
|
| 199 |
+
return {
|
| 200 |
+
"dir": output_dir,
|
| 201 |
+
"output": output_dir / f"{method}_output.json",
|
| 202 |
+
"eval": output_dir / f"{method}_eval.json",
|
| 203 |
+
"scores": output_dir / f"{method}_scores.json",
|
| 204 |
+
"build_timing": output_dir / "build_timing.json",
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def memory_store_name(run_prefix: str, split: int) -> str:
|
| 209 |
+
safe_prefix = "".join(
|
| 210 |
+
char if char.isalnum() or char in "._-" else "-"
|
| 211 |
+
for char in run_prefix
|
| 212 |
+
).strip("-")
|
| 213 |
+
if not safe_prefix:
|
| 214 |
+
raise ValueError("--run-prefix must contain at least one safe character")
|
| 215 |
+
return f"{safe_prefix}-split{split:02d}"
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
def build_split_command(
|
| 219 |
+
*,
|
| 220 |
+
python: Path,
|
| 221 |
+
run_prefix: str,
|
| 222 |
+
strategy: str,
|
| 223 |
+
split: int,
|
| 224 |
+
force_rebuild: bool,
|
| 225 |
+
) -> list[str]:
|
| 226 |
+
return [
|
| 227 |
+
str(python),
|
| 228 |
+
"run_memora.py",
|
| 229 |
+
"--config-name",
|
| 230 |
+
"config_glm",
|
| 231 |
+
f"general.project_path={LOCOMO_ROOT}",
|
| 232 |
+
f"general.results_path={campaign_root(run_prefix) / 'runs'}",
|
| 233 |
+
"general.debug=false",
|
| 234 |
+
"general.resume=true",
|
| 235 |
+
f"general.run_name=split{split:02d}",
|
| 236 |
+
f"eval.subset_idx={split}",
|
| 237 |
+
"eval.max_workers=1",
|
| 238 |
+
f"memory.memory_store={memory_store_name(run_prefix, split)}",
|
| 239 |
+
f"memory.force_rebuild={'true' if force_rebuild else 'false'}",
|
| 240 |
+
f"retrieval.strategy={strategy}",
|
| 241 |
+
]
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def evaluated_item_count(path: Path) -> int:
|
| 245 |
+
with path.open() as f:
|
| 246 |
+
data = json.load(f)
|
| 247 |
+
return sum(len(items) for items in data.values())
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
def completed_split(
|
| 251 |
+
run_prefix: str,
|
| 252 |
+
strategy: str,
|
| 253 |
+
split: int,
|
| 254 |
+
expected_count: int,
|
| 255 |
+
) -> bool:
|
| 256 |
+
paths = split_paths(run_prefix, strategy, split)
|
| 257 |
+
if not paths["scores"].is_file() or not paths["eval"].is_file():
|
| 258 |
+
return False
|
| 259 |
+
try:
|
| 260 |
+
with paths["scores"].open() as f:
|
| 261 |
+
scores = json.load(f)
|
| 262 |
+
return (
|
| 263 |
+
scores["summary"]["total_questions"] == expected_count
|
| 264 |
+
and evaluated_item_count(paths["eval"]) == expected_count
|
| 265 |
+
)
|
| 266 |
+
except (KeyError, OSError, ValueError, json.JSONDecodeError):
|
| 267 |
+
return False
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
def tail(path: Path, line_count: int = 40) -> str:
|
| 271 |
+
try:
|
| 272 |
+
lines = path.read_text(errors="replace").splitlines()
|
| 273 |
+
except OSError:
|
| 274 |
+
return ""
|
| 275 |
+
return "\n".join(lines[-line_count:])
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
def run_split(
|
| 279 |
+
*,
|
| 280 |
+
python: Path,
|
| 281 |
+
run_prefix: str,
|
| 282 |
+
strategy: str,
|
| 283 |
+
split: int,
|
| 284 |
+
expected_count: int,
|
| 285 |
+
retries: int,
|
| 286 |
+
dry_run: bool,
|
| 287 |
+
) -> dict[str, Any]:
|
| 288 |
+
started = time.monotonic()
|
| 289 |
+
root = campaign_root(run_prefix)
|
| 290 |
+
log_path = root / "logs" / f"{strategy}-split{split:02d}.log"
|
| 291 |
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
| 292 |
+
|
| 293 |
+
if completed_split(run_prefix, strategy, split, expected_count):
|
| 294 |
+
return {
|
| 295 |
+
"split": split,
|
| 296 |
+
"status": "skipped",
|
| 297 |
+
"duration_seconds": 0.0,
|
| 298 |
+
"log": str(log_path),
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
persist_path = LOCOMO_ROOT / "memory_store" / memory_store_name(
|
| 302 |
+
run_prefix,
|
| 303 |
+
split,
|
| 304 |
+
)
|
| 305 |
+
if strategy == "semantic" and not persist_path.exists():
|
| 306 |
+
raise RuntimeError(
|
| 307 |
+
f"Split {split}: semantic retrieval requires the prompt-built "
|
| 308 |
+
f"memory store at {persist_path}"
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
+
attempts = max(1, retries)
|
| 312 |
+
for attempt in range(1, attempts + 1):
|
| 313 |
+
paths = split_paths(run_prefix, strategy, split)
|
| 314 |
+
force_rebuild = (
|
| 315 |
+
strategy == "prompt" and not paths["build_timing"].is_file()
|
| 316 |
+
)
|
| 317 |
+
command = build_split_command(
|
| 318 |
+
python=python,
|
| 319 |
+
run_prefix=run_prefix,
|
| 320 |
+
strategy=strategy,
|
| 321 |
+
split=split,
|
| 322 |
+
force_rebuild=force_rebuild,
|
| 323 |
+
)
|
| 324 |
+
if dry_run:
|
| 325 |
+
return {
|
| 326 |
+
"split": split,
|
| 327 |
+
"status": "dry-run",
|
| 328 |
+
"duration_seconds": 0.0,
|
| 329 |
+
"command": command,
|
| 330 |
+
"log": str(log_path),
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
with log_path.open("a") as log:
|
| 334 |
+
log.write(
|
| 335 |
+
f"\n=== attempt {attempt}/{attempts} "
|
| 336 |
+
f"{datetime.now(timezone.utc).isoformat()} ===\n"
|
| 337 |
+
)
|
| 338 |
+
log.flush()
|
| 339 |
+
completed = subprocess.run(
|
| 340 |
+
command,
|
| 341 |
+
cwd=LOCOMO_ROOT,
|
| 342 |
+
env={**os.environ, "TOKENIZERS_PARALLELISM": "false"},
|
| 343 |
+
stdout=log,
|
| 344 |
+
stderr=subprocess.STDOUT,
|
| 345 |
+
text=True,
|
| 346 |
+
check=False,
|
| 347 |
+
)
|
| 348 |
+
|
| 349 |
+
if completed.returncode == 0 and completed_split(
|
| 350 |
+
run_prefix,
|
| 351 |
+
strategy,
|
| 352 |
+
split,
|
| 353 |
+
expected_count,
|
| 354 |
+
):
|
| 355 |
+
return {
|
| 356 |
+
"split": split,
|
| 357 |
+
"status": "completed",
|
| 358 |
+
"duration_seconds": round(time.monotonic() - started, 3),
|
| 359 |
+
"attempts": attempt,
|
| 360 |
+
"log": str(log_path),
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
if attempt == attempts:
|
| 364 |
+
raise RuntimeError(
|
| 365 |
+
f"Split {split} failed after {attempts} attempts "
|
| 366 |
+
f"(exit {completed.returncode}).\n{tail(log_path)}"
|
| 367 |
+
)
|
| 368 |
+
|
| 369 |
+
raise AssertionError("unreachable")
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
def mean(values: list[float]) -> float:
|
| 373 |
+
return round(fmean(values), 4)
|
| 374 |
+
|
| 375 |
+
|
| 376 |
+
def aggregate_strategy(
|
| 377 |
+
*,
|
| 378 |
+
run_prefix: str,
|
| 379 |
+
strategy: str,
|
| 380 |
+
splits: list[int],
|
| 381 |
+
expected_counts: list[int],
|
| 382 |
+
) -> dict[str, Any]:
|
| 383 |
+
aggregate_eval: dict[str, list[dict[str, Any]]] = {}
|
| 384 |
+
aggregate_output: dict[str, list[dict[str, Any]]] = {}
|
| 385 |
+
all_eval_items: list[dict[str, Any]] = []
|
| 386 |
+
|
| 387 |
+
for split in splits:
|
| 388 |
+
paths = split_paths(run_prefix, strategy, split)
|
| 389 |
+
expected = expected_counts[split - 1]
|
| 390 |
+
if not completed_split(run_prefix, strategy, split, expected):
|
| 391 |
+
raise RuntimeError(f"Split {split} is incomplete and cannot be aggregated")
|
| 392 |
+
|
| 393 |
+
with paths["eval"].open() as f:
|
| 394 |
+
split_eval = json.load(f)
|
| 395 |
+
with paths["output"].open() as f:
|
| 396 |
+
split_output = json.load(f)
|
| 397 |
+
|
| 398 |
+
eval_items = [
|
| 399 |
+
item for items in split_eval.values() for item in items
|
| 400 |
+
]
|
| 401 |
+
output_items = [
|
| 402 |
+
item for items in split_output.values() for item in items
|
| 403 |
+
]
|
| 404 |
+
aggregate_eval[str(split - 1)] = eval_items
|
| 405 |
+
aggregate_output[str(split - 1)] = output_items
|
| 406 |
+
all_eval_items.extend(eval_items)
|
| 407 |
+
|
| 408 |
+
by_category: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
| 409 |
+
for item in all_eval_items:
|
| 410 |
+
by_category[str(item["category"])].append(item)
|
| 411 |
+
|
| 412 |
+
metrics = ("bleu_score", "f1_score", "llm_score")
|
| 413 |
+
per_category = {
|
| 414 |
+
metric: {
|
| 415 |
+
category: mean([float(item[metric]) for item in items])
|
| 416 |
+
for category, items in sorted(by_category.items())
|
| 417 |
+
}
|
| 418 |
+
for metric in metrics
|
| 419 |
+
}
|
| 420 |
+
per_category["count"] = {
|
| 421 |
+
category: len(items)
|
| 422 |
+
for category, items in sorted(by_category.items())
|
| 423 |
+
}
|
| 424 |
+
overall = {
|
| 425 |
+
metric: mean([float(item[metric]) for item in all_eval_items])
|
| 426 |
+
for metric in metrics
|
| 427 |
+
}
|
| 428 |
+
scores = {
|
| 429 |
+
"mean_scores_per_category": per_category,
|
| 430 |
+
"overall_mean_scores": overall,
|
| 431 |
+
"summary": {
|
| 432 |
+
"total_questions": len(all_eval_items),
|
| 433 |
+
"categories_evaluated": sorted(int(key) for key in by_category),
|
| 434 |
+
"evaluation_timestamp": datetime.now(timezone.utc).isoformat(),
|
| 435 |
+
"conversation_splits": splits,
|
| 436 |
+
},
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
root = campaign_root(run_prefix)
|
| 440 |
+
root.mkdir(parents=True, exist_ok=True)
|
| 441 |
+
for name, payload in (
|
| 442 |
+
(f"{strategy}_output.json", aggregate_output),
|
| 443 |
+
(f"{strategy}_eval.json", aggregate_eval),
|
| 444 |
+
(f"{strategy}_scores.json", scores),
|
| 445 |
+
):
|
| 446 |
+
with (root / name).open("w") as f:
|
| 447 |
+
json.dump(payload, f, indent=2)
|
| 448 |
+
return scores
|
| 449 |
+
|
| 450 |
+
|
| 451 |
+
def write_manifest(
|
| 452 |
+
*,
|
| 453 |
+
run_prefix: str,
|
| 454 |
+
strategy: str,
|
| 455 |
+
splits: list[int],
|
| 456 |
+
summary: dict[str, Any],
|
| 457 |
+
scores: dict[str, Any] | None,
|
| 458 |
+
) -> Path:
|
| 459 |
+
selected_questions = sum(summary["question_counts"][i - 1] for i in splits)
|
| 460 |
+
selected_evaluated = sum(
|
| 461 |
+
summary["evaluated_counts"][i - 1] for i in splits
|
| 462 |
+
)
|
| 463 |
+
manifest = {
|
| 464 |
+
"paper": {
|
| 465 |
+
"title": (
|
| 466 |
+
"Memora: A Harmonic Memory Representation Balancing "
|
| 467 |
+
"Abstraction and Specificity"
|
| 468 |
+
),
|
| 469 |
+
"arxiv_id": "2602.03315",
|
| 470 |
+
"openreview_id": "zSrvkj0ers",
|
| 471 |
+
"official_repository": "https://github.com/microsoft/Memora",
|
| 472 |
+
"official_commit": "dec3f8f2444eace7004fc084abe1be9f3d88270e",
|
| 473 |
+
},
|
| 474 |
+
"campaign": run_prefix,
|
| 475 |
+
"strategy": strategy,
|
| 476 |
+
"splits": splits,
|
| 477 |
+
"full_scale": splits == list(range(1, 11)),
|
| 478 |
+
"dataset": {
|
| 479 |
+
**summary,
|
| 480 |
+
"selected_questions": selected_questions,
|
| 481 |
+
"selected_evaluated_questions": selected_evaluated,
|
| 482 |
+
"sha256": file_sha256(DATA_PATH),
|
| 483 |
+
},
|
| 484 |
+
"implementation": {
|
| 485 |
+
"llm_provider": "Z.AI OpenAI-compatible API",
|
| 486 |
+
"llm_model": os.getenv("GLM_MODEL", "glm-4.5-flash"),
|
| 487 |
+
"embedding_provider": "local FastEmbed",
|
| 488 |
+
"embedding_model": "BAAI/bge-small-en-v1.5",
|
| 489 |
+
"retrieval_strategy": strategy,
|
| 490 |
+
"top_k": 30,
|
| 491 |
+
"hybrid_search": "BM25",
|
| 492 |
+
"segmentation": True,
|
| 493 |
+
"episodic_memory": "raw segments plus factual memories",
|
| 494 |
+
"cue_index": True,
|
| 495 |
+
"prompted_policy_max_steps": 4 if strategy == "prompt" else None,
|
| 496 |
+
},
|
| 497 |
+
"protocol_deviations": [
|
| 498 |
+
(
|
| 499 |
+
"GPT-4.1-mini was replaced by GLM because the original model "
|
| 500 |
+
"endpoint was unavailable."
|
| 501 |
+
),
|
| 502 |
+
(
|
| 503 |
+
"The repository-default OpenAI embedding endpoint was replaced "
|
| 504 |
+
"by local BAAI/bge-small-en-v1.5 embeddings."
|
| 505 |
+
),
|
| 506 |
+
(
|
| 507 |
+
"The GLM endpoint does not accept the paper's seed parameter; "
|
| 508 |
+
"temperature was fixed at 0 and thinking was disabled."
|
| 509 |
+
),
|
| 510 |
+
(
|
| 511 |
+
"Conversations were executed as ten isolated processes and "
|
| 512 |
+
"aggregated; Memora user namespaces are conversation-local."
|
| 513 |
+
),
|
| 514 |
+
],
|
| 515 |
+
"paper_targets": PAPER_TARGETS,
|
| 516 |
+
"scores": scores,
|
| 517 |
+
"generated_at": datetime.now(timezone.utc).isoformat(),
|
| 518 |
+
}
|
| 519 |
+
path = campaign_root(run_prefix) / f"{strategy}_manifest.json"
|
| 520 |
+
with path.open("w") as f:
|
| 521 |
+
json.dump(manifest, f, indent=2)
|
| 522 |
+
return path
|
| 523 |
+
|
| 524 |
+
|
| 525 |
+
def initialize_trackio(args: argparse.Namespace, splits: list[int]):
|
| 526 |
+
if not args.trackio:
|
| 527 |
+
return None
|
| 528 |
+
try:
|
| 529 |
+
import trackio
|
| 530 |
+
except ImportError as exc:
|
| 531 |
+
raise RuntimeError(
|
| 532 |
+
"Trackio is not installed in the orchestrator environment. "
|
| 533 |
+
"Install requirements-glm.txt first."
|
| 534 |
+
) from exc
|
| 535 |
+
|
| 536 |
+
trackio.init(
|
| 537 |
+
project=DEFAULT_PROJECT,
|
| 538 |
+
name=f"{args.run_prefix}-{args.strategy}",
|
| 539 |
+
group=args.run_prefix,
|
| 540 |
+
config={
|
| 541 |
+
"paper_arxiv": "2602.03315",
|
| 542 |
+
"strategy": args.strategy,
|
| 543 |
+
"model": os.getenv("GLM_MODEL", "glm-4.5-flash"),
|
| 544 |
+
"embedding_model": "BAAI/bge-small-en-v1.5",
|
| 545 |
+
"splits": ",".join(str(split) for split in splits),
|
| 546 |
+
"full_scale": splits == list(range(1, 11)),
|
| 547 |
+
},
|
| 548 |
+
)
|
| 549 |
+
return trackio
|
| 550 |
+
|
| 551 |
+
|
| 552 |
+
def main() -> int:
|
| 553 |
+
args = parse_args()
|
| 554 |
+
splits = validate_splits(args.splits)
|
| 555 |
+
if args.max_parallel < 1:
|
| 556 |
+
raise ValueError("--max-parallel must be at least 1")
|
| 557 |
+
if args.env_file:
|
| 558 |
+
load_dotenv(args.env_file, override=False)
|
| 559 |
+
if not args.dry_run and not os.getenv("GLM_API_KEY"):
|
| 560 |
+
raise RuntimeError("GLM_API_KEY is required")
|
| 561 |
+
if not args.python.is_file():
|
| 562 |
+
raise FileNotFoundError(f"Python interpreter not found: {args.python}")
|
| 563 |
+
|
| 564 |
+
data = load_dataset()
|
| 565 |
+
summary = dataset_summary(data)
|
| 566 |
+
root = campaign_root(args.run_prefix)
|
| 567 |
+
root.mkdir(parents=True, exist_ok=True)
|
| 568 |
+
print(
|
| 569 |
+
json.dumps(
|
| 570 |
+
{
|
| 571 |
+
"campaign": args.run_prefix,
|
| 572 |
+
"strategy": args.strategy,
|
| 573 |
+
"splits": splits,
|
| 574 |
+
"selected_questions": sum(
|
| 575 |
+
summary["question_counts"][i - 1] for i in splits
|
| 576 |
+
),
|
| 577 |
+
"selected_evaluated_questions": sum(
|
| 578 |
+
summary["evaluated_counts"][i - 1] for i in splits
|
| 579 |
+
),
|
| 580 |
+
"max_parallel": args.max_parallel,
|
| 581 |
+
}
|
| 582 |
+
),
|
| 583 |
+
flush=True,
|
| 584 |
+
)
|
| 585 |
+
|
| 586 |
+
tracker = initialize_trackio(args, splits)
|
| 587 |
+
failures: list[str] = []
|
| 588 |
+
results: list[dict[str, Any]] = []
|
| 589 |
+
with concurrent.futures.ThreadPoolExecutor(
|
| 590 |
+
max_workers=args.max_parallel
|
| 591 |
+
) as executor:
|
| 592 |
+
future_to_split = {
|
| 593 |
+
executor.submit(
|
| 594 |
+
run_split,
|
| 595 |
+
python=args.python,
|
| 596 |
+
run_prefix=args.run_prefix,
|
| 597 |
+
strategy=args.strategy,
|
| 598 |
+
split=split,
|
| 599 |
+
expected_count=summary["evaluated_counts"][split - 1],
|
| 600 |
+
retries=args.retries,
|
| 601 |
+
dry_run=args.dry_run,
|
| 602 |
+
): split
|
| 603 |
+
for split in splits
|
| 604 |
+
}
|
| 605 |
+
for future in concurrent.futures.as_completed(future_to_split):
|
| 606 |
+
split = future_to_split[future]
|
| 607 |
+
try:
|
| 608 |
+
result = future.result()
|
| 609 |
+
results.append(result)
|
| 610 |
+
print(json.dumps(result), flush=True)
|
| 611 |
+
if tracker and result["status"] != "dry-run":
|
| 612 |
+
tracker.log(
|
| 613 |
+
{
|
| 614 |
+
"split": split,
|
| 615 |
+
"split_success": 1,
|
| 616 |
+
"split_duration_seconds": result[
|
| 617 |
+
"duration_seconds"
|
| 618 |
+
],
|
| 619 |
+
}
|
| 620 |
+
)
|
| 621 |
+
except Exception as exc:
|
| 622 |
+
message = f"split {split}: {exc}"
|
| 623 |
+
failures.append(message)
|
| 624 |
+
print(json.dumps({"split": split, "error": str(exc)}), flush=True)
|
| 625 |
+
if tracker:
|
| 626 |
+
tracker.log({"split": split, "split_success": 0})
|
| 627 |
+
|
| 628 |
+
if failures:
|
| 629 |
+
write_manifest(
|
| 630 |
+
run_prefix=args.run_prefix,
|
| 631 |
+
strategy=args.strategy,
|
| 632 |
+
splits=splits,
|
| 633 |
+
summary=summary,
|
| 634 |
+
scores=None,
|
| 635 |
+
)
|
| 636 |
+
if tracker:
|
| 637 |
+
tracker.finish()
|
| 638 |
+
raise RuntimeError("\n\n".join(failures))
|
| 639 |
+
|
| 640 |
+
if args.dry_run:
|
| 641 |
+
write_manifest(
|
| 642 |
+
run_prefix=args.run_prefix,
|
| 643 |
+
strategy=args.strategy,
|
| 644 |
+
splits=splits,
|
| 645 |
+
summary=summary,
|
| 646 |
+
scores=None,
|
| 647 |
+
)
|
| 648 |
+
if tracker:
|
| 649 |
+
tracker.finish()
|
| 650 |
+
return 0
|
| 651 |
+
|
| 652 |
+
scores = aggregate_strategy(
|
| 653 |
+
run_prefix=args.run_prefix,
|
| 654 |
+
strategy=args.strategy,
|
| 655 |
+
splits=splits,
|
| 656 |
+
expected_counts=summary["evaluated_counts"],
|
| 657 |
+
)
|
| 658 |
+
manifest_path = write_manifest(
|
| 659 |
+
run_prefix=args.run_prefix,
|
| 660 |
+
strategy=args.strategy,
|
| 661 |
+
splits=splits,
|
| 662 |
+
summary=summary,
|
| 663 |
+
scores=scores,
|
| 664 |
+
)
|
| 665 |
+
if tracker:
|
| 666 |
+
tracker.log(
|
| 667 |
+
{
|
| 668 |
+
"overall_llm_score": scores["overall_mean_scores"]["llm_score"],
|
| 669 |
+
"overall_f1_score": scores["overall_mean_scores"]["f1_score"],
|
| 670 |
+
"overall_bleu_score": scores["overall_mean_scores"]["bleu_score"],
|
| 671 |
+
"evaluated_questions": scores["summary"]["total_questions"],
|
| 672 |
+
"paper_llm_target": PAPER_TARGETS[args.strategy]["llm_score"],
|
| 673 |
+
"delta_from_paper": (
|
| 674 |
+
scores["overall_mean_scores"]["llm_score"]
|
| 675 |
+
- PAPER_TARGETS[args.strategy]["llm_score"]
|
| 676 |
+
),
|
| 677 |
+
}
|
| 678 |
+
)
|
| 679 |
+
tracker.finish()
|
| 680 |
+
print(
|
| 681 |
+
json.dumps(
|
| 682 |
+
{
|
| 683 |
+
"status": "complete",
|
| 684 |
+
"scores": scores["overall_mean_scores"],
|
| 685 |
+
"evaluated_questions": scores["summary"]["total_questions"],
|
| 686 |
+
"manifest": str(manifest_path),
|
| 687 |
+
}
|
| 688 |
+
),
|
| 689 |
+
flush=True,
|
| 690 |
+
)
|
| 691 |
+
return 0
|
| 692 |
+
|
| 693 |
+
|
| 694 |
+
if __name__ == "__main__":
|
| 695 |
+
raise SystemExit(main())
|
| 696 |
+
|
| 697 |
+
````
|
| 698 |
+
|
| 699 |
+
|
| 700 |
+
````output
|
| 701 |
+
{"campaign": "glm52-full", "strategy": "prompt", "splits": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], "selected_questions": 1986, "selected_evaluated_questions": 1540, "max_parallel": 3}
|
| 702 |
+
* Trackio project initialized: memora-icml2026-repro
|
| 703 |
+
* Trackio metrics logged to: /Users/conanssam-m4/.cache/huggingface/trackio
|
| 704 |
+
* View dashboard by running in your terminal:
|
| 705 |
+
[1m[38;5;208mtrackio show --project "memora-icml2026-repro"[0m
|
| 706 |
+
* or by running in Python: trackio.show(project="memora-icml2026-repro")
|
| 707 |
+
* Created new run: glm52-full-prompt
|
| 708 |
+
{"split": 3, "error": "Split 3 failed after 3 attempts (exit 1).\n\n=== attempt 1/3 2026-07-24T05:17:55.438755+00:00 ===\nTraceback (most recent call last):\n File \"/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py\", line 23, in <module>\n from providers.memora.add import MemoraADD\n File \"/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py\", line 13, in <module>\n from memora.memora_client import MemoraClient\nModuleNotFoundError: No module named 'memora'\n\n=== attempt 2/3 2026-07-24T05:17:55.608328+00:00 ===\nTraceback (most recent call last):\n File \"/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py\", line 23, in <module>\n from providers.memora.add import MemoraADD\n File \"/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py\", line 13, in <module>\n from memora.memora_client import MemoraClient\nModuleNotFoundError: No module named 'memora'\n\n=== attempt 3/3 2026-07-24T05:17:55.704686+00:00 ===\nTraceback (most recent call last):\n File \"/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py\", line 23, in <module>\n from providers.memora.add import MemoraADD\n File \"/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py\", line 13, in <module>\n from memora.memora_client import MemoraClient\nModuleNotFoundError: No module named 'memora'"}
|
| 709 |
+
{"split": 1, "error": "Split 1 failed after 3 attempts (exit 1).\n\n=== attempt 1/3 2026-07-24T05:17:55.438713+00:00 ===\
|
| 710 |
+
... [7444 chars elided] ...
|
| 711 |
+
in <module>\n from memora.memora_client import MemoraClient\nModuleNotFoundError: No module named 'memora'\n\n=== attempt 3/3 2026-07-24T05:17:56.267074+00:00 ===\nTraceback (most recent call last):\n File \"/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py\", line 23, in <module>\n from providers.memora.add import MemoraADD\n File \"/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py\", line 13, in <module>\n from memora.memora_client import MemoraClient\nModuleNotFoundError: No module named 'memora'"}
|
| 712 |
+
{"split": 7, "error": "Split 7 failed after 3 attempts (exit 1).\n\n=== attempt 1/3 2026-07-24T05:17:56.084924+00:00 ===\nTraceback (most recent call last):\n File \"/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py\", line 23, in <module>\n from providers.memora.add import MemoraADD\n File \"/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py\", line 13, in <module>\n from memora.memora_client import MemoraClient\nModuleNotFoundError: No module named 'memora'\n\n=== attempt 2/3 2026-07-24T05:17:56.175829+00:00 ===\nTraceback (most recent call last):\n File \"/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py\", line 23, in <module>\n from providers.memora.add import MemoraADD\n File \"/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py\", line 13, in <module>\n from memora.memora_client import MemoraClient\nModuleNotFoundError: No module named 'memora'\n\n=== attempt 3/3 2026-07-24T05:17:56.269298+00:00 ===\nTraceback (most recent call last):\n File \"/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py\", line 23, in <module>\n from providers.memora.add import MemoraADD\n File \"/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py\", line 13, in <module>\n from memora.memora_client import MemoraClient\nModuleNotFoundError: No module named 'memora'"}
|
| 713 |
+
{"split": 9, "error": "Split 9 failed after 3 attempts (exit 1).\n\n=== attempt 1/3 2026-07-24T05:17:56.084946+00:00 ===\nTraceback (most recent call last):\n File \"/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py\", line 23, in <module>\n from providers.memora.add import MemoraADD\n File \"/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py\", line 13, in <module>\n from memora.memora_client import MemoraClient\nModuleNotFoundError: No module named 'memora'\n\n=== attempt 2/3 2026-07-24T05:17:56.178439+00:00 ===\nTraceback (most recent call last):\n File \"/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py\", line 23, in <module>\n from providers.memora.add import MemoraADD\n File \"/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py\", line 13, in <module>\n from memora.memora_client import MemoraClient\nModuleNotFoundError: No module named 'memora'\n\n=== attempt 3/3 2026-07-24T05:17:56.267040+00:00 ===\nTraceback (most recent call last):\n File \"/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py\", line 23, in <module>\n from providers.memora.add import MemoraADD\n File \"/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py\", line 13, in <module>\n from memora.memora_client import MemoraClient\nModuleNotFoundError: No module named 'memora'"}
|
| 714 |
+
{"split": 10, "error": "Split 10 failed after 3 attempts (exit 1).\n\n=== attempt 1/3 2026-07-24T05:17:56.358486+00:00 ===\nTraceback (most recent call last):\n File \"/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py\", line 23, in <module>\n from providers.memora.add import MemoraADD\n File \"/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py\", line 13, in <module>\n from memora.memora_client import MemoraClient\nModuleNotFoundError: No module named 'memora'\n\n=== attempt 2/3 2026-07-24T05:17:56.439604+00:00 ===\nTraceback (most recent call last):\n File \"/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py\", line 23, in <module>\n from providers.memora.add import MemoraADD\n File \"/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py\", line 13, in <module>\n from memora.memora_client import MemoraClient\nModuleNotFoundError: No module named 'memora'\n\n=== attempt 3/3 2026-07-24T05:17:56.530859+00:00 ===\nTraceback (most recent call last):\n File \"/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py\", line 23, in <module>\n from providers.memora.add import MemoraADD\n File \"/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py\", line 13, in <module>\n from memora.memora_client import MemoraClient\nModuleNotFoundError: No module named 'memora'"}
|
| 715 |
+
* Run finished. Uploading logs to Trackio (please wait...)
|
| 716 |
+
Traceback (most recent call last):
|
| 717 |
+
File "/Users/conanssam-m4/memora-glm/scripts/run_locomo_reproduction.py", line 666, in <module>
|
| 718 |
+
raise SystemExit(main())
|
| 719 |
+
^^^^^^
|
| 720 |
+
File "/Users/conanssam-m4/memora-glm/scripts/run_locomo_reproduction.py", line 609, in main
|
| 721 |
+
raise RuntimeError("\n\n".join(failures))
|
| 722 |
+
RuntimeError: split 3: Split 3 failed after 3 attempts (exit 1).
|
| 723 |
+
|
| 724 |
+
=== attempt 1/3 2026-07-24T05:17:55.438755+00:00 ===
|
| 725 |
+
Traceback (most recent call last):
|
| 726 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py", line 23, in <module>
|
| 727 |
+
from providers.memora.add import MemoraADD
|
| 728 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py", line 13, in <module>
|
| 729 |
+
from memora.memora_client import MemoraClient
|
| 730 |
+
ModuleNotFoundError: No module named 'memora'
|
| 731 |
+
|
| 732 |
+
=== attempt 2/3 2026-07-24T05:17:55.608328+00:00 ===
|
| 733 |
+
Traceback (most recent call last):
|
| 734 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py", line 23, in <module>
|
| 735 |
+
from providers.memora.add import MemoraADD
|
| 736 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py", line 13, in <module>
|
| 737 |
+
from memora.memora_client import MemoraClient
|
| 738 |
+
ModuleNotFoundError: No module named 'memora'
|
| 739 |
+
|
| 740 |
+
=== attempt 3/3 2026-07-24T05:17:55.704686+00:00 ===
|
| 741 |
+
Traceback (most recent call last):
|
| 742 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py", line 23, in <module>
|
| 743 |
+
from providers.memora.add import MemoraADD
|
| 744 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py", line 13, in <module>
|
| 745 |
+
from memora.memora_client import MemoraClient
|
| 746 |
+
ModuleNotFoundError: No module named 'memora'
|
| 747 |
+
|
| 748 |
+
split 1: Split 1 failed after 3 attempts (exit 1).
|
| 749 |
+
|
| 750 |
+
=== attempt 1/3 2026-07-24T05:17:55.438713+00:00 ===
|
| 751 |
+
Traceback (most recent call last):
|
| 752 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py", line 23, in <module>
|
| 753 |
+
from providers.memora.add import MemoraADD
|
| 754 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py", line 13, in <module>
|
| 755 |
+
from memora.memora_client import MemoraClient
|
| 756 |
+
ModuleNotFoundError: No module named 'memora'
|
| 757 |
+
|
| 758 |
+
=== attempt 2/3 2026-07-24T05:17:55.608280+00:00 ===
|
| 759 |
+
Traceback (most recent call last):
|
| 760 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py", line 23, in <module>
|
| 761 |
+
from providers.memora.add import MemoraADD
|
| 762 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py", line 13, in <module>
|
| 763 |
+
from memora.memora_client import MemoraClient
|
| 764 |
+
ModuleNotFoundError: No module named 'memora'
|
| 765 |
+
|
| 766 |
+
=== attempt 3/3 2026-07-24T05:17:55.702021+00:00 ===
|
| 767 |
+
Traceback (most recent call last):
|
| 768 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py", line 23, in <module>
|
| 769 |
+
from providers.memora.add import MemoraADD
|
| 770 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py", line 13, in <module>
|
| 771 |
+
from memora.memora_client import MemoraClient
|
| 772 |
+
ModuleNotFoundError: No module named 'memora'
|
| 773 |
+
|
| 774 |
+
split 2: Split 2 failed after 3 attempts (exit 1).
|
| 775 |
+
|
| 776 |
+
=== attempt 1/3 2026-07-24T05:17:55.438778+00:00 ===
|
| 777 |
+
Traceback (most recent call last):
|
| 778 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py", line 23, in <module>
|
| 779 |
+
from providers.memora.add import MemoraADD
|
| 780 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py", line 13, in <module>
|
| 781 |
+
from memora.memora_client import MemoraClient
|
| 782 |
+
ModuleNotFoundError: No module named 'memora'
|
| 783 |
+
|
| 784 |
+
=== attempt 2/3 2026-07-24T05:17:55.606921+00:00 ===
|
| 785 |
+
Traceback (most recent call last):
|
| 786 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py", line 23, in <module>
|
| 787 |
+
from providers.memora.add import MemoraADD
|
| 788 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py", line 13, in <module>
|
| 789 |
+
from memora.memora_client import MemoraClient
|
| 790 |
+
ModuleNotFoundError: No module named 'memora'
|
| 791 |
+
|
| 792 |
+
=== attempt 3/3 2026-07-24T05:17:55.703130+00:00 ===
|
| 793 |
+
Traceback (most recent call last):
|
| 794 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py", line 23, in <module>
|
| 795 |
+
from providers.memora.add import MemoraADD
|
| 796 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py", line 13, in <module>
|
| 797 |
+
from memora.memora_client import MemoraClient
|
| 798 |
+
ModuleNotFoundError: No module named 'memora'
|
| 799 |
+
|
| 800 |
+
split 4: Split 4 failed after 3 attempts (exit 1).
|
| 801 |
+
|
| 802 |
+
=== attempt 1/3 2026-07-24T05:17:55.806544+00:00 ===
|
| 803 |
+
Traceback (most recent call last):
|
| 804 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py", line 23, in <module>
|
| 805 |
+
from providers.memora.add import MemoraADD
|
| 806 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py", line 13, in <module>
|
| 807 |
+
from memora.memora_client import MemoraClient
|
| 808 |
+
ModuleNotFoundError: No module named 'memora'
|
| 809 |
+
|
| 810 |
+
=== attempt 2/3 2026-07-24T05:17:55.900859+00:00 ===
|
| 811 |
+
Traceback (most recent call last):
|
| 812 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py", line 23, in <module>
|
| 813 |
+
from providers.memora.add import MemoraADD
|
| 814 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py", line 13, in <module>
|
| 815 |
+
from memora.memora_client import MemoraClient
|
| 816 |
+
ModuleNotFoundError: No module named 'memora'
|
| 817 |
+
|
| 818 |
+
=== attempt 3/3 2026-07-24T05:17:55.995169+00:00 ===
|
| 819 |
+
Traceback (most recent call last):
|
| 820 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py", line 23, in <module>
|
| 821 |
+
from providers.memora.add import MemoraADD
|
| 822 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py", line 13, in <module>
|
| 823 |
+
from memora.memora_client import MemoraClient
|
| 824 |
+
ModuleNotFoundError: No module named 'memora'
|
| 825 |
+
|
| 826 |
+
split 6: Split 6 failed after 3 attempts (exit 1).
|
| 827 |
+
|
| 828 |
+
=== attempt 1/3 2026-07-24T05:17:55.808622+00:00 ===
|
| 829 |
+
Traceback (most recent call last):
|
| 830 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py", line 23, in <module>
|
| 831 |
+
from providers.memora.add import MemoraADD
|
| 832 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py", line 13, in <module>
|
| 833 |
+
from memora.memora_client import MemoraClient
|
| 834 |
+
ModuleNotFoundError: No module named 'memora'
|
| 835 |
+
|
| 836 |
+
=== attempt 2/3 2026-07-24T05:17:55.900824+00:00 ===
|
| 837 |
+
Traceback (most recent call last):
|
| 838 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py", line 23, in <module>
|
| 839 |
+
from providers.memora.add import MemoraADD
|
| 840 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py", line 13, in <module>
|
| 841 |
+
from memora.memora_client import MemoraClient
|
| 842 |
+
ModuleNotFoundError: No module named 'memora'
|
| 843 |
+
|
| 844 |
+
=== attempt 3/3 2026-07-24T05:17:55.993034+00:00 ===
|
| 845 |
+
Traceback (most recent call last):
|
| 846 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py", line 23, in <module>
|
| 847 |
+
from providers.memora.add import MemoraADD
|
| 848 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py", line 13, in <module>
|
| 849 |
+
from memora.memora_client import MemoraClient
|
| 850 |
+
ModuleNotFoundError: No module named 'memora'
|
| 851 |
+
|
| 852 |
+
split 5: Split 5 failed after 3 attempts (exit 1).
|
| 853 |
+
|
| 854 |
+
=== attempt 1/3 2026-07-24T05:17:55.808656+00:00 ===
|
| 855 |
+
Traceback (most recent call last):
|
| 856 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py", line 23, in <module>
|
| 857 |
+
from providers.memora.add import MemoraADD
|
| 858 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py", line 13, in <module>
|
| 859 |
+
from memora.memora_client import MemoraClient
|
| 860 |
+
ModuleNotFoundError: No module named 'memora'
|
| 861 |
+
|
| 862 |
+
=== attempt 2/3 2026-07-24T05:17:55.903523+00:00 ===
|
| 863 |
+
Traceback (most recent call last):
|
| 864 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py", line 23, in <module>
|
| 865 |
+
from providers.memora.add import MemoraADD
|
| 866 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py", line 13, in <module>
|
| 867 |
+
from memora.memora_client import MemoraClient
|
| 868 |
+
ModuleNotFoundError: No module named 'memora'
|
| 869 |
+
|
| 870 |
+
=== attempt 3/3 2026-07-24T05:17:55.993003+00:00 ===
|
| 871 |
+
Traceback (most recent call last):
|
| 872 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py", line 23, in <module>
|
| 873 |
+
from providers.memora.add import MemoraADD
|
| 874 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py", line 13, in <module>
|
| 875 |
+
from memora.memora_client import MemoraClient
|
| 876 |
+
ModuleNotFoundError: No module named 'memora'
|
| 877 |
+
|
| 878 |
+
split 8: Split 8 failed after 3 attempts (exit 1).
|
| 879 |
+
|
| 880 |
+
=== attempt 1/3 2026-07-24T05:17:56.084657+00:00 ===
|
| 881 |
+
Traceback (most recent call last):
|
| 882 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py", line 23, in <module>
|
| 883 |
+
from providers.memora.add import MemoraADD
|
| 884 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py", line 13, in <module>
|
| 885 |
+
from memora.memora_client import MemoraClient
|
| 886 |
+
ModuleNotFoundError: No module named 'memora'
|
| 887 |
+
|
| 888 |
+
=== attempt 2/3 2026-07-24T05:17:56.175864+00:00 ===
|
| 889 |
+
Traceback (most recent call last):
|
| 890 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py", line 23, in <module>
|
| 891 |
+
from providers.memora.add import MemoraADD
|
| 892 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py", line 13, in <module>
|
| 893 |
+
from memora.memora_client import MemoraClient
|
| 894 |
+
ModuleNotFoundError: No module named 'memora'
|
| 895 |
+
|
| 896 |
+
=== attempt 3/3 2026-07-24T05:17:56.267074+00:00 ===
|
| 897 |
+
Traceback (most recent call last):
|
| 898 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py", line 23, in <module>
|
| 899 |
+
from providers.memora.add import MemoraADD
|
| 900 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py", line 13, in <module>
|
| 901 |
+
from memora.memora_client import MemoraClient
|
| 902 |
+
ModuleNotFoundError: No module named 'memora'
|
| 903 |
+
|
| 904 |
+
split 7: Split 7 failed after 3 attempts (exit 1).
|
| 905 |
+
|
| 906 |
+
=== attempt 1/3 2026-07-24T05:17:56.084924+00:00 ===
|
| 907 |
+
Traceback (most recent call last):
|
| 908 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py", line 23, in <module>
|
| 909 |
+
from providers.memora.add import MemoraADD
|
| 910 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py", line 13, in <module>
|
| 911 |
+
from memora.memora_client import MemoraClient
|
| 912 |
+
ModuleNotFoundError: No module named 'memora'
|
| 913 |
+
|
| 914 |
+
=== attempt 2/3 2026-07-24T05:17:56.175829+00:00 ===
|
| 915 |
+
Traceback (most recent call last):
|
| 916 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py", line 23, in <module>
|
| 917 |
+
from providers.memora.add import MemoraADD
|
| 918 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py", line 13, in <module>
|
| 919 |
+
from memora.memora_client import MemoraClient
|
| 920 |
+
ModuleNotFoundError: No module named 'memora'
|
| 921 |
+
|
| 922 |
+
=== attempt 3/3 2026-07-24T05:17:56.269298+00:00 ===
|
| 923 |
+
Traceback (most recent call last):
|
| 924 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py", line 23, in <module>
|
| 925 |
+
from providers.memora.add import MemoraADD
|
| 926 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py", line 13, in <module>
|
| 927 |
+
from memora.memora_client import MemoraClient
|
| 928 |
+
ModuleNotFoundError: No module named 'memora'
|
| 929 |
+
|
| 930 |
+
split 9: Split 9 failed after 3 attempts (exit 1).
|
| 931 |
+
|
| 932 |
+
=== attempt 1/3 2026-07-24T05:17:56.084946+00:00 ===
|
| 933 |
+
Traceback (most recent call last):
|
| 934 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py", line 23, in <module>
|
| 935 |
+
from providers.memora.add import MemoraADD
|
| 936 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py", line 13, in <module>
|
| 937 |
+
from memora.memora_client import MemoraClient
|
| 938 |
+
ModuleNotFoundError: No module named 'memora'
|
| 939 |
+
|
| 940 |
+
=== attempt 2/3 2026-07-24T05:17:56.178439+00:00 ===
|
| 941 |
+
Traceback (most recent call last):
|
| 942 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py", line 23, in <module>
|
| 943 |
+
from providers.memora.add import MemoraADD
|
| 944 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py", line 13, in <module>
|
| 945 |
+
from memora.memora_client import MemoraClient
|
| 946 |
+
ModuleNotFoundError: No module named 'memora'
|
| 947 |
+
|
| 948 |
+
=== attempt 3/3 2026-07-24T05:17:56.267040+00:00 ===
|
| 949 |
+
Traceback (most recent call last):
|
| 950 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py", line 23, in <module>
|
| 951 |
+
from providers.memora.add import MemoraADD
|
| 952 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py", line 13, in <module>
|
| 953 |
+
from memora.memora_client import MemoraClient
|
| 954 |
+
ModuleNotFoundError: No module named 'memora'
|
| 955 |
+
|
| 956 |
+
split 10: Split 10 failed after 3 attempts (exit 1).
|
| 957 |
+
|
| 958 |
+
=== attempt 1/3 2026-07-24T05:17:56.358486+00:00 ===
|
| 959 |
+
Traceback (most recent call last):
|
| 960 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py", line 23, in <module>
|
| 961 |
+
from providers.memora.add import MemoraADD
|
| 962 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py", line 13, in <module>
|
| 963 |
+
from memora.memora_client import MemoraClient
|
| 964 |
+
ModuleNotFoundError: No module named 'memora'
|
| 965 |
+
|
| 966 |
+
=== attempt 2/3 2026-07-24T05:17:56.439604+00:00 ===
|
| 967 |
+
Traceback (most recent call last):
|
| 968 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py", line 23, in <module>
|
| 969 |
+
from providers.memora.add import MemoraADD
|
| 970 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py", line 13, in <module>
|
| 971 |
+
from memora.memora_client import MemoraClient
|
| 972 |
+
ModuleNotFoundError: No module named 'memora'
|
| 973 |
+
|
| 974 |
+
=== attempt 3/3 2026-07-24T05:17:56.530859+00:00 ===
|
| 975 |
+
Traceback (most recent call last):
|
| 976 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/run_memora.py", line 23, in <module>
|
| 977 |
+
from providers.memora.add import MemoraADD
|
| 978 |
+
File "/Users/conanssam-m4/memora-glm/app/locomo/providers/memora/add.py", line 13, in <module>
|
| 979 |
+
from memora.memora_client import MemoraClient
|
| 980 |
+
ModuleNotFoundError: No module named 'memora'
|
| 981 |
+
|
| 982 |
+
````
|
| 983 |
+
|
| 984 |
+
|
| 985 |
+
---
|
| 986 |
+
<!-- trackio-cell
|
| 987 |
+
{"type": "code", "id": "cell_77cbe73f6342", "created_at": "2026-07-24T11:35:13+00:00", "title": "Run: python run_locomo_reproduction.py (exit 0)", "command": [".venv/bin/python", "scripts/run_locomo_reproduction.py", "--strategy", "prompt", "--run-prefix", "glm52-full", "--max-parallel", "3", "--retries", "3", "--env-file", "/Users/conanssam-m4/.ouroboros/worktrees/studymemory/orch_d223290f8ae3/.env", "--trackio"], "exit_code": 0, "duration_s": 22604.442}
|
| 988 |
+
-->
|
| 989 |
+
````bash
|
| 990 |
+
$ .venv/bin/python scripts/run_locomo_reproduction.py --strategy prompt --run-prefix glm52-full --max-parallel 3 --retries 3 --env-file /Users/conanssam-m4/.ouroboros/worktrees/studymemory/orch_d223290f8ae3/.env --trackio
|
| 991 |
+
````
|
| 992 |
+
|
| 993 |
+
exit 0 · 22604.4s
|
| 994 |
+
|
| 995 |
+
|
| 996 |
+
````python title=run_locomo_reproduction.py
|
| 997 |
+
#!/usr/bin/env python3
|
| 998 |
+
"""Run the full LoCoMo Memora reproduction as restartable conversation splits.
|
| 999 |
+
|
| 1000 |
+
Each LoCoMo conversation has an isolated Memora user namespace. Running the ten
|
| 1001 |
+
conversations in separate processes therefore preserves the official method
|
| 1002 |
+
while avoiding result-directory collisions and making multi-hour API runs
|
| 1003 |
+
restartable.
|
| 1004 |
+
"""
|
| 1005 |
+
|
| 1006 |
+
from __future__ import annotations
|
| 1007 |
+
|
| 1008 |
+
import argparse
|
| 1009 |
+
import concurrent.futures
|
| 1010 |
+
import hashlib
|
| 1011 |
+
import json
|
| 1012 |
+
import os
|
| 1013 |
+
import subprocess
|
| 1014 |
+
import sys
|
| 1015 |
+
import time
|
| 1016 |
+
from collections import defaultdict
|
| 1017 |
+
from datetime import datetime, timezone
|
| 1018 |
+
from pathlib import Path
|
| 1019 |
+
from statistics import fmean
|
| 1020 |
+
from typing import Any
|
| 1021 |
+
|
| 1022 |
+
from dotenv import load_dotenv
|
| 1023 |
+
|
| 1024 |
+
|
| 1025 |
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
| 1026 |
+
LOCOMO_ROOT = REPO_ROOT / "app" / "locomo"
|
| 1027 |
+
DATA_PATH = LOCOMO_ROOT / "data" / "locomo10.json"
|
| 1028 |
+
DEFAULT_PROJECT = "memora-icml2026-repro"
|
| 1029 |
+
PAPER_TARGETS = {
|
| 1030 |
+
"prompt": {"llm_score": 0.863, "f1_score": 0.553, "bleu_score": 0.466},
|
| 1031 |
+
"semantic": {"llm_score": 0.849, "f1_score": 0.552, "bleu_score": 0.464},
|
| 1032 |
+
"full_context": {"llm_score": 0.825},
|
| 1033 |
+
}
|
| 1034 |
+
|
| 1035 |
+
|
| 1036 |
+
def parse_args() -> argparse.Namespace:
|
| 1037 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 1038 |
+
parser.add_argument(
|
| 1039 |
+
"--strategy",
|
| 1040 |
+
choices=("prompt", "semantic"),
|
| 1041 |
+
required=True,
|
| 1042 |
+
help="Memora retrieval strategy to reproduce.",
|
| 1043 |
+
)
|
| 1044 |
+
parser.add_argument(
|
| 1045 |
+
"--run-prefix",
|
| 1046 |
+
default="glm52-full",
|
| 1047 |
+
help="Stable campaign name used for result and memory directories.",
|
| 1048 |
+
)
|
| 1049 |
+
parser.add_argument(
|
| 1050 |
+
"--splits",
|
| 1051 |
+
nargs="+",
|
| 1052 |
+
type=int,
|
| 1053 |
+
default=list(range(1, 11)),
|
| 1054 |
+
help="One-based LoCoMo conversation indices (default: all 1..10).",
|
| 1055 |
+
)
|
| 1056 |
+
parser.add_argument(
|
| 1057 |
+
"--max-parallel",
|
| 1058 |
+
type=int,
|
| 1059 |
+
default=3,
|
| 1060 |
+
help="Maximum concurrent GLM-backed split processes.",
|
| 1061 |
+
)
|
| 1062 |
+
parser.add_argument(
|
| 1063 |
+
"--retries",
|
| 1064 |
+
type=int,
|
| 1065 |
+
default=2,
|
| 1066 |
+
help="Process-level attempts per split.",
|
| 1067 |
+
)
|
| 1068 |
+
parser.add_argument(
|
| 1069 |
+
"--env-file",
|
| 1070 |
+
type=Path,
|
| 1071 |
+
help="Optional dotenv file containing GLM_API_KEY and GLM_MODEL.",
|
| 1072 |
+
)
|
| 1073 |
+
parser.add_argument(
|
| 1074 |
+
"--python",
|
| 1075 |
+
type=Path,
|
| 1076 |
+
default=REPO_ROOT / ".venv" / "bin" / "python",
|
| 1077 |
+
help="Python interpreter for split processes.",
|
| 1078 |
+
)
|
| 1079 |
+
parser.add_argument(
|
| 1080 |
+
"--trackio",
|
| 1081 |
+
action="store_true",
|
| 1082 |
+
help="Log split completion and aggregate metrics to Trackio.",
|
| 1083 |
+
)
|
| 1084 |
+
parser.add_argument(
|
| 1085 |
+
"--dry-run",
|
| 1086 |
+
action="store_true",
|
| 1087 |
+
help="Print commands without running the experiment.",
|
| 1088 |
+
)
|
| 1089 |
+
return parser.parse_args()
|
| 1090 |
+
|
| 1091 |
+
|
| 1092 |
+
def validate_splits(splits: list[int]) -> list[int]:
|
| 1093 |
+
normalized = sorted(set(splits))
|
| 1094 |
+
if not normalized or normalized[0] < 1 or normalized[-1] > 10:
|
| 1095 |
+
raise ValueError("--splits must contain one-based indices between 1 and 10")
|
| 1096 |
+
return normalized
|
| 1097 |
+
|
| 1098 |
+
|
| 1099 |
+
def load_dataset(path: Path = DATA_PATH) -> list[dict[str, Any]]:
|
| 1100 |
+
with path.open() as f:
|
| 1101 |
+
data = json.load(f)
|
| 1102 |
+
if len(data) != 10:
|
| 1103 |
+
raise ValueError(f"Expected 10 LoCoMo conversations, found {len(data)}")
|
| 1104 |
+
return data
|
| 1105 |
+
|
| 1106 |
+
|
| 1107 |
+
def dataset_summary(data: list[dict[str, Any]]) -> dict[str, Any]:
|
| 1108 |
+
category_counts: dict[str, int] = defaultdict(int)
|
| 1109 |
+
sessions = 0
|
| 1110 |
+
question_counts = []
|
| 1111 |
+
evaluated_counts = []
|
| 1112 |
+
for item in data:
|
| 1113 |
+
conversation = item["conversation"]
|
| 1114 |
+
sessions += sum(
|
| 1115 |
+
1
|
| 1116 |
+
for key in conversation
|
| 1117 |
+
if key.startswith("session_") and not key.endswith("_date_time")
|
| 1118 |
+
)
|
| 1119 |
+
qa = item["qa"]
|
| 1120 |
+
question_counts.append(len(qa))
|
| 1121 |
+
evaluated = 0
|
| 1122 |
+
for question in qa:
|
| 1123 |
+
category = str(question["category"])
|
| 1124 |
+
category_counts[category] += 1
|
| 1125 |
+
if category != "5":
|
| 1126 |
+
evaluated += 1
|
| 1127 |
+
evaluated_counts.append(evaluated)
|
| 1128 |
+
return {
|
| 1129 |
+
"conversations": len(data),
|
| 1130 |
+
"sessions": sessions,
|
| 1131 |
+
"questions": sum(question_counts),
|
| 1132 |
+
"evaluated_questions": sum(evaluated_counts),
|
| 1133 |
+
"question_counts": question_counts,
|
| 1134 |
+
"evaluated_counts": evaluated_counts,
|
| 1135 |
+
"category_counts": dict(sorted(category_counts.items())),
|
| 1136 |
+
}
|
| 1137 |
+
|
| 1138 |
+
|
| 1139 |
+
def file_sha256(path: Path) -> str:
|
| 1140 |
+
digest = hashlib.sha256()
|
| 1141 |
+
with path.open("rb") as f:
|
| 1142 |
+
for chunk in iter(lambda: f.read(1024 * 1024), b""):
|
| 1143 |
+
digest.update(chunk)
|
| 1144 |
+
return digest.hexdigest()
|
| 1145 |
+
|
| 1146 |
+
|
| 1147 |
+
def campaign_root(run_prefix: str) -> Path:
|
| 1148 |
+
return LOCOMO_ROOT / "results" / "reproduction" / run_prefix
|
| 1149 |
+
|
| 1150 |
+
|
| 1151 |
+
def split_output_dir(run_prefix: str, strategy: str, split: int) -> Path:
|
| 1152 |
+
return (
|
| 1153 |
+
campaign_root(run_prefix)
|
| 1154 |
+
/ "runs"
|
| 1155 |
+
/ f"memora_{strategy}_split{split:02d}"
|
| 1156 |
+
)
|
| 1157 |
+
|
| 1158 |
+
|
| 1159 |
+
def split_paths(
|
| 1160 |
+
run_prefix: str,
|
| 1161 |
+
strategy: str,
|
| 1162 |
+
split: int,
|
| 1163 |
+
) -> dict[str, Path]:
|
| 1164 |
+
method = f"memora_{strategy}"
|
| 1165 |
+
output_dir = split_output_dir(run_prefix, strategy, split)
|
| 1166 |
+
return {
|
| 1167 |
+
"dir": output_dir,
|
| 1168 |
+
"output": output_dir / f"{method}_output.json",
|
| 1169 |
+
"eval": output_dir / f"{method}_eval.json",
|
| 1170 |
+
"scores": output_dir / f"{method}_scores.json",
|
| 1171 |
+
"build_timing": output_dir / "build_timing.json",
|
| 1172 |
+
}
|
| 1173 |
+
|
| 1174 |
+
|
| 1175 |
+
def memory_store_name(run_prefix: str, split: int) -> str:
|
| 1176 |
+
safe_prefix = "".join(
|
| 1177 |
+
char if char.isalnum() or char in "._-" else "-"
|
| 1178 |
+
for char in run_prefix
|
| 1179 |
+
).strip("-")
|
| 1180 |
+
if not safe_prefix:
|
| 1181 |
+
raise ValueError("--run-prefix must contain at least one safe character")
|
| 1182 |
+
return f"{safe_prefix}-split{split:02d}"
|
| 1183 |
+
|
| 1184 |
+
|
| 1185 |
+
def build_split_command(
|
| 1186 |
+
*,
|
| 1187 |
+
python: Path,
|
| 1188 |
+
run_prefix: str,
|
| 1189 |
+
strategy: str,
|
| 1190 |
+
split: int,
|
| 1191 |
+
force_rebuild: bool,
|
| 1192 |
+
) -> list[str]:
|
| 1193 |
+
return [
|
| 1194 |
+
str(python),
|
| 1195 |
+
"run_memora.py",
|
| 1196 |
+
"--config-name",
|
| 1197 |
+
"config_glm",
|
| 1198 |
+
f"general.project_path={LOCOMO_ROOT}",
|
| 1199 |
+
f"general.results_path={campaign_root(run_prefix) / 'runs'}",
|
| 1200 |
+
"general.debug=false",
|
| 1201 |
+
"general.resume=true",
|
| 1202 |
+
f"general.run_name=split{split:02d}",
|
| 1203 |
+
f"eval.subset_idx={split}",
|
| 1204 |
+
"eval.max_workers=1",
|
| 1205 |
+
f"memory.memory_store={memory_store_name(run_prefix, split)}",
|
| 1206 |
+
f"memory.force_rebuild={'true' if force_rebuild else 'false'}",
|
| 1207 |
+
f"retrieval.strategy={strategy}",
|
| 1208 |
+
]
|
| 1209 |
+
|
| 1210 |
+
|
| 1211 |
+
def evaluated_item_count(path: Path) -> int:
|
| 1212 |
+
with path.open() as f:
|
| 1213 |
+
data = json.load(f)
|
| 1214 |
+
return sum(len(items) for items in data.values())
|
| 1215 |
+
|
| 1216 |
+
|
| 1217 |
+
def completed_split(
|
| 1218 |
+
run_prefix: str,
|
| 1219 |
+
strategy: str,
|
| 1220 |
+
split: int,
|
| 1221 |
+
expected_count: int,
|
| 1222 |
+
) -> bool:
|
| 1223 |
+
paths = split_paths(run_prefix, strategy, split)
|
| 1224 |
+
if not paths["scores"].is_file() or not paths["eval"].is_file():
|
| 1225 |
+
return False
|
| 1226 |
+
try:
|
| 1227 |
+
with paths["scores"].open() as f:
|
| 1228 |
+
scores = json.load(f)
|
| 1229 |
+
return (
|
| 1230 |
+
scores["summary"]["total_questions"] == expected_count
|
| 1231 |
+
and evaluated_item_count(paths["eval"]) == expected_count
|
| 1232 |
+
)
|
| 1233 |
+
except (KeyError, OSError, ValueError, json.JSONDecodeError):
|
| 1234 |
+
return False
|
| 1235 |
+
|
| 1236 |
+
|
| 1237 |
+
def tail(path: Path, line_count: int = 40) -> str:
|
| 1238 |
+
try:
|
| 1239 |
+
lines = path.read_text(errors="replace").splitlines()
|
| 1240 |
+
except OSError:
|
| 1241 |
+
return ""
|
| 1242 |
+
return "\n".join(lines[-line_count:])
|
| 1243 |
+
|
| 1244 |
+
|
| 1245 |
+
def child_environment() -> dict[str, str]:
|
| 1246 |
+
env = dict(os.environ)
|
| 1247 |
+
existing = env.get("PYTHONPATH")
|
| 1248 |
+
env["PYTHONPATH"] = (
|
| 1249 |
+
f"{REPO_ROOT / 'src'}{os.pathsep}{existing}"
|
| 1250 |
+
if existing
|
| 1251 |
+
else str(REPO_ROOT / "src")
|
| 1252 |
+
)
|
| 1253 |
+
env["TOKENIZERS_PARALLELISM"] = "false"
|
| 1254 |
+
return env
|
| 1255 |
+
|
| 1256 |
+
|
| 1257 |
+
def run_split(
|
| 1258 |
+
*,
|
| 1259 |
+
python: Path,
|
| 1260 |
+
run_prefix: str,
|
| 1261 |
+
strategy: str,
|
| 1262 |
+
split: int,
|
| 1263 |
+
expected_count: int,
|
| 1264 |
+
retries: int,
|
| 1265 |
+
dry_run: bool,
|
| 1266 |
+
) -> dict[str, Any]:
|
| 1267 |
+
started = time.monotonic()
|
| 1268 |
+
root = campaign_root(run_prefix)
|
| 1269 |
+
log_path = root / "logs" / f"{strategy}-split{split:02d}.log"
|
| 1270 |
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
| 1271 |
+
|
| 1272 |
+
if completed_split(run_prefix, strategy, split, expected_count):
|
| 1273 |
+
return {
|
| 1274 |
+
"split": split,
|
| 1275 |
+
"status": "skipped",
|
| 1276 |
+
"duration_seconds": 0.0,
|
| 1277 |
+
"log": str(log_path),
|
| 1278 |
+
}
|
| 1279 |
+
|
| 1280 |
+
persist_path = LOCOMO_ROOT / "memory_store" / memory_store_name(
|
| 1281 |
+
run_prefix,
|
| 1282 |
+
split,
|
| 1283 |
+
)
|
| 1284 |
+
if strategy == "semantic" and not persist_path.exists():
|
| 1285 |
+
raise RuntimeError(
|
| 1286 |
+
f"Split {split}: semantic retrieval requires the prompt-built "
|
| 1287 |
+
f"memory store at {persist_path}"
|
| 1288 |
+
)
|
| 1289 |
+
|
| 1290 |
+
attempts = max(1, retries)
|
| 1291 |
+
for attempt in range(1, attempts + 1):
|
| 1292 |
+
paths = split_paths(run_prefix, strategy, split)
|
| 1293 |
+
force_rebuild = (
|
| 1294 |
+
strategy == "prompt" and not paths["build_timing"].is_file()
|
| 1295 |
+
)
|
| 1296 |
+
command = build_split_command(
|
| 1297 |
+
python=python,
|
| 1298 |
+
run_prefix=run_prefix,
|
| 1299 |
+
strategy=strategy,
|
| 1300 |
+
split=split,
|
| 1301 |
+
force_rebuild=force_rebuild,
|
| 1302 |
+
)
|
| 1303 |
+
if dry_run:
|
| 1304 |
+
return {
|
| 1305 |
+
"split": split,
|
| 1306 |
+
"status": "dry-run",
|
| 1307 |
+
"duration_seconds": 0.0,
|
| 1308 |
+
"command": command,
|
| 1309 |
+
"log": str(log_path),
|
| 1310 |
+
}
|
| 1311 |
+
|
| 1312 |
+
with log_path.open("a") as log:
|
| 1313 |
+
log.write(
|
| 1314 |
+
f"\n=== attempt {attempt}/{attempts} "
|
| 1315 |
+
f"{datetime.now(timezone.utc).isoformat()} ===\n"
|
| 1316 |
+
)
|
| 1317 |
+
log.flush()
|
| 1318 |
+
completed = subprocess.run(
|
| 1319 |
+
command,
|
| 1320 |
+
cwd=LOCOMO_ROOT,
|
| 1321 |
+
env=child_environment(),
|
| 1322 |
+
stdout=log,
|
| 1323 |
+
stderr=subprocess.STDOUT,
|
| 1324 |
+
text=True,
|
| 1325 |
+
check=False,
|
| 1326 |
+
)
|
| 1327 |
+
|
| 1328 |
+
if completed.returncode == 0 and completed_split(
|
| 1329 |
+
run_prefix,
|
| 1330 |
+
strategy,
|
| 1331 |
+
split,
|
| 1332 |
+
expected_count,
|
| 1333 |
+
):
|
| 1334 |
+
return {
|
| 1335 |
+
"split": split,
|
| 1336 |
+
"status": "completed",
|
| 1337 |
+
"duration_seconds": round(time.monotonic() - started, 3),
|
| 1338 |
+
"attempts": attempt,
|
| 1339 |
+
"log": str(log_path),
|
| 1340 |
+
}
|
| 1341 |
+
|
| 1342 |
+
if attempt == attempts:
|
| 1343 |
+
raise RuntimeError(
|
| 1344 |
+
f"Split {split} failed after {attempts} attempts "
|
| 1345 |
+
f"(exit {completed.returncode}).\n{tail(log_path)}"
|
| 1346 |
+
)
|
| 1347 |
+
|
| 1348 |
+
raise AssertionError("unreachable")
|
| 1349 |
+
|
| 1350 |
+
|
| 1351 |
+
def mean(values: list[float]) -> float:
|
| 1352 |
+
return round(fmean(values), 4)
|
| 1353 |
+
|
| 1354 |
+
|
| 1355 |
+
def aggregate_strategy(
|
| 1356 |
+
*,
|
| 1357 |
+
run_prefix: str,
|
| 1358 |
+
strategy: str,
|
| 1359 |
+
splits: list[int],
|
| 1360 |
+
expected_counts: list[int],
|
| 1361 |
+
) -> dict[str, Any]:
|
| 1362 |
+
aggregate_eval: dict[str, list[dict[str, Any]]] = {}
|
| 1363 |
+
aggregate_output: dict[str, list[dict[str, Any]]] = {}
|
| 1364 |
+
all_eval_items: list[dict[str, Any]] = []
|
| 1365 |
+
|
| 1366 |
+
for split in splits:
|
| 1367 |
+
paths = split_paths(run_prefix, strategy, split)
|
| 1368 |
+
expected = expected_counts[split - 1]
|
| 1369 |
+
if not completed_split(run_prefix, strategy, split, expected):
|
| 1370 |
+
raise RuntimeError(f"Split {split} is incomplete and cannot be aggregated")
|
| 1371 |
+
|
| 1372 |
+
with paths["eval"].open() as f:
|
| 1373 |
+
split_eval = json.load(f)
|
| 1374 |
+
with paths["output"].open() as f:
|
| 1375 |
+
split_output = json.load(f)
|
| 1376 |
+
|
| 1377 |
+
eval_items = [
|
| 1378 |
+
item for items in split_eval.values() for item in items
|
| 1379 |
+
]
|
| 1380 |
+
output_items = [
|
| 1381 |
+
item for items in split_output.values() for item in items
|
| 1382 |
+
]
|
| 1383 |
+
aggregate_eval[str(split - 1)] = eval_items
|
| 1384 |
+
aggregate_output[str(split - 1)] = output_items
|
| 1385 |
+
all_eval_items.extend(eval_items)
|
| 1386 |
+
|
| 1387 |
+
by_category: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
| 1388 |
+
for item in all_eval_items:
|
| 1389 |
+
by_category[str(item["category"])].append(item)
|
| 1390 |
+
|
| 1391 |
+
metrics = ("bleu_score", "f1_score", "llm_score")
|
| 1392 |
+
per_category = {
|
| 1393 |
+
metric: {
|
| 1394 |
+
category: mean([float(item[metric]) for item in items])
|
| 1395 |
+
for category, items in sorted(by_category.items())
|
| 1396 |
+
}
|
| 1397 |
+
for metric in metrics
|
| 1398 |
+
}
|
| 1399 |
+
per_category["count"] = {
|
| 1400 |
+
category: len(items)
|
| 1401 |
+
for category, items in sorted(by_category.items())
|
| 1402 |
+
}
|
| 1403 |
+
overall = {
|
| 1404 |
+
metric: mean([float(item[metric]) for item in all_eval_items])
|
| 1405 |
+
for metric in metrics
|
| 1406 |
+
}
|
| 1407 |
+
scores = {
|
| 1408 |
+
"mean_scores_per_category": per_category,
|
| 1409 |
+
"overall_mean_scores": overall,
|
| 1410 |
+
"summary": {
|
| 1411 |
+
"total_questions": len(all_eval_items),
|
| 1412 |
+
"categories_evaluated": sorted(int(key) for key in by_category),
|
| 1413 |
+
"evaluation_timestamp": datetime.now(timezone.utc).isoformat(),
|
| 1414 |
+
"conversation_splits": splits,
|
| 1415 |
+
},
|
| 1416 |
+
}
|
| 1417 |
+
|
| 1418 |
+
root = campaign_root(run_prefix)
|
| 1419 |
+
root.mkdir(parents=True, exist_ok=True)
|
| 1420 |
+
for name, payload in (
|
| 1421 |
+
(f"{strategy}_output.json", aggregate_output),
|
| 1422 |
+
(f"{strategy}_eval.json", aggregate_eval),
|
| 1423 |
+
(f"{strategy}_scores.json", scores),
|
| 1424 |
+
):
|
| 1425 |
+
with (root / name).open("w") as f:
|
| 1426 |
+
json.dump(payload, f, indent=2)
|
| 1427 |
+
return scores
|
| 1428 |
+
|
| 1429 |
+
|
| 1430 |
+
def write_manifest(
|
| 1431 |
+
*,
|
| 1432 |
+
run_prefix: str,
|
| 1433 |
+
strategy: str,
|
| 1434 |
+
splits: list[int],
|
| 1435 |
+
summary: dict[str, Any],
|
| 1436 |
+
scores: dict[str, Any] | None,
|
| 1437 |
+
) -> Path:
|
| 1438 |
+
selected_questions = sum(summary["question_counts"][i - 1] for i in splits)
|
| 1439 |
+
selected_evaluated = sum(
|
| 1440 |
+
summary["evaluated_counts"][i - 1] for i in splits
|
| 1441 |
+
)
|
| 1442 |
+
manifest = {
|
| 1443 |
+
"paper": {
|
| 1444 |
+
"title": (
|
| 1445 |
+
"Memora: A Harmonic Memory Representation Balancing "
|
| 1446 |
+
"Abstraction and Specificity"
|
| 1447 |
+
),
|
| 1448 |
+
"arxiv_id": "2602.03315",
|
| 1449 |
+
"openreview_id": "zSrvkj0ers",
|
| 1450 |
+
"official_repository": "https://github.com/microsoft/Memora",
|
| 1451 |
+
"official_commit": "dec3f8f2444eace7004fc084abe1be9f3d88270e",
|
| 1452 |
+
},
|
| 1453 |
+
"campaign": run_prefix,
|
| 1454 |
+
"strategy": strategy,
|
| 1455 |
+
"splits": splits,
|
| 1456 |
+
"full_scale": splits == list(range(1, 11)),
|
| 1457 |
+
"dataset": {
|
| 1458 |
+
**summary,
|
| 1459 |
+
"selected_questions": selected_questions,
|
| 1460 |
+
"selected_evaluated_questions": selected_evaluated,
|
| 1461 |
+
"sha256": file_sha256(DATA_PATH),
|
| 1462 |
+
},
|
| 1463 |
+
"implementation": {
|
| 1464 |
+
"llm_provider": "Z.AI OpenAI-compatible API",
|
| 1465 |
+
"llm_model": os.getenv("GLM_MODEL", "glm-4.5-flash"),
|
| 1466 |
+
"embedding_provider": "local FastEmbed",
|
| 1467 |
+
"embedding_model": "BAAI/bge-small-en-v1.5",
|
| 1468 |
+
"retrieval_strategy": strategy,
|
| 1469 |
+
"top_k": 30,
|
| 1470 |
+
"hybrid_search": "BM25",
|
| 1471 |
+
"segmentation": True,
|
| 1472 |
+
"episodic_memory": "raw segments plus factual memories",
|
| 1473 |
+
"cue_index": True,
|
| 1474 |
+
"prompted_policy_max_steps": 4 if strategy == "prompt" else None,
|
| 1475 |
+
},
|
| 1476 |
+
"protocol_deviations": [
|
| 1477 |
+
(
|
| 1478 |
+
"GPT-4.1-mini was replaced by GLM because the original model "
|
| 1479 |
+
"endpoint was unavailable."
|
| 1480 |
+
),
|
| 1481 |
+
(
|
| 1482 |
+
"The repository-default OpenAI embedding endpoint was replaced "
|
| 1483 |
+
"by local BAAI/bge-small-en-v1.5 embeddings."
|
| 1484 |
+
),
|
| 1485 |
+
(
|
| 1486 |
+
"The GLM endpoint does not accept the paper's seed parameter; "
|
| 1487 |
+
"temperature was fixed at 0 and thinking was disabled."
|
| 1488 |
+
),
|
| 1489 |
+
(
|
| 1490 |
+
"Conversations were executed as ten isolated processes and "
|
| 1491 |
+
"aggregated; Memora user namespaces are conversation-local."
|
| 1492 |
+
),
|
| 1493 |
+
],
|
| 1494 |
+
"paper_targets": PAPER_TARGETS,
|
| 1495 |
+
"scores": scores,
|
| 1496 |
+
"generated_at": datetime.now(timezone.utc).isoformat(),
|
| 1497 |
+
}
|
| 1498 |
+
path = campaign_root(run_prefix) / f"{strategy}_manifest.json"
|
| 1499 |
+
with path.open("w") as f:
|
| 1500 |
+
json.dump(manifest, f, indent=2)
|
| 1501 |
+
return path
|
| 1502 |
+
|
| 1503 |
+
|
| 1504 |
+
def initialize_trackio(args: argparse.Namespace, splits: list[int]):
|
| 1505 |
+
if not args.trackio:
|
| 1506 |
+
return None
|
| 1507 |
+
try:
|
| 1508 |
+
import trackio
|
| 1509 |
+
except ImportError as exc:
|
| 1510 |
+
raise RuntimeError(
|
| 1511 |
+
"Trackio is not installed in the orchestrator environment. "
|
| 1512 |
+
"Install requirements-glm.txt first."
|
| 1513 |
+
) from exc
|
| 1514 |
+
|
| 1515 |
+
trackio.init(
|
| 1516 |
+
project=DEFAULT_PROJECT,
|
| 1517 |
+
name=f"{args.run_prefix}-{args.strategy}",
|
| 1518 |
+
group=args.run_prefix,
|
| 1519 |
+
config={
|
| 1520 |
+
"paper_arxiv": "2602.03315",
|
| 1521 |
+
"strategy": args.strategy,
|
| 1522 |
+
"model": os.getenv("GLM_MODEL", "glm-4.5-flash"),
|
| 1523 |
+
"embedding_model": "BAAI/bge-small-en-v1.5",
|
| 1524 |
+
"splits": ",".join(str(split) for split in splits),
|
| 1525 |
+
"full_scale": splits == list(range(1, 11)),
|
| 1526 |
+
},
|
| 1527 |
+
)
|
| 1528 |
+
return trackio
|
| 1529 |
+
|
| 1530 |
+
|
| 1531 |
+
def main() -> int:
|
| 1532 |
+
args = parse_args()
|
| 1533 |
+
splits = validate_splits(args.splits)
|
| 1534 |
+
if args.max_parallel < 1:
|
| 1535 |
+
raise ValueError("--max-parallel must be at least 1")
|
| 1536 |
+
if args.env_file:
|
| 1537 |
+
load_dotenv(args.env_file, override=False)
|
| 1538 |
+
if not args.dry_run and not os.getenv("GLM_API_KEY"):
|
| 1539 |
+
raise RuntimeError("GLM_API_KEY is required")
|
| 1540 |
+
if not args.python.is_file():
|
| 1541 |
+
raise FileNotFoundError(f"Python interpreter not found: {args.python}")
|
| 1542 |
+
|
| 1543 |
+
data = load_dataset()
|
| 1544 |
+
summary = dataset_summary(data)
|
| 1545 |
+
root = campaign_root(args.run_prefix)
|
| 1546 |
+
root.mkdir(parents=True, exist_ok=True)
|
| 1547 |
+
print(
|
| 1548 |
+
json.dumps(
|
| 1549 |
+
{
|
| 1550 |
+
"campaign": args.run_prefix,
|
| 1551 |
+
"strategy": args.strategy,
|
| 1552 |
+
"splits": splits,
|
| 1553 |
+
"selected_questions": sum(
|
| 1554 |
+
summary["question_counts"][i - 1] for i in splits
|
| 1555 |
+
),
|
| 1556 |
+
"selected_evaluated_questions": sum(
|
| 1557 |
+
summary["evaluated_counts"][i - 1] for i in splits
|
| 1558 |
+
),
|
| 1559 |
+
"max_parallel": args.max_parallel,
|
| 1560 |
+
}
|
| 1561 |
+
),
|
| 1562 |
+
flush=True,
|
| 1563 |
+
)
|
| 1564 |
+
|
| 1565 |
+
tracker = initialize_trackio(args, splits)
|
| 1566 |
+
failures: list[str] = []
|
| 1567 |
+
results: list[dict[str, Any]] = []
|
| 1568 |
+
with concurrent.futures.ThreadPoolExecutor(
|
| 1569 |
+
max_workers=args.max_parallel
|
| 1570 |
+
) as executor:
|
| 1571 |
+
future_to_split = {
|
| 1572 |
+
executor.submit(
|
| 1573 |
+
run_split,
|
| 1574 |
+
python=args.python,
|
| 1575 |
+
run_prefix=args.run_prefix,
|
| 1576 |
+
strategy=args.strategy,
|
| 1577 |
+
split=split,
|
| 1578 |
+
expected_count=summary["evaluated_counts"][split - 1],
|
| 1579 |
+
retries=args.retries,
|
| 1580 |
+
dry_run=args.dry_run,
|
| 1581 |
+
): split
|
| 1582 |
+
for split in splits
|
| 1583 |
+
}
|
| 1584 |
+
for future in concurrent.futures.as_completed(future_to_split):
|
| 1585 |
+
split = future_to_split[future]
|
| 1586 |
+
try:
|
| 1587 |
+
result = future.result()
|
| 1588 |
+
results.append(result)
|
| 1589 |
+
print(json.dumps(result), flush=True)
|
| 1590 |
+
if tracker and result["status"] != "dry-run":
|
| 1591 |
+
tracker.log(
|
| 1592 |
+
{
|
| 1593 |
+
"split": split,
|
| 1594 |
+
"split_success": 1,
|
| 1595 |
+
"split_duration_seconds": result[
|
| 1596 |
+
"duration_seconds"
|
| 1597 |
+
],
|
| 1598 |
+
}
|
| 1599 |
+
)
|
| 1600 |
+
except Exception as exc:
|
| 1601 |
+
message = f"split {split}: {exc}"
|
| 1602 |
+
failures.append(message)
|
| 1603 |
+
print(json.dumps({"split": split, "error": str(exc)}), flush=True)
|
| 1604 |
+
if tracker:
|
| 1605 |
+
tracker.log({"split": split, "split_success": 0})
|
| 1606 |
+
|
| 1607 |
+
if failures:
|
| 1608 |
+
write_manifest(
|
| 1609 |
+
run_prefix=args.run_prefix,
|
| 1610 |
+
strategy=args.strategy,
|
| 1611 |
+
splits=splits,
|
| 1612 |
+
summary=summary,
|
| 1613 |
+
scores=None,
|
| 1614 |
+
)
|
| 1615 |
+
if tracker:
|
| 1616 |
+
tracker.finish()
|
| 1617 |
+
raise RuntimeError("\n\n".join(failures))
|
| 1618 |
+
|
| 1619 |
+
if args.dry_run:
|
| 1620 |
+
write_manifest(
|
| 1621 |
+
run_prefix=args.run_prefix,
|
| 1622 |
+
strategy=args.strategy,
|
| 1623 |
+
splits=splits,
|
| 1624 |
+
summary=summary,
|
| 1625 |
+
scores=None,
|
| 1626 |
+
)
|
| 1627 |
+
if tracker:
|
| 1628 |
+
tracker.finish()
|
| 1629 |
+
return 0
|
| 1630 |
+
|
| 1631 |
+
scores = aggregate_strategy(
|
| 1632 |
+
run_prefix=args.run_prefix,
|
| 1633 |
+
strategy=args.strategy,
|
| 1634 |
+
splits=splits,
|
| 1635 |
+
expected_counts=summary["evaluated_counts"],
|
| 1636 |
+
)
|
| 1637 |
+
manifest_path = write_manifest(
|
| 1638 |
+
run_prefix=args.run_prefix,
|
| 1639 |
+
strategy=args.strategy,
|
| 1640 |
+
splits=splits,
|
| 1641 |
+
summary=summary,
|
| 1642 |
+
scores=scores,
|
| 1643 |
+
)
|
| 1644 |
+
if tracker:
|
| 1645 |
+
tracker.log(
|
| 1646 |
+
{
|
| 1647 |
+
"overall_llm_score": scores["overall_mean_scores"]["llm_score"],
|
| 1648 |
+
"overall_f1_score": scores["overall_mean_scores"]["f1_score"],
|
| 1649 |
+
"overall_bleu_score": scores["overall_mean_scores"]["bleu_score"],
|
| 1650 |
+
"evaluated_questions": scores["summary"]["total_questions"],
|
| 1651 |
+
"paper_llm_target": PAPER_TARGETS[args.strategy]["llm_score"],
|
| 1652 |
+
"delta_from_paper": (
|
| 1653 |
+
scores["overall_mean_scores"]["llm_score"]
|
| 1654 |
+
- PAPER_TARGETS[args.strategy]["llm_score"]
|
| 1655 |
+
),
|
| 1656 |
+
}
|
| 1657 |
+
)
|
| 1658 |
+
tracker.finish()
|
| 1659 |
+
print(
|
| 1660 |
+
json.dumps(
|
| 1661 |
+
{
|
| 1662 |
+
"status": "complete",
|
| 1663 |
+
"scores": scores["overall_mean_scores"],
|
| 1664 |
+
"evaluated_questions": scores["summary"]["total_questions"],
|
| 1665 |
+
"manifest": str(manifest_path),
|
| 1666 |
+
}
|
| 1667 |
+
),
|
| 1668 |
+
flush=True,
|
| 1669 |
+
)
|
| 1670 |
+
return 0
|
| 1671 |
+
|
| 1672 |
+
|
| 1673 |
+
if __name__ == "__main__":
|
| 1674 |
+
raise SystemExit(main())
|
| 1675 |
+
|
| 1676 |
+
````
|
| 1677 |
+
|
| 1678 |
+
|
| 1679 |
+
````output
|
| 1680 |
+
{"campaign": "glm52-full", "strategy": "prompt", "splits": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], "selected_questions": 1986, "selected_evaluated_questions": 1540, "max_parallel": 3}
|
| 1681 |
+
* Trackio project initialized: memora-icml2026-repro
|
| 1682 |
+
* Trackio metrics logged to: /Users/conanssam-m4/.cache/huggingface/trackio
|
| 1683 |
+
* View dashboard by running in your terminal:
|
| 1684 |
+
[1m[38;5;208mtrackio show --project "memora-icml2026-repro"[0m
|
| 1685 |
+
* or by running in Python: trackio.show(project="memora-icml2026-repro")
|
| 1686 |
+
* Created new run: glm52-full-prompt
|
| 1687 |
+
{"split": 2, "status": "completed", "duration_seconds": 3519.903, "attempts": 1, "log": "/Users/conanssam-m4/memora-glm/app/locomo/results/reproduction/glm52-full/logs/prompt-split02.log"}
|
| 1688 |
+
{"split": 1, "status": "completed", "duration_seconds": 4800.859, "attempts": 1, "log": "/Users/conanssam-m4/memora-glm/app/locomo/results/reproduction/glm52-full/logs/prompt-split01.log"}
|
| 1689 |
+
{"split": 3, "status": "completed", "duration_seconds": 5642.447, "attempts": 2, "log": "/Users/conanssam-m4/memora-glm/app/locomo/results/reproduction/glm52-full/logs/prompt-split03.log"}
|
| 1690 |
+
{"split": 4, "status": "completed", "duration_seconds": 8027.456, "attempts": 2, "log": "/Users/conanssam-m4/memora-glm/app/locomo/results/reproduction/glm52-full/logs/prompt-split04.log"}
|
| 1691 |
+
{"split": 5, "status": "completed", "duration_seconds": 8503.683, "attempts": 1, "log": "/Users/conanssam-m4/memora-glm/app/locomo/results/reproduction/glm52-full/logs/prompt-split05.log"}
|
| 1692 |
+
{"split": 6, "status": "completed", "duration_seconds": 8146.552, "attempts": 1, "log": "/Users/conanssam-m4/memora-glm/app/locomo/results/reproduction/glm52-full/logs/prompt-split06.log"}
|
| 1693 |
+
{"split": 7, "status": "completed", "duration_seconds": 6247.446, "attempts": 1, "log": "/Users/conanssam-m4/memora-glm/app/locomo/results/reproduction/glm52-full/logs/prompt-split07.log"}
|
| 1694 |
+
{"split": 9, "status": "completed", "duration_seconds": 4944.347, "attempts": 1, "log": "/Users/conanssam-m4/memora-glm/app/locomo/results/reproduction/glm52-full/logs/prompt-split09.log"}
|
| 1695 |
+
{"split": 8, "status": "completed", "duration_seconds": 6118.614, "attempts": 1, "log": "/Users/conanssam-m4/memora-glm/app/locomo/results/reproduction/glm52-full/logs/prompt-split08.log"}
|
| 1696 |
+
{"split": 10, "status": "completed", "duration_seconds": 4807.175, "attempts": 1, "log": "/Users/conanssam-m4/memora-glm/app/locomo/results/reproduction/glm52-full/logs/prompt-split10.log"}
|
| 1697 |
+
* Run finished. Uploading logs to Trackio (please wait...)
|
| 1698 |
+
{"status": "complete", "scores": {"bleu_score": 0.4879, "f1_score": 0.5937, "llm_score": 0.7851}, "evaluated_questions": 1540, "manifest": "/Users/conanssam-m4/memora-glm/app/locomo/results/reproduction/glm52-full/prompt_manifest.json"}
|
| 1699 |
+
|
| 1700 |
+
````
|
| 1701 |
+
|
| 1702 |
+
|
| 1703 |
+
---
|
| 1704 |
+
<!-- trackio-cell
|
| 1705 |
+
{"type": "code", "id": "cell_24968c758ad2", "created_at": "2026-07-24T14:33:53+00:00", "title": "Run: python run_locomo_reproduction.py (exit 0)", "command": [".venv/bin/python", "scripts/run_locomo_reproduction.py", "--strategy", "prompt", "--run-prefix", "glm52-full", "--max-parallel", "1", "--retries", "3", "--env-file", "/Users/conanssam-m4/.ouroboros/worktrees/studymemory/orch_d223290f8ae3/.env", "--trackio"], "exit_code": 0, "duration_s": 363.349}
|
| 1706 |
+
-->
|
| 1707 |
+
````bash
|
| 1708 |
+
$ .venv/bin/python scripts/run_locomo_reproduction.py --strategy prompt --run-prefix glm52-full --max-parallel 1 --retries 3 --env-file /Users/conanssam-m4/.ouroboros/worktrees/studymemory/orch_d223290f8ae3/.env --trackio
|
| 1709 |
+
````
|
| 1710 |
+
|
| 1711 |
+
exit 0 · 363.3s
|
| 1712 |
+
|
| 1713 |
+
|
| 1714 |
+
````python title=run_locomo_reproduction.py
|
| 1715 |
+
#!/usr/bin/env python3
|
| 1716 |
+
"""Run the full LoCoMo Memora reproduction as restartable conversation splits.
|
| 1717 |
+
|
| 1718 |
+
Each LoCoMo conversation has an isolated Memora user namespace. Running the ten
|
| 1719 |
+
conversations in separate processes therefore preserves the official method
|
| 1720 |
+
while avoiding result-directory collisions and making multi-hour API runs
|
| 1721 |
+
restartable.
|
| 1722 |
+
"""
|
| 1723 |
+
|
| 1724 |
+
from __future__ import annotations
|
| 1725 |
+
|
| 1726 |
+
import argparse
|
| 1727 |
+
import concurrent.futures
|
| 1728 |
+
import hashlib
|
| 1729 |
+
import json
|
| 1730 |
+
import os
|
| 1731 |
+
import subprocess
|
| 1732 |
+
import sys
|
| 1733 |
+
import time
|
| 1734 |
+
from collections import defaultdict
|
| 1735 |
+
from datetime import datetime, timezone
|
| 1736 |
+
from pathlib import Path
|
| 1737 |
+
from statistics import fmean
|
| 1738 |
+
from typing import Any
|
| 1739 |
+
|
| 1740 |
+
from dotenv import load_dotenv
|
| 1741 |
+
|
| 1742 |
+
|
| 1743 |
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
| 1744 |
+
LOCOMO_ROOT = REPO_ROOT / "app" / "locomo"
|
| 1745 |
+
DATA_PATH = LOCOMO_ROOT / "data" / "locomo10.json"
|
| 1746 |
+
DEFAULT_PROJECT = "memora-icml2026-repro"
|
| 1747 |
+
PAPER_TARGETS = {
|
| 1748 |
+
"prompt": {"llm_score": 0.863, "f1_score": 0.553, "bleu_score": 0.466},
|
| 1749 |
+
"semantic": {"llm_score": 0.849, "f1_score": 0.552, "bleu_score": 0.464},
|
| 1750 |
+
"full_context": {"llm_score": 0.825},
|
| 1751 |
+
}
|
| 1752 |
+
|
| 1753 |
+
|
| 1754 |
+
def parse_args() -> argparse.Namespace:
|
| 1755 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 1756 |
+
parser.add_argument(
|
| 1757 |
+
"--strategy",
|
| 1758 |
+
choices=("prompt", "semantic"),
|
| 1759 |
+
required=True,
|
| 1760 |
+
help="Memora retrieval strategy to reproduce.",
|
| 1761 |
+
)
|
| 1762 |
+
parser.add_argument(
|
| 1763 |
+
"--run-prefix",
|
| 1764 |
+
default="glm52-full",
|
| 1765 |
+
help="Stable campaign name used for result and memory directories.",
|
| 1766 |
+
)
|
| 1767 |
+
parser.add_argument(
|
| 1768 |
+
"--splits",
|
| 1769 |
+
nargs="+",
|
| 1770 |
+
type=int,
|
| 1771 |
+
default=list(range(1, 11)),
|
| 1772 |
+
help="One-based LoCoMo conversation indices (default: all 1..10).",
|
| 1773 |
+
)
|
| 1774 |
+
parser.add_argument(
|
| 1775 |
+
"--max-parallel",
|
| 1776 |
+
type=int,
|
| 1777 |
+
default=3,
|
| 1778 |
+
help="Maximum concurrent GLM-backed split processes.",
|
| 1779 |
+
)
|
| 1780 |
+
parser.add_argument(
|
| 1781 |
+
"--retries",
|
| 1782 |
+
type=int,
|
| 1783 |
+
default=2,
|
| 1784 |
+
help="Process-level attempts per split.",
|
| 1785 |
+
)
|
| 1786 |
+
parser.add_argument(
|
| 1787 |
+
"--env-file",
|
| 1788 |
+
type=Path,
|
| 1789 |
+
help="Optional dotenv file containing GLM_API_KEY and GLM_MODEL.",
|
| 1790 |
+
)
|
| 1791 |
+
parser.add_argument(
|
| 1792 |
+
"--python",
|
| 1793 |
+
type=Path,
|
| 1794 |
+
default=REPO_ROOT / ".venv" / "bin" / "python",
|
| 1795 |
+
help="Python interpreter for split processes.",
|
| 1796 |
+
)
|
| 1797 |
+
parser.add_argument(
|
| 1798 |
+
"--trackio",
|
| 1799 |
+
action="store_true",
|
| 1800 |
+
help="Log split completion and aggregate metrics to Trackio.",
|
| 1801 |
+
)
|
| 1802 |
+
parser.add_argument(
|
| 1803 |
+
"--dry-run",
|
| 1804 |
+
action="store_true",
|
| 1805 |
+
help="Print commands without running the experiment.",
|
| 1806 |
+
)
|
| 1807 |
+
return parser.parse_args()
|
| 1808 |
+
|
| 1809 |
+
|
| 1810 |
+
def validate_splits(splits: list[int]) -> list[int]:
|
| 1811 |
+
normalized = sorted(set(splits))
|
| 1812 |
+
if not normalized or normalized[0] < 1 or normalized[-1] > 10:
|
| 1813 |
+
raise ValueError("--splits must contain one-based indices between 1 and 10")
|
| 1814 |
+
return normalized
|
| 1815 |
+
|
| 1816 |
+
|
| 1817 |
+
def load_dataset(path: Path = DATA_PATH) -> list[dict[str, Any]]:
|
| 1818 |
+
with path.open() as f:
|
| 1819 |
+
data = json.load(f)
|
| 1820 |
+
if len(data) != 10:
|
| 1821 |
+
raise ValueError(f"Expected 10 LoCoMo conversations, found {len(data)}")
|
| 1822 |
+
return data
|
| 1823 |
+
|
| 1824 |
+
|
| 1825 |
+
def dataset_summary(data: list[dict[str, Any]]) -> dict[str, Any]:
|
| 1826 |
+
category_counts: dict[str, int] = defaultdict(int)
|
| 1827 |
+
sessions = 0
|
| 1828 |
+
question_counts = []
|
| 1829 |
+
evaluated_counts = []
|
| 1830 |
+
for item in data:
|
| 1831 |
+
conversation = item["conversation"]
|
| 1832 |
+
sessions += sum(
|
| 1833 |
+
1
|
| 1834 |
+
for key in conversation
|
| 1835 |
+
if key.startswith("session_") and not key.endswith("_date_time")
|
| 1836 |
+
)
|
| 1837 |
+
qa = item["qa"]
|
| 1838 |
+
question_counts.append(len(qa))
|
| 1839 |
+
evaluated = 0
|
| 1840 |
+
for question in qa:
|
| 1841 |
+
category = str(question["category"])
|
| 1842 |
+
category_counts[category] += 1
|
| 1843 |
+
if category != "5":
|
| 1844 |
+
evaluated += 1
|
| 1845 |
+
evaluated_counts.append(evaluated)
|
| 1846 |
+
return {
|
| 1847 |
+
"conversations": len(data),
|
| 1848 |
+
"sessions": sessions,
|
| 1849 |
+
"questions": sum(question_counts),
|
| 1850 |
+
"evaluated_questions": sum(evaluated_counts),
|
| 1851 |
+
"question_counts": question_counts,
|
| 1852 |
+
"evaluated_counts": evaluated_counts,
|
| 1853 |
+
"category_counts": dict(sorted(category_counts.items())),
|
| 1854 |
+
}
|
| 1855 |
+
|
| 1856 |
+
|
| 1857 |
+
def file_sha256(path: Path) -> str:
|
| 1858 |
+
digest = hashlib.sha256()
|
| 1859 |
+
with path.open("rb") as f:
|
| 1860 |
+
for chunk in iter(lambda: f.read(1024 * 1024), b""):
|
| 1861 |
+
digest.update(chunk)
|
| 1862 |
+
return digest.hexdigest()
|
| 1863 |
+
|
| 1864 |
+
|
| 1865 |
+
def campaign_root(run_prefix: str) -> Path:
|
| 1866 |
+
return LOCOMO_ROOT / "results" / "reproduction" / run_prefix
|
| 1867 |
+
|
| 1868 |
+
|
| 1869 |
+
def split_output_dir(run_prefix: str, strategy: str, split: int) -> Path:
|
| 1870 |
+
return (
|
| 1871 |
+
campaign_root(run_prefix)
|
| 1872 |
+
/ "runs"
|
| 1873 |
+
/ f"memora_{strategy}_split{split:02d}"
|
| 1874 |
+
)
|
| 1875 |
+
|
| 1876 |
+
|
| 1877 |
+
def split_paths(
|
| 1878 |
+
run_prefix: str,
|
| 1879 |
+
strategy: str,
|
| 1880 |
+
split: int,
|
| 1881 |
+
) -> dict[str, Path]:
|
| 1882 |
+
method = f"memora_{strategy}"
|
| 1883 |
+
output_dir = split_output_dir(run_prefix, strategy, split)
|
| 1884 |
+
return {
|
| 1885 |
+
"dir": output_dir,
|
| 1886 |
+
"output": output_dir / f"{method}_output.json",
|
| 1887 |
+
"eval": output_dir / f"{method}_eval.json",
|
| 1888 |
+
"scores": output_dir / f"{method}_scores.json",
|
| 1889 |
+
"build_timing": output_dir / "build_timing.json",
|
| 1890 |
+
}
|
| 1891 |
+
|
| 1892 |
+
|
| 1893 |
+
def memory_store_name(run_prefix: str, split: int) -> str:
|
| 1894 |
+
safe_prefix = "".join(
|
| 1895 |
+
char if char.isalnum() or char in "._-" else "-"
|
| 1896 |
+
for char in run_prefix
|
| 1897 |
+
).strip("-")
|
| 1898 |
+
if not safe_prefix:
|
| 1899 |
+
raise ValueError("--run-prefix must contain at least one safe character")
|
| 1900 |
+
return f"{safe_prefix}-split{split:02d}"
|
| 1901 |
+
|
| 1902 |
+
|
| 1903 |
+
def build_split_command(
|
| 1904 |
+
*,
|
| 1905 |
+
python: Path,
|
| 1906 |
+
run_prefix: str,
|
| 1907 |
+
strategy: str,
|
| 1908 |
+
split: int,
|
| 1909 |
+
force_rebuild: bool,
|
| 1910 |
+
) -> list[str]:
|
| 1911 |
+
return [
|
| 1912 |
+
str(python),
|
| 1913 |
+
"run_memora.py",
|
| 1914 |
+
"--config-name",
|
| 1915 |
+
"config_glm",
|
| 1916 |
+
f"general.project_path={LOCOMO_ROOT}",
|
| 1917 |
+
f"general.results_path={campaign_root(run_prefix) / 'runs'}",
|
| 1918 |
+
"general.debug=false",
|
| 1919 |
+
"general.resume=true",
|
| 1920 |
+
f"general.run_name=split{split:02d}",
|
| 1921 |
+
f"eval.subset_idx={split}",
|
| 1922 |
+
"eval.max_workers=1",
|
| 1923 |
+
f"memory.memory_store={memory_store_name(run_prefix, split)}",
|
| 1924 |
+
f"memory.force_rebuild={'true' if force_rebuild else 'false'}",
|
| 1925 |
+
f"retrieval.strategy={strategy}",
|
| 1926 |
+
]
|
| 1927 |
+
|
| 1928 |
+
|
| 1929 |
+
def evaluated_item_count(path: Path) -> int:
|
| 1930 |
+
with path.open() as f:
|
| 1931 |
+
data = json.load(f)
|
| 1932 |
+
return sum(len(items) for items in data.values())
|
| 1933 |
+
|
| 1934 |
+
|
| 1935 |
+
def output_has_failed_results(path: Path) -> bool:
|
| 1936 |
+
with path.open() as f:
|
| 1937 |
+
data = json.load(f)
|
| 1938 |
+
return any(
|
| 1939 |
+
str(item.get("response", "")).strip().startswith("ERROR:")
|
| 1940 |
+
for items in data.values()
|
| 1941 |
+
for item in items
|
| 1942 |
+
)
|
| 1943 |
+
|
| 1944 |
+
|
| 1945 |
+
def completed_split(
|
| 1946 |
+
run_prefix: str,
|
| 1947 |
+
strategy: str,
|
| 1948 |
+
split: int,
|
| 1949 |
+
expected_count: int,
|
| 1950 |
+
) -> bool:
|
| 1951 |
+
paths = split_paths(run_prefix, strategy, split)
|
| 1952 |
+
if (
|
| 1953 |
+
not paths["output"].is_file()
|
| 1954 |
+
or not paths["scores"].is_file()
|
| 1955 |
+
or not paths["eval"].is_file()
|
| 1956 |
+
):
|
| 1957 |
+
return False
|
| 1958 |
+
try:
|
| 1959 |
+
with paths["scores"].open() as f:
|
| 1960 |
+
scores = json.load(f)
|
| 1961 |
+
return (
|
| 1962 |
+
scores["summary"]["total_questions"] == expected_count
|
| 1963 |
+
and evaluated_item_count(paths["eval"]) == expected_count
|
| 1964 |
+
and not output_has_failed_results(paths["output"])
|
| 1965 |
+
)
|
| 1966 |
+
except (KeyError, OSError, ValueError, json.JSONDecodeError):
|
| 1967 |
+
return False
|
| 1968 |
+
|
| 1969 |
+
|
| 1970 |
+
def tail(path: Path, line_count: int = 40) -> str:
|
| 1971 |
+
try:
|
| 1972 |
+
lines = path.read_text(errors="replace").splitlines()
|
| 1973 |
+
except OSError:
|
| 1974 |
+
return ""
|
| 1975 |
+
return "\n".join(lines[-line_count:])
|
| 1976 |
+
|
| 1977 |
+
|
| 1978 |
+
def child_environment() -> dict[str, str]:
|
| 1979 |
+
env = dict(os.environ)
|
| 1980 |
+
existing = env.get("PYTHONPATH")
|
| 1981 |
+
env["PYTHONPATH"] = (
|
| 1982 |
+
f"{REPO_ROOT / 'src'}{os.pathsep}{existing}"
|
| 1983 |
+
if existing
|
| 1984 |
+
else str(REPO_ROOT / "src")
|
| 1985 |
+
)
|
| 1986 |
+
env["TOKENIZERS_PARALLELISM"] = "false"
|
| 1987 |
+
return env
|
| 1988 |
+
|
| 1989 |
+
|
| 1990 |
+
def run_split(
|
| 1991 |
+
*,
|
| 1992 |
+
python: Path,
|
| 1993 |
+
run_prefix: str,
|
| 1994 |
+
strategy: str,
|
| 1995 |
+
split: int,
|
| 1996 |
+
expected_count: int,
|
| 1997 |
+
retries: int,
|
| 1998 |
+
dry_run: bool,
|
| 1999 |
+
) -> dict[str, Any]:
|
| 2000 |
+
started = time.monotonic()
|
| 2001 |
+
root = campaign_root(run_prefix)
|
| 2002 |
+
log_path = root / "logs" / f"{strategy}-split{split:02d}.log"
|
| 2003 |
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
| 2004 |
+
|
| 2005 |
+
if completed_split(run_prefix, strategy, split, expected_count):
|
| 2006 |
+
return {
|
| 2007 |
+
"split": split,
|
| 2008 |
+
"status": "skipped",
|
| 2009 |
+
"duration_seconds": 0.0,
|
| 2010 |
+
"log": str(log_path),
|
| 2011 |
+
}
|
| 2012 |
+
|
| 2013 |
+
persist_path = LOCOMO_ROOT / "memory_store" / memory_store_name(
|
| 2014 |
+
run_prefix,
|
| 2015 |
+
split,
|
| 2016 |
+
)
|
| 2017 |
+
if strategy == "semantic" and not persist_path.exists():
|
| 2018 |
+
raise RuntimeError(
|
| 2019 |
+
f"Split {split}: semantic retrieval requires the prompt-built "
|
| 2020 |
+
f"memory store at {persist_path}"
|
| 2021 |
+
)
|
| 2022 |
+
|
| 2023 |
+
attempts = max(1, retries)
|
| 2024 |
+
for attempt in range(1, attempts + 1):
|
| 2025 |
+
paths = split_paths(run_prefix, strategy, split)
|
| 2026 |
+
force_rebuild = (
|
| 2027 |
+
strategy == "prompt" and not paths["build_timing"].is_file()
|
| 2028 |
+
)
|
| 2029 |
+
command = build_split_command(
|
| 2030 |
+
python=python,
|
| 2031 |
+
run_prefix=run_prefix,
|
| 2032 |
+
strategy=strategy,
|
| 2033 |
+
split=split,
|
| 2034 |
+
force_rebuild=force_rebuild,
|
| 2035 |
+
)
|
| 2036 |
+
if dry_run:
|
| 2037 |
+
return {
|
| 2038 |
+
"split": split,
|
| 2039 |
+
"status": "dry-run",
|
| 2040 |
+
"duration_seconds": 0.0,
|
| 2041 |
+
"command": command,
|
| 2042 |
+
"log": str(log_path),
|
| 2043 |
+
}
|
| 2044 |
+
|
| 2045 |
+
with log_path.open("a") as log:
|
| 2046 |
+
log.write(
|
| 2047 |
+
f"\n=== attempt {attempt}/{attempts} "
|
| 2048 |
+
f"{datetime.now(timezone.utc).isoformat()} ===\n"
|
| 2049 |
+
)
|
| 2050 |
+
log.flush()
|
| 2051 |
+
completed = subprocess.run(
|
| 2052 |
+
command,
|
| 2053 |
+
cwd=LOCOMO_ROOT,
|
| 2054 |
+
env=child_environment(),
|
| 2055 |
+
stdout=log,
|
| 2056 |
+
stderr=subprocess.STDOUT,
|
| 2057 |
+
text=True,
|
| 2058 |
+
check=False,
|
| 2059 |
+
)
|
| 2060 |
+
|
| 2061 |
+
if completed.returncode == 0 and completed_split(
|
| 2062 |
+
run_prefix,
|
| 2063 |
+
strategy,
|
| 2064 |
+
split,
|
| 2065 |
+
expected_count,
|
| 2066 |
+
):
|
| 2067 |
+
return {
|
| 2068 |
+
"split": split,
|
| 2069 |
+
"status": "completed",
|
| 2070 |
+
"duration_seconds": round(time.monotonic() - started, 3),
|
| 2071 |
+
"attempts": attempt,
|
| 2072 |
+
"log": str(log_path),
|
| 2073 |
+
}
|
| 2074 |
+
|
| 2075 |
+
if attempt == attempts:
|
| 2076 |
+
raise RuntimeError(
|
| 2077 |
+
f"Split {split} failed after {attempts} attempts "
|
| 2078 |
+
f"(exit {completed.returncode}).\n{tail(log_path)}"
|
| 2079 |
+
)
|
| 2080 |
+
|
| 2081 |
+
raise AssertionError("unreachable")
|
| 2082 |
+
|
| 2083 |
+
|
| 2084 |
+
def mean(values: list[float]) -> float:
|
| 2085 |
+
return round(fmean(values), 4)
|
| 2086 |
+
|
| 2087 |
+
|
| 2088 |
+
def aggregate_strategy(
|
| 2089 |
+
*,
|
| 2090 |
+
run_prefix: str,
|
| 2091 |
+
strategy: str,
|
| 2092 |
+
splits: list[int],
|
| 2093 |
+
expected_counts: list[int],
|
| 2094 |
+
) -> dict[str, Any]:
|
| 2095 |
+
aggregate_eval: dict[str, list[dict[str, Any]]] = {}
|
| 2096 |
+
aggregate_output: dict[str, list[dict[str, Any]]] = {}
|
| 2097 |
+
all_eval_items: list[dict[str, Any]] = []
|
| 2098 |
+
|
| 2099 |
+
for split in splits:
|
| 2100 |
+
paths = split_paths(run_prefix, strategy, split)
|
| 2101 |
+
expected = expected_counts[split - 1]
|
| 2102 |
+
if not completed_split(run_prefix, strategy, split, expected):
|
| 2103 |
+
raise RuntimeError(f"Split {split} is incomplete and cannot be aggregated")
|
| 2104 |
+
|
| 2105 |
+
with paths["eval"].open() as f:
|
| 2106 |
+
split_eval = json.load(f)
|
| 2107 |
+
with paths["output"].open() as f:
|
| 2108 |
+
split_output = json.load(f)
|
| 2109 |
+
|
| 2110 |
+
eval_items = [
|
| 2111 |
+
item for items in split_eval.values() for item in items
|
| 2112 |
+
]
|
| 2113 |
+
output_items = [
|
| 2114 |
+
item for items in split_output.values() for item in items
|
| 2115 |
+
]
|
| 2116 |
+
aggregate_eval[str(split - 1)] = eval_items
|
| 2117 |
+
aggregate_output[str(split - 1)] = output_items
|
| 2118 |
+
all_eval_items.extend(eval_items)
|
| 2119 |
+
|
| 2120 |
+
by_category: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
| 2121 |
+
for item in all_eval_items:
|
| 2122 |
+
by_category[str(item["category"])].append(item)
|
| 2123 |
+
|
| 2124 |
+
metrics = ("bleu_score", "f1_score", "llm_score")
|
| 2125 |
+
per_category = {
|
| 2126 |
+
metric: {
|
| 2127 |
+
category: mean([float(item[metric]) for item in items])
|
| 2128 |
+
for category, items in sorted(by_category.items())
|
| 2129 |
+
}
|
| 2130 |
+
for metric in metrics
|
| 2131 |
+
}
|
| 2132 |
+
per_category["count"] = {
|
| 2133 |
+
category: len(items)
|
| 2134 |
+
for category, items in sorted(by_category.items())
|
| 2135 |
+
}
|
| 2136 |
+
overall = {
|
| 2137 |
+
metric: mean([float(item[metric]) for item in all_eval_items])
|
| 2138 |
+
for metric in metrics
|
| 2139 |
+
}
|
| 2140 |
+
scores = {
|
| 2141 |
+
"mean_scores_per_category": per_category,
|
| 2142 |
+
"overall_mean_scores": overall,
|
| 2143 |
+
"summary": {
|
| 2144 |
+
"total_questions": len(all_eval_items),
|
| 2145 |
+
"categories_evaluated": sorted(int(key) for key in by_category),
|
| 2146 |
+
"evaluation_timestamp": datetime.now(timezone.utc).isoformat(),
|
| 2147 |
+
"conversation_splits": splits,
|
| 2148 |
+
},
|
| 2149 |
+
}
|
| 2150 |
+
|
| 2151 |
+
root = campaign_root(run_prefix)
|
| 2152 |
+
root.mkdir(parents=True, exist_ok=True)
|
| 2153 |
+
for name, payload in (
|
| 2154 |
+
(f"{strategy}_output.json", aggregate_output),
|
| 2155 |
+
(f"{strategy}_eval.json", aggregate_eval),
|
| 2156 |
+
(f"{strategy}_scores.json", scores),
|
| 2157 |
+
):
|
| 2158 |
+
with (root / name).open("w") as f:
|
| 2159 |
+
json.dump(payload, f, indent=2)
|
| 2160 |
+
return scores
|
| 2161 |
+
|
| 2162 |
+
|
| 2163 |
+
def write_manifest(
|
| 2164 |
+
*,
|
| 2165 |
+
run_prefix: str,
|
| 2166 |
+
strategy: str,
|
| 2167 |
+
splits: list[int],
|
| 2168 |
+
summary: dict[str, Any],
|
| 2169 |
+
scores: dict[str, Any] | None,
|
| 2170 |
+
) -> Path:
|
| 2171 |
+
selected_questions = sum(summary["question_counts"][i - 1] for i in splits)
|
| 2172 |
+
selected_evaluated = sum(
|
| 2173 |
+
summary["evaluated_counts"][i - 1] for i in splits
|
| 2174 |
+
)
|
| 2175 |
+
manifest = {
|
| 2176 |
+
"paper": {
|
| 2177 |
+
"title": (
|
| 2178 |
+
"Memora: A Harmonic Memory Representation Balancing "
|
| 2179 |
+
"Abstraction and Specificity"
|
| 2180 |
+
),
|
| 2181 |
+
"arxiv_id": "2602.03315",
|
| 2182 |
+
"openreview_id": "zSrvkj0ers",
|
| 2183 |
+
"official_repository": "https://github.com/microsoft/Memora",
|
| 2184 |
+
"official_commit": "dec3f8f2444eace7004fc084abe1be9f3d88270e",
|
| 2185 |
+
},
|
| 2186 |
+
"campaign": run_prefix,
|
| 2187 |
+
"strategy": strategy,
|
| 2188 |
+
"splits": splits,
|
| 2189 |
+
"full_scale": splits == list(range(1, 11)),
|
| 2190 |
+
"dataset": {
|
| 2191 |
+
**summary,
|
| 2192 |
+
"selected_questions": selected_questions,
|
| 2193 |
+
"selected_evaluated_questions": selected_evaluated,
|
| 2194 |
+
"sha256": file_sha256(DATA_PATH),
|
| 2195 |
+
},
|
| 2196 |
+
"implementation": {
|
| 2197 |
+
"llm_provider": "Z.AI OpenAI-compatible API",
|
| 2198 |
+
"llm_model": os.getenv("GLM_MODEL", "glm-4.5-flash"),
|
| 2199 |
+
"embedding_provider": "local FastEmbed",
|
| 2200 |
+
"embedding_model": "BAAI/bge-small-en-v1.5",
|
| 2201 |
+
"retrieval_strategy": strategy,
|
| 2202 |
+
"top_k": 30,
|
| 2203 |
+
"hybrid_search": "BM25",
|
| 2204 |
+
"segmentation": True,
|
| 2205 |
+
"episodic_memory": "raw segments plus factual memories",
|
| 2206 |
+
"cue_index": True,
|
| 2207 |
+
"prompted_policy_max_steps": 4 if strategy == "prompt" else None,
|
| 2208 |
+
},
|
| 2209 |
+
"protocol_deviations": [
|
| 2210 |
+
(
|
| 2211 |
+
"GPT-4.1-mini was replaced by GLM because the original model "
|
| 2212 |
+
"endpoint was unavailable."
|
| 2213 |
+
),
|
| 2214 |
+
(
|
| 2215 |
+
"The repository-default OpenAI embedding endpoint was replaced "
|
| 2216 |
+
"by local BAAI/bge-small-en-v1.5 embeddings."
|
| 2217 |
+
),
|
| 2218 |
+
(
|
| 2219 |
+
"The GLM endpoint does not accept the paper's seed parameter; "
|
| 2220 |
+
"temperature was fixed at 0 and thinking was disabled."
|
| 2221 |
+
),
|
| 2222 |
+
(
|
| 2223 |
+
"Conversations were executed as ten isolated processes and "
|
| 2224 |
+
"aggregated; Memora user namespaces are conversation-local."
|
| 2225 |
+
),
|
| 2226 |
+
],
|
| 2227 |
+
"paper_targets": PAPER_TARGETS,
|
| 2228 |
+
"scores": scores,
|
| 2229 |
+
"generated_at": datetime.now(timezone.utc).isoformat(),
|
| 2230 |
+
}
|
| 2231 |
+
path = campaign_root(run_prefix) / f"{strategy}_manifest.json"
|
| 2232 |
+
with path.open("w") as f:
|
| 2233 |
+
json.dump(manifest, f, indent=2)
|
| 2234 |
+
return path
|
| 2235 |
+
|
| 2236 |
+
|
| 2237 |
+
def initialize_trackio(args: argparse.Namespace, splits: list[int]):
|
| 2238 |
+
if not args.trackio:
|
| 2239 |
+
return None
|
| 2240 |
+
try:
|
| 2241 |
+
import trackio
|
| 2242 |
+
except ImportError as exc:
|
| 2243 |
+
raise RuntimeError(
|
| 2244 |
+
"Trackio is not installed in the orchestrator environment. "
|
| 2245 |
+
"Install requirements-glm.txt first."
|
| 2246 |
+
) from exc
|
| 2247 |
+
|
| 2248 |
+
trackio.init(
|
| 2249 |
+
project=DEFAULT_PROJECT,
|
| 2250 |
+
name=f"{args.run_prefix}-{args.strategy}",
|
| 2251 |
+
group=args.run_prefix,
|
| 2252 |
+
config={
|
| 2253 |
+
"paper_arxiv": "2602.03315",
|
| 2254 |
+
"strategy": args.strategy,
|
| 2255 |
+
"model": os.getenv("GLM_MODEL", "glm-4.5-flash"),
|
| 2256 |
+
"embedding_model": "BAAI/bge-small-en-v1.5",
|
| 2257 |
+
"splits": ",".join(str(split) for split in splits),
|
| 2258 |
+
"full_scale": splits == list(range(1, 11)),
|
| 2259 |
+
},
|
| 2260 |
+
)
|
| 2261 |
+
return trackio
|
| 2262 |
+
|
| 2263 |
+
|
| 2264 |
+
def main() -> int:
|
| 2265 |
+
args = parse_args()
|
| 2266 |
+
splits = validate_splits(args.splits)
|
| 2267 |
+
if args.max_parallel < 1:
|
| 2268 |
+
raise ValueError("--max-parallel must be at least 1")
|
| 2269 |
+
if args.env_file:
|
| 2270 |
+
load_dotenv(args.env_file, override=False)
|
| 2271 |
+
if not args.dry_run and not os.getenv("GLM_API_KEY"):
|
| 2272 |
+
raise RuntimeError("GLM_API_KEY is required")
|
| 2273 |
+
if not args.python.is_file():
|
| 2274 |
+
raise FileNotFoundError(f"Python interpreter not found: {args.python}")
|
| 2275 |
+
|
| 2276 |
+
data = load_dataset()
|
| 2277 |
+
summary = dataset_summary(data)
|
| 2278 |
+
root = campaign_root(args.run_prefix)
|
| 2279 |
+
root.mkdir(parents=True, exist_ok=True)
|
| 2280 |
+
print(
|
| 2281 |
+
json.dumps(
|
| 2282 |
+
{
|
| 2283 |
+
"campaign": args.run_prefix,
|
| 2284 |
+
"strategy": args.strategy,
|
| 2285 |
+
"splits": splits,
|
| 2286 |
+
"selected_questions": sum(
|
| 2287 |
+
summary["question_counts"][i - 1] for i in splits
|
| 2288 |
+
),
|
| 2289 |
+
"selected_evaluated_questions": sum(
|
| 2290 |
+
summary["evaluated_counts"][i - 1] for i in splits
|
| 2291 |
+
),
|
| 2292 |
+
"max_parallel": args.max_parallel,
|
| 2293 |
+
}
|
| 2294 |
+
),
|
| 2295 |
+
flush=True,
|
| 2296 |
+
)
|
| 2297 |
+
|
| 2298 |
+
tracker = initialize_trackio(args, splits)
|
| 2299 |
+
failures: list[str] = []
|
| 2300 |
+
results: list[dict[str, Any]] = []
|
| 2301 |
+
with concurrent.futures.ThreadPoolExecutor(
|
| 2302 |
+
max_workers=args.max_parallel
|
| 2303 |
+
) as executor:
|
| 2304 |
+
future_to_split = {
|
| 2305 |
+
executor.submit(
|
| 2306 |
+
run_split,
|
| 2307 |
+
python=args.python,
|
| 2308 |
+
run_prefix=args.run_prefix,
|
| 2309 |
+
strategy=args.strategy,
|
| 2310 |
+
split=split,
|
| 2311 |
+
expected_count=summary["evaluated_counts"][split - 1],
|
| 2312 |
+
retries=args.retries,
|
| 2313 |
+
dry_run=args.dry_run,
|
| 2314 |
+
): split
|
| 2315 |
+
for split in splits
|
| 2316 |
+
}
|
| 2317 |
+
for future in concurrent.futures.as_completed(future_to_split):
|
| 2318 |
+
split = future_to_split[future]
|
| 2319 |
+
try:
|
| 2320 |
+
result = future.result()
|
| 2321 |
+
results.append(result)
|
| 2322 |
+
print(json.dumps(result), flush=True)
|
| 2323 |
+
if tracker and result["status"] != "dry-run":
|
| 2324 |
+
tracker.log(
|
| 2325 |
+
{
|
| 2326 |
+
"split": split,
|
| 2327 |
+
"split_success": 1,
|
| 2328 |
+
"split_duration_seconds": result[
|
| 2329 |
+
"duration_seconds"
|
| 2330 |
+
],
|
| 2331 |
+
}
|
| 2332 |
+
)
|
| 2333 |
+
except Exception as exc:
|
| 2334 |
+
message = f"split {split}: {exc}"
|
| 2335 |
+
failures.append(message)
|
| 2336 |
+
print(json.dumps({"split": split, "error": str(exc)}), flush=True)
|
| 2337 |
+
if tracker:
|
| 2338 |
+
tracker.log({"split": split, "split_success": 0})
|
| 2339 |
+
|
| 2340 |
+
if failures:
|
| 2341 |
+
write_manifest(
|
| 2342 |
+
run_prefix=args.run_prefix,
|
| 2343 |
+
strategy=args.strategy,
|
| 2344 |
+
splits=splits,
|
| 2345 |
+
summary=summary,
|
| 2346 |
+
scores=None,
|
| 2347 |
+
)
|
| 2348 |
+
if tracker:
|
| 2349 |
+
tracker.finish()
|
| 2350 |
+
raise RuntimeError("\n\n".join(failures))
|
| 2351 |
+
|
| 2352 |
+
if args.dry_run:
|
| 2353 |
+
write_manifest(
|
| 2354 |
+
run_prefix=args.run_prefix,
|
| 2355 |
+
strategy=args.strategy,
|
| 2356 |
+
splits=splits,
|
| 2357 |
+
summary=summary,
|
| 2358 |
+
scores=None,
|
| 2359 |
+
)
|
| 2360 |
+
if tracker:
|
| 2361 |
+
tracker.finish()
|
| 2362 |
+
return 0
|
| 2363 |
+
|
| 2364 |
+
scores = aggregate_strategy(
|
| 2365 |
+
run_prefix=args.run_prefix,
|
| 2366 |
+
strategy=args.strategy,
|
| 2367 |
+
splits=splits,
|
| 2368 |
+
expected_counts=summary["evaluated_counts"],
|
| 2369 |
+
)
|
| 2370 |
+
manifest_path = write_manifest(
|
| 2371 |
+
run_prefix=args.run_prefix,
|
| 2372 |
+
strategy=args.strategy,
|
| 2373 |
+
splits=splits,
|
| 2374 |
+
summary=summary,
|
| 2375 |
+
scores=scores,
|
| 2376 |
+
)
|
| 2377 |
+
if tracker:
|
| 2378 |
+
tracker.log(
|
| 2379 |
+
{
|
| 2380 |
+
"overall_llm_score": scores["overall_mean_scores"]["llm_score"],
|
| 2381 |
+
"overall_f1_score": scores["overall_mean_scores"]["f1_score"],
|
| 2382 |
+
"overall_bleu_score": scores["overall_mean_scores"]["bleu_score"],
|
| 2383 |
+
"evaluated_questions": scores["summary"]["total_questions"],
|
| 2384 |
+
"paper_llm_target": PAPER_TARGETS[args.strategy]["llm_score"],
|
| 2385 |
+
"delta_from_paper": (
|
| 2386 |
+
scores["overall_mean_scores"]["llm_score"]
|
| 2387 |
+
- PAPER_TARGETS[args.strategy]["llm_score"]
|
| 2388 |
+
),
|
| 2389 |
+
}
|
| 2390 |
+
)
|
| 2391 |
+
tracker.finish()
|
| 2392 |
+
print(
|
| 2393 |
+
json.dumps(
|
| 2394 |
+
{
|
| 2395 |
+
"status": "complete",
|
| 2396 |
+
"scores": scores["overall_mean_scores"],
|
| 2397 |
+
"evaluated_questions": scores["summary"]["total_questions"],
|
| 2398 |
+
"manifest": str(manifest_path),
|
| 2399 |
+
}
|
| 2400 |
+
),
|
| 2401 |
+
flush=True,
|
| 2402 |
+
)
|
| 2403 |
+
return 0
|
| 2404 |
+
|
| 2405 |
+
|
| 2406 |
+
if __name__ == "__main__":
|
| 2407 |
+
raise SystemExit(main())
|
| 2408 |
+
|
| 2409 |
+
````
|
| 2410 |
+
|
| 2411 |
+
|
| 2412 |
+
````output
|
| 2413 |
+
{"campaign": "glm52-full", "strategy": "prompt", "splits": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], "selected_questions": 1986, "selected_evaluated_questions": 1540, "max_parallel": 1}
|
| 2414 |
+
* Trackio project initialized: memora-icml2026-repro
|
| 2415 |
+
* Trackio metrics logged to: /Users/conanssam-m4/.cache/huggingface/trackio
|
| 2416 |
+
* View dashboard by running in your terminal:
|
| 2417 |
+
[1m[38;5;208mtrackio show --project "memora-icml2026-repro"[0m
|
| 2418 |
+
* or by running in Python: trackio.show(project="memora-icml2026-repro")
|
| 2419 |
+
* Created new run: glm52-full-prompt
|
| 2420 |
+
{"split": 1, "status": "skipped", "duration_seconds": 0.0, "log": "/Users/conanssam-m4/memora-glm/app/locomo/results/reproduction/glm52-full/logs/prompt-split01.log"}
|
| 2421 |
+
{"split": 2, "status": "skipped", "duration_seconds": 0.0, "log": "/Users/conanssam-m4/memora-glm/app/locomo/results/reproduction/glm52-full/logs/prompt-split02.log"}
|
| 2422 |
+
{"split": 3, "status": "skipped", "duration_seconds": 0.0, "log": "/Users/conanssam-m4/memora-glm/app/locomo/results/reproduction/glm52-full/logs/prompt-split03.log"}
|
| 2423 |
+
{"split": 4, "status": "completed", "duration_seconds": 359.562, "attempts": 1, "log": "/Users/conanssam-m4/memora-glm/app/locomo/results/reproduction/glm52-full/logs/prompt-split04.log"}
|
| 2424 |
+
{"split": 5, "status": "skipped", "duration_seconds": 0.0, "log": "/Users/conanssam-m4/memora-glm/app/locomo/results/reproduction/glm52-full/logs/prompt-split05.log"}
|
| 2425 |
+
{"split": 6, "status": "skipped", "duration_seconds": 0.0, "log": "/Users/conanssam-m4/memora-glm/app/locomo/results/reproduction/glm52-full/logs/prompt-split06.log"}
|
| 2426 |
+
{"split": 7, "status": "skipped", "duration_seconds": 0.0, "log": "/Users/conanssam-m4/memora-glm/app/locomo/results/reproduction/glm52-full/logs/prompt-split07.log"}
|
| 2427 |
+
{"split": 8, "status": "skipped", "duration_seconds": 0.0, "log": "/Users/conanssam-m4/memora-glm/app/locomo/results/reproduction/glm52-full/logs/prompt-split08.log"}
|
| 2428 |
+
{"split": 9, "status": "skipped", "duration_seconds": 0.0, "log": "/Users/conanssam-m4/memora-glm/app/locomo/results/reproduction/glm52-full/logs/prompt-split09.log"}
|
| 2429 |
+
{"split": 10, "status": "skipped", "duration_seconds": 0.0, "log": "/Users/conanssam-m4/memora-glm/app/locomo/results/reproduction/glm52-full/logs/prompt-split10.log"}
|
| 2430 |
+
* Run finished. Uploading logs to Trackio (please wait...)
|
| 2431 |
+
{"status": "complete", "scores": {"bleu_score": 0.4889, "f1_score": 0.5953, "llm_score": 0.7877}, "evaluated_questions": 1540, "manifest": "/Users/conanssam-m4/memora-glm/app/locomo/results/reproduction/glm52-full/prompt_manifest.json"}
|
| 2432 |
+
|
| 2433 |
+
````
|
| 2434 |
+
|
| 2435 |
+
|
| 2436 |
+
---
|
| 2437 |
+
<!-- trackio-cell
|
| 2438 |
+
{"type": "markdown", "id": "cell_5e699394a9cc", "created_at": "2026-07-24T14:34:57+00:00", "title": "Corrected full-scale Policy result"}
|
| 2439 |
+
-->
|
| 2440 |
+
The complete GLM-5.2 substitution run covered all 10 LoCoMo conversations and all 1,540 repository-standard evaluated questions. After automatically retrying three terminal API-error answers, the verified error-free Policy result is LLM-as-a-Judge 0.7877, F1 0.5953, and BLEU-1 0.4889. The LLM score is 0.0753 below the paper Policy value of 0.863 and 0.0373 below its 0.825 full-context baseline, while F1 exceeds the paper value by 0.0423 and BLEU by 0.0229. Thus the headline LLM-judge claim is not reproduced under the documented GLM/local-embedding substitution; this demonstrates portability sensitivity and does not falsify the original same-model experiment.
|
pages/claim-2-locomo-semantic-retriever-reaches-0-849-llm-as-a-judge-and-exceeds-full-context/page.md
ADDED
|
@@ -0,0 +1,752 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Claim 2: LoCoMo Semantic Retriever reaches 0.849 LLM-as-a-Judge and exceeds full-context.
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
---
|
| 5 |
+
<!-- trackio-cell
|
| 6 |
+
{"type": "markdown", "id": "cell_7573342754b8", "created_at": "2026-07-24T05:17:39+00:00", "title": "Claim 2: LoCoMo Semantic Retriever reaches 0.849 LLM-as-a-Judge and exceeds full-context."}
|
| 7 |
+
-->
|
| 8 |
+
Document setup, runs, and results for **Claim 2: LoCoMo Semantic Retriever reaches 0.849 LLM-as-a-Judge and exceeds full-context.**.
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
---
|
| 12 |
+
<!-- trackio-cell
|
| 13 |
+
{"type": "markdown", "id": "cell_22f4dbfd0414", "created_at": "2026-07-24T13:22:29+00:00", "title": "Protocol and reuse of full memories"}
|
| 14 |
+
-->
|
| 15 |
+
We evaluate the official Semantic Retriever on the same complete LoCoMo dataset and reuse the ten fully built Memora stores from Claim 1. This preserves all 272 sessions and avoids changing memory construction between retrieval strategies. The only documented substitutions remain GLM-5.2 for GPT-4.1-mini and local BAAI/bge-small-en-v1.5 embeddings for the unavailable OpenAI embedding endpoint. The target is the paper’s 0.849 Semantic LLM-as-a-Judge score and its comparison against the 0.825 full-context baseline.
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
---
|
| 19 |
+
<!-- trackio-cell
|
| 20 |
+
{"type": "code", "id": "cell_1b7c2bac0cea", "created_at": "2026-07-24T14:23:03+00:00", "title": "Run: python run_locomo_reproduction.py (exit 0)", "command": [".venv/bin/python", "scripts/run_locomo_reproduction.py", "--strategy", "semantic", "--run-prefix", "glm52-full", "--max-parallel", "3", "--retries", "3", "--env-file", "/Users/conanssam-m4/.ouroboros/worktrees/studymemory/orch_d223290f8ae3/.env", "--trackio"], "exit_code": 0, "duration_s": 3629.683}
|
| 21 |
+
-->
|
| 22 |
+
````bash
|
| 23 |
+
$ .venv/bin/python scripts/run_locomo_reproduction.py --strategy semantic --run-prefix glm52-full --max-parallel 3 --retries 3 --env-file /Users/conanssam-m4/.ouroboros/worktrees/studymemory/orch_d223290f8ae3/.env --trackio
|
| 24 |
+
````
|
| 25 |
+
|
| 26 |
+
exit 0 · 3629.7s
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
````python title=run_locomo_reproduction.py
|
| 30 |
+
#!/usr/bin/env python3
|
| 31 |
+
"""Run the full LoCoMo Memora reproduction as restartable conversation splits.
|
| 32 |
+
|
| 33 |
+
Each LoCoMo conversation has an isolated Memora user namespace. Running the ten
|
| 34 |
+
conversations in separate processes therefore preserves the official method
|
| 35 |
+
while avoiding result-directory collisions and making multi-hour API runs
|
| 36 |
+
restartable.
|
| 37 |
+
"""
|
| 38 |
+
|
| 39 |
+
from __future__ import annotations
|
| 40 |
+
|
| 41 |
+
import argparse
|
| 42 |
+
import concurrent.futures
|
| 43 |
+
import hashlib
|
| 44 |
+
import json
|
| 45 |
+
import os
|
| 46 |
+
import subprocess
|
| 47 |
+
import sys
|
| 48 |
+
import time
|
| 49 |
+
from collections import defaultdict
|
| 50 |
+
from datetime import datetime, timezone
|
| 51 |
+
from pathlib import Path
|
| 52 |
+
from statistics import fmean
|
| 53 |
+
from typing import Any
|
| 54 |
+
|
| 55 |
+
from dotenv import load_dotenv
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
| 59 |
+
LOCOMO_ROOT = REPO_ROOT / "app" / "locomo"
|
| 60 |
+
DATA_PATH = LOCOMO_ROOT / "data" / "locomo10.json"
|
| 61 |
+
DEFAULT_PROJECT = "memora-icml2026-repro"
|
| 62 |
+
PAPER_TARGETS = {
|
| 63 |
+
"prompt": {"llm_score": 0.863, "f1_score": 0.553, "bleu_score": 0.466},
|
| 64 |
+
"semantic": {"llm_score": 0.849, "f1_score": 0.552, "bleu_score": 0.464},
|
| 65 |
+
"full_context": {"llm_score": 0.825},
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def parse_args() -> argparse.Namespace:
|
| 70 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 71 |
+
parser.add_argument(
|
| 72 |
+
"--strategy",
|
| 73 |
+
choices=("prompt", "semantic"),
|
| 74 |
+
required=True,
|
| 75 |
+
help="Memora retrieval strategy to reproduce.",
|
| 76 |
+
)
|
| 77 |
+
parser.add_argument(
|
| 78 |
+
"--run-prefix",
|
| 79 |
+
default="glm52-full",
|
| 80 |
+
help="Stable campaign name used for result and memory directories.",
|
| 81 |
+
)
|
| 82 |
+
parser.add_argument(
|
| 83 |
+
"--splits",
|
| 84 |
+
nargs="+",
|
| 85 |
+
type=int,
|
| 86 |
+
default=list(range(1, 11)),
|
| 87 |
+
help="One-based LoCoMo conversation indices (default: all 1..10).",
|
| 88 |
+
)
|
| 89 |
+
parser.add_argument(
|
| 90 |
+
"--max-parallel",
|
| 91 |
+
type=int,
|
| 92 |
+
default=3,
|
| 93 |
+
help="Maximum concurrent GLM-backed split processes.",
|
| 94 |
+
)
|
| 95 |
+
parser.add_argument(
|
| 96 |
+
"--retries",
|
| 97 |
+
type=int,
|
| 98 |
+
default=2,
|
| 99 |
+
help="Process-level attempts per split.",
|
| 100 |
+
)
|
| 101 |
+
parser.add_argument(
|
| 102 |
+
"--env-file",
|
| 103 |
+
type=Path,
|
| 104 |
+
help="Optional dotenv file containing GLM_API_KEY and GLM_MODEL.",
|
| 105 |
+
)
|
| 106 |
+
parser.add_argument(
|
| 107 |
+
"--python",
|
| 108 |
+
type=Path,
|
| 109 |
+
default=REPO_ROOT / ".venv" / "bin" / "python",
|
| 110 |
+
help="Python interpreter for split processes.",
|
| 111 |
+
)
|
| 112 |
+
parser.add_argument(
|
| 113 |
+
"--trackio",
|
| 114 |
+
action="store_true",
|
| 115 |
+
help="Log split completion and aggregate metrics to Trackio.",
|
| 116 |
+
)
|
| 117 |
+
parser.add_argument(
|
| 118 |
+
"--dry-run",
|
| 119 |
+
action="store_true",
|
| 120 |
+
help="Print commands without running the experiment.",
|
| 121 |
+
)
|
| 122 |
+
return parser.parse_args()
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def validate_splits(splits: list[int]) -> list[int]:
|
| 126 |
+
normalized = sorted(set(splits))
|
| 127 |
+
if not normalized or normalized[0] < 1 or normalized[-1] > 10:
|
| 128 |
+
raise ValueError("--splits must contain one-based indices between 1 and 10")
|
| 129 |
+
return normalized
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def load_dataset(path: Path = DATA_PATH) -> list[dict[str, Any]]:
|
| 133 |
+
with path.open() as f:
|
| 134 |
+
data = json.load(f)
|
| 135 |
+
if len(data) != 10:
|
| 136 |
+
raise ValueError(f"Expected 10 LoCoMo conversations, found {len(data)}")
|
| 137 |
+
return data
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def dataset_summary(data: list[dict[str, Any]]) -> dict[str, Any]:
|
| 141 |
+
category_counts: dict[str, int] = defaultdict(int)
|
| 142 |
+
sessions = 0
|
| 143 |
+
question_counts = []
|
| 144 |
+
evaluated_counts = []
|
| 145 |
+
for item in data:
|
| 146 |
+
conversation = item["conversation"]
|
| 147 |
+
sessions += sum(
|
| 148 |
+
1
|
| 149 |
+
for key in conversation
|
| 150 |
+
if key.startswith("session_") and not key.endswith("_date_time")
|
| 151 |
+
)
|
| 152 |
+
qa = item["qa"]
|
| 153 |
+
question_counts.append(len(qa))
|
| 154 |
+
evaluated = 0
|
| 155 |
+
for question in qa:
|
| 156 |
+
category = str(question["category"])
|
| 157 |
+
category_counts[category] += 1
|
| 158 |
+
if category != "5":
|
| 159 |
+
evaluated += 1
|
| 160 |
+
evaluated_counts.append(evaluated)
|
| 161 |
+
return {
|
| 162 |
+
"conversations": len(data),
|
| 163 |
+
"sessions": sessions,
|
| 164 |
+
"questions": sum(question_counts),
|
| 165 |
+
"evaluated_questions": sum(evaluated_counts),
|
| 166 |
+
"question_counts": question_counts,
|
| 167 |
+
"evaluated_counts": evaluated_counts,
|
| 168 |
+
"category_counts": dict(sorted(category_counts.items())),
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def file_sha256(path: Path) -> str:
|
| 173 |
+
digest = hashlib.sha256()
|
| 174 |
+
with path.open("rb") as f:
|
| 175 |
+
for chunk in iter(lambda: f.read(1024 * 1024), b""):
|
| 176 |
+
digest.update(chunk)
|
| 177 |
+
return digest.hexdigest()
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def campaign_root(run_prefix: str) -> Path:
|
| 181 |
+
return LOCOMO_ROOT / "results" / "reproduction" / run_prefix
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def split_output_dir(run_prefix: str, strategy: str, split: int) -> Path:
|
| 185 |
+
return (
|
| 186 |
+
campaign_root(run_prefix)
|
| 187 |
+
/ "runs"
|
| 188 |
+
/ f"memora_{strategy}_split{split:02d}"
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def split_paths(
|
| 193 |
+
run_prefix: str,
|
| 194 |
+
strategy: str,
|
| 195 |
+
split: int,
|
| 196 |
+
) -> dict[str, Path]:
|
| 197 |
+
method = f"memora_{strategy}"
|
| 198 |
+
output_dir = split_output_dir(run_prefix, strategy, split)
|
| 199 |
+
return {
|
| 200 |
+
"dir": output_dir,
|
| 201 |
+
"output": output_dir / f"{method}_output.json",
|
| 202 |
+
"eval": output_dir / f"{method}_eval.json",
|
| 203 |
+
"scores": output_dir / f"{method}_scores.json",
|
| 204 |
+
"build_timing": output_dir / "build_timing.json",
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def memory_store_name(run_prefix: str, split: int) -> str:
|
| 209 |
+
safe_prefix = "".join(
|
| 210 |
+
char if char.isalnum() or char in "._-" else "-"
|
| 211 |
+
for char in run_prefix
|
| 212 |
+
).strip("-")
|
| 213 |
+
if not safe_prefix:
|
| 214 |
+
raise ValueError("--run-prefix must contain at least one safe character")
|
| 215 |
+
return f"{safe_prefix}-split{split:02d}"
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
def build_split_command(
|
| 219 |
+
*,
|
| 220 |
+
python: Path,
|
| 221 |
+
run_prefix: str,
|
| 222 |
+
strategy: str,
|
| 223 |
+
split: int,
|
| 224 |
+
force_rebuild: bool,
|
| 225 |
+
) -> list[str]:
|
| 226 |
+
return [
|
| 227 |
+
str(python),
|
| 228 |
+
"run_memora.py",
|
| 229 |
+
"--config-name",
|
| 230 |
+
"config_glm",
|
| 231 |
+
f"general.project_path={LOCOMO_ROOT}",
|
| 232 |
+
f"general.results_path={campaign_root(run_prefix) / 'runs'}",
|
| 233 |
+
"general.debug=false",
|
| 234 |
+
"general.resume=true",
|
| 235 |
+
f"general.run_name=split{split:02d}",
|
| 236 |
+
f"eval.subset_idx={split}",
|
| 237 |
+
"eval.max_workers=1",
|
| 238 |
+
f"memory.memory_store={memory_store_name(run_prefix, split)}",
|
| 239 |
+
f"memory.force_rebuild={'true' if force_rebuild else 'false'}",
|
| 240 |
+
f"retrieval.strategy={strategy}",
|
| 241 |
+
]
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def evaluated_item_count(path: Path) -> int:
|
| 245 |
+
with path.open() as f:
|
| 246 |
+
data = json.load(f)
|
| 247 |
+
return sum(len(items) for items in data.values())
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
def completed_split(
|
| 251 |
+
run_prefix: str,
|
| 252 |
+
strategy: str,
|
| 253 |
+
split: int,
|
| 254 |
+
expected_count: int,
|
| 255 |
+
) -> bool:
|
| 256 |
+
paths = split_paths(run_prefix, strategy, split)
|
| 257 |
+
if not paths["scores"].is_file() or not paths["eval"].is_file():
|
| 258 |
+
return False
|
| 259 |
+
try:
|
| 260 |
+
with paths["scores"].open() as f:
|
| 261 |
+
scores = json.load(f)
|
| 262 |
+
return (
|
| 263 |
+
scores["summary"]["total_questions"] == expected_count
|
| 264 |
+
and evaluated_item_count(paths["eval"]) == expected_count
|
| 265 |
+
)
|
| 266 |
+
except (KeyError, OSError, ValueError, json.JSONDecodeError):
|
| 267 |
+
return False
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
def tail(path: Path, line_count: int = 40) -> str:
|
| 271 |
+
try:
|
| 272 |
+
lines = path.read_text(errors="replace").splitlines()
|
| 273 |
+
except OSError:
|
| 274 |
+
return ""
|
| 275 |
+
return "\n".join(lines[-line_count:])
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
def child_environment() -> dict[str, str]:
|
| 279 |
+
env = dict(os.environ)
|
| 280 |
+
existing = env.get("PYTHONPATH")
|
| 281 |
+
env["PYTHONPATH"] = (
|
| 282 |
+
f"{REPO_ROOT / 'src'}{os.pathsep}{existing}"
|
| 283 |
+
if existing
|
| 284 |
+
else str(REPO_ROOT / "src")
|
| 285 |
+
)
|
| 286 |
+
env["TOKENIZERS_PARALLELISM"] = "false"
|
| 287 |
+
return env
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
def run_split(
|
| 291 |
+
*,
|
| 292 |
+
python: Path,
|
| 293 |
+
run_prefix: str,
|
| 294 |
+
strategy: str,
|
| 295 |
+
split: int,
|
| 296 |
+
expected_count: int,
|
| 297 |
+
retries: int,
|
| 298 |
+
dry_run: bool,
|
| 299 |
+
) -> dict[str, Any]:
|
| 300 |
+
started = time.monotonic()
|
| 301 |
+
root = campaign_root(run_prefix)
|
| 302 |
+
log_path = root / "logs" / f"{strategy}-split{split:02d}.log"
|
| 303 |
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
| 304 |
+
|
| 305 |
+
if completed_split(run_prefix, strategy, split, expected_count):
|
| 306 |
+
return {
|
| 307 |
+
"split": split,
|
| 308 |
+
"status": "skipped",
|
| 309 |
+
"duration_seconds": 0.0,
|
| 310 |
+
"log": str(log_path),
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
persist_path = LOCOMO_ROOT / "memory_store" / memory_store_name(
|
| 314 |
+
run_prefix,
|
| 315 |
+
split,
|
| 316 |
+
)
|
| 317 |
+
if strategy == "semantic" and not persist_path.exists():
|
| 318 |
+
raise RuntimeError(
|
| 319 |
+
f"Split {split}: semantic retrieval requires the prompt-built "
|
| 320 |
+
f"memory store at {persist_path}"
|
| 321 |
+
)
|
| 322 |
+
|
| 323 |
+
attempts = max(1, retries)
|
| 324 |
+
for attempt in range(1, attempts + 1):
|
| 325 |
+
paths = split_paths(run_prefix, strategy, split)
|
| 326 |
+
force_rebuild = (
|
| 327 |
+
strategy == "prompt" and not paths["build_timing"].is_file()
|
| 328 |
+
)
|
| 329 |
+
command = build_split_command(
|
| 330 |
+
python=python,
|
| 331 |
+
run_prefix=run_prefix,
|
| 332 |
+
strategy=strategy,
|
| 333 |
+
split=split,
|
| 334 |
+
force_rebuild=force_rebuild,
|
| 335 |
+
)
|
| 336 |
+
if dry_run:
|
| 337 |
+
return {
|
| 338 |
+
"split": split,
|
| 339 |
+
"status": "dry-run",
|
| 340 |
+
"duration_seconds": 0.0,
|
| 341 |
+
"command": command,
|
| 342 |
+
"log": str(log_path),
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
with log_path.open("a") as log:
|
| 346 |
+
log.write(
|
| 347 |
+
f"\n=== attempt {attempt}/{attempts} "
|
| 348 |
+
f"{datetime.now(timezone.utc).isoformat()} ===\n"
|
| 349 |
+
)
|
| 350 |
+
log.flush()
|
| 351 |
+
completed = subprocess.run(
|
| 352 |
+
command,
|
| 353 |
+
cwd=LOCOMO_ROOT,
|
| 354 |
+
env=child_environment(),
|
| 355 |
+
stdout=log,
|
| 356 |
+
stderr=subprocess.STDOUT,
|
| 357 |
+
text=True,
|
| 358 |
+
check=False,
|
| 359 |
+
)
|
| 360 |
+
|
| 361 |
+
if completed.returncode == 0 and completed_split(
|
| 362 |
+
run_prefix,
|
| 363 |
+
strategy,
|
| 364 |
+
split,
|
| 365 |
+
expected_count,
|
| 366 |
+
):
|
| 367 |
+
return {
|
| 368 |
+
"split": split,
|
| 369 |
+
"status": "completed",
|
| 370 |
+
"duration_seconds": round(time.monotonic() - started, 3),
|
| 371 |
+
"attempts": attempt,
|
| 372 |
+
"log": str(log_path),
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
if attempt == attempts:
|
| 376 |
+
raise RuntimeError(
|
| 377 |
+
f"Split {split} failed after {attempts} attempts "
|
| 378 |
+
f"(exit {completed.returncode}).\n{tail(log_path)}"
|
| 379 |
+
)
|
| 380 |
+
|
| 381 |
+
raise AssertionError("unreachable")
|
| 382 |
+
|
| 383 |
+
|
| 384 |
+
def mean(values: list[float]) -> float:
|
| 385 |
+
return round(fmean(values), 4)
|
| 386 |
+
|
| 387 |
+
|
| 388 |
+
def aggregate_strategy(
|
| 389 |
+
*,
|
| 390 |
+
run_prefix: str,
|
| 391 |
+
strategy: str,
|
| 392 |
+
splits: list[int],
|
| 393 |
+
expected_counts: list[int],
|
| 394 |
+
) -> dict[str, Any]:
|
| 395 |
+
aggregate_eval: dict[str, list[dict[str, Any]]] = {}
|
| 396 |
+
aggregate_output: dict[str, list[dict[str, Any]]] = {}
|
| 397 |
+
all_eval_items: list[dict[str, Any]] = []
|
| 398 |
+
|
| 399 |
+
for split in splits:
|
| 400 |
+
paths = split_paths(run_prefix, strategy, split)
|
| 401 |
+
expected = expected_counts[split - 1]
|
| 402 |
+
if not completed_split(run_prefix, strategy, split, expected):
|
| 403 |
+
raise RuntimeError(f"Split {split} is incomplete and cannot be aggregated")
|
| 404 |
+
|
| 405 |
+
with paths["eval"].open() as f:
|
| 406 |
+
split_eval = json.load(f)
|
| 407 |
+
with paths["output"].open() as f:
|
| 408 |
+
split_output = json.load(f)
|
| 409 |
+
|
| 410 |
+
eval_items = [
|
| 411 |
+
item for items in split_eval.values() for item in items
|
| 412 |
+
]
|
| 413 |
+
output_items = [
|
| 414 |
+
item for items in split_output.values() for item in items
|
| 415 |
+
]
|
| 416 |
+
aggregate_eval[str(split - 1)] = eval_items
|
| 417 |
+
aggregate_output[str(split - 1)] = output_items
|
| 418 |
+
all_eval_items.extend(eval_items)
|
| 419 |
+
|
| 420 |
+
by_category: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
| 421 |
+
for item in all_eval_items:
|
| 422 |
+
by_category[str(item["category"])].append(item)
|
| 423 |
+
|
| 424 |
+
metrics = ("bleu_score", "f1_score", "llm_score")
|
| 425 |
+
per_category = {
|
| 426 |
+
metric: {
|
| 427 |
+
category: mean([float(item[metric]) for item in items])
|
| 428 |
+
for category, items in sorted(by_category.items())
|
| 429 |
+
}
|
| 430 |
+
for metric in metrics
|
| 431 |
+
}
|
| 432 |
+
per_category["count"] = {
|
| 433 |
+
category: len(items)
|
| 434 |
+
for category, items in sorted(by_category.items())
|
| 435 |
+
}
|
| 436 |
+
overall = {
|
| 437 |
+
metric: mean([float(item[metric]) for item in all_eval_items])
|
| 438 |
+
for metric in metrics
|
| 439 |
+
}
|
| 440 |
+
scores = {
|
| 441 |
+
"mean_scores_per_category": per_category,
|
| 442 |
+
"overall_mean_scores": overall,
|
| 443 |
+
"summary": {
|
| 444 |
+
"total_questions": len(all_eval_items),
|
| 445 |
+
"categories_evaluated": sorted(int(key) for key in by_category),
|
| 446 |
+
"evaluation_timestamp": datetime.now(timezone.utc).isoformat(),
|
| 447 |
+
"conversation_splits": splits,
|
| 448 |
+
},
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
root = campaign_root(run_prefix)
|
| 452 |
+
root.mkdir(parents=True, exist_ok=True)
|
| 453 |
+
for name, payload in (
|
| 454 |
+
(f"{strategy}_output.json", aggregate_output),
|
| 455 |
+
(f"{strategy}_eval.json", aggregate_eval),
|
| 456 |
+
(f"{strategy}_scores.json", scores),
|
| 457 |
+
):
|
| 458 |
+
with (root / name).open("w") as f:
|
| 459 |
+
json.dump(payload, f, indent=2)
|
| 460 |
+
return scores
|
| 461 |
+
|
| 462 |
+
|
| 463 |
+
def write_manifest(
|
| 464 |
+
*,
|
| 465 |
+
run_prefix: str,
|
| 466 |
+
strategy: str,
|
| 467 |
+
splits: list[int],
|
| 468 |
+
summary: dict[str, Any],
|
| 469 |
+
scores: dict[str, Any] | None,
|
| 470 |
+
) -> Path:
|
| 471 |
+
selected_questions = sum(summary["question_counts"][i - 1] for i in splits)
|
| 472 |
+
selected_evaluated = sum(
|
| 473 |
+
summary["evaluated_counts"][i - 1] for i in splits
|
| 474 |
+
)
|
| 475 |
+
manifest = {
|
| 476 |
+
"paper": {
|
| 477 |
+
"title": (
|
| 478 |
+
"Memora: A Harmonic Memory Representation Balancing "
|
| 479 |
+
"Abstraction and Specificity"
|
| 480 |
+
),
|
| 481 |
+
"arxiv_id": "2602.03315",
|
| 482 |
+
"openreview_id": "zSrvkj0ers",
|
| 483 |
+
"official_repository": "https://github.com/microsoft/Memora",
|
| 484 |
+
"official_commit": "dec3f8f2444eace7004fc084abe1be9f3d88270e",
|
| 485 |
+
},
|
| 486 |
+
"campaign": run_prefix,
|
| 487 |
+
"strategy": strategy,
|
| 488 |
+
"splits": splits,
|
| 489 |
+
"full_scale": splits == list(range(1, 11)),
|
| 490 |
+
"dataset": {
|
| 491 |
+
**summary,
|
| 492 |
+
"selected_questions": selected_questions,
|
| 493 |
+
"selected_evaluated_questions": selected_evaluated,
|
| 494 |
+
"sha256": file_sha256(DATA_PATH),
|
| 495 |
+
},
|
| 496 |
+
"implementation": {
|
| 497 |
+
"llm_provider": "Z.AI OpenAI-compatible API",
|
| 498 |
+
"llm_model": os.getenv("GLM_MODEL", "glm-4.5-flash"),
|
| 499 |
+
"embedding_provider": "local FastEmbed",
|
| 500 |
+
"embedding_model": "BAAI/bge-small-en-v1.5",
|
| 501 |
+
"retrieval_strategy": strategy,
|
| 502 |
+
"top_k": 30,
|
| 503 |
+
"hybrid_search": "BM25",
|
| 504 |
+
"segmentation": True,
|
| 505 |
+
"episodic_memory": "raw segments plus factual memories",
|
| 506 |
+
"cue_index": True,
|
| 507 |
+
"prompted_policy_max_steps": 4 if strategy == "prompt" else None,
|
| 508 |
+
},
|
| 509 |
+
"protocol_deviations": [
|
| 510 |
+
(
|
| 511 |
+
"GPT-4.1-mini was replaced by GLM because the original model "
|
| 512 |
+
"endpoint was unavailable."
|
| 513 |
+
),
|
| 514 |
+
(
|
| 515 |
+
"The repository-default OpenAI embedding endpoint was replaced "
|
| 516 |
+
"by local BAAI/bge-small-en-v1.5 embeddings."
|
| 517 |
+
),
|
| 518 |
+
(
|
| 519 |
+
"The GLM endpoint does not accept the paper's seed parameter; "
|
| 520 |
+
"temperature was fixed at 0 and thinking was disabled."
|
| 521 |
+
),
|
| 522 |
+
(
|
| 523 |
+
"Conversations were executed as ten isolated processes and "
|
| 524 |
+
"aggregated; Memora user namespaces are conversation-local."
|
| 525 |
+
),
|
| 526 |
+
],
|
| 527 |
+
"paper_targets": PAPER_TARGETS,
|
| 528 |
+
"scores": scores,
|
| 529 |
+
"generated_at": datetime.now(timezone.utc).isoformat(),
|
| 530 |
+
}
|
| 531 |
+
path = campaign_root(run_prefix) / f"{strategy}_manifest.json"
|
| 532 |
+
with path.open("w") as f:
|
| 533 |
+
json.dump(manifest, f, indent=2)
|
| 534 |
+
return path
|
| 535 |
+
|
| 536 |
+
|
| 537 |
+
def initialize_trackio(args: argparse.Namespace, splits: list[int]):
|
| 538 |
+
if not args.trackio:
|
| 539 |
+
return None
|
| 540 |
+
try:
|
| 541 |
+
import trackio
|
| 542 |
+
except ImportError as exc:
|
| 543 |
+
raise RuntimeError(
|
| 544 |
+
"Trackio is not installed in the orchestrator environment. "
|
| 545 |
+
"Install requirements-glm.txt first."
|
| 546 |
+
) from exc
|
| 547 |
+
|
| 548 |
+
trackio.init(
|
| 549 |
+
project=DEFAULT_PROJECT,
|
| 550 |
+
name=f"{args.run_prefix}-{args.strategy}",
|
| 551 |
+
group=args.run_prefix,
|
| 552 |
+
config={
|
| 553 |
+
"paper_arxiv": "2602.03315",
|
| 554 |
+
"strategy": args.strategy,
|
| 555 |
+
"model": os.getenv("GLM_MODEL", "glm-4.5-flash"),
|
| 556 |
+
"embedding_model": "BAAI/bge-small-en-v1.5",
|
| 557 |
+
"splits": ",".join(str(split) for split in splits),
|
| 558 |
+
"full_scale": splits == list(range(1, 11)),
|
| 559 |
+
},
|
| 560 |
+
)
|
| 561 |
+
return trackio
|
| 562 |
+
|
| 563 |
+
|
| 564 |
+
def main() -> int:
|
| 565 |
+
args = parse_args()
|
| 566 |
+
splits = validate_splits(args.splits)
|
| 567 |
+
if args.max_parallel < 1:
|
| 568 |
+
raise ValueError("--max-parallel must be at least 1")
|
| 569 |
+
if args.env_file:
|
| 570 |
+
load_dotenv(args.env_file, override=False)
|
| 571 |
+
if not args.dry_run and not os.getenv("GLM_API_KEY"):
|
| 572 |
+
raise RuntimeError("GLM_API_KEY is required")
|
| 573 |
+
if not args.python.is_file():
|
| 574 |
+
raise FileNotFoundError(f"Python interpreter not found: {args.python}")
|
| 575 |
+
|
| 576 |
+
data = load_dataset()
|
| 577 |
+
summary = dataset_summary(data)
|
| 578 |
+
root = campaign_root(args.run_prefix)
|
| 579 |
+
root.mkdir(parents=True, exist_ok=True)
|
| 580 |
+
print(
|
| 581 |
+
json.dumps(
|
| 582 |
+
{
|
| 583 |
+
"campaign": args.run_prefix,
|
| 584 |
+
"strategy": args.strategy,
|
| 585 |
+
"splits": splits,
|
| 586 |
+
"selected_questions": sum(
|
| 587 |
+
summary["question_counts"][i - 1] for i in splits
|
| 588 |
+
),
|
| 589 |
+
"selected_evaluated_questions": sum(
|
| 590 |
+
summary["evaluated_counts"][i - 1] for i in splits
|
| 591 |
+
),
|
| 592 |
+
"max_parallel": args.max_parallel,
|
| 593 |
+
}
|
| 594 |
+
),
|
| 595 |
+
flush=True,
|
| 596 |
+
)
|
| 597 |
+
|
| 598 |
+
tracker = initialize_trackio(args, splits)
|
| 599 |
+
failures: list[str] = []
|
| 600 |
+
results: list[dict[str, Any]] = []
|
| 601 |
+
with concurrent.futures.ThreadPoolExecutor(
|
| 602 |
+
max_workers=args.max_parallel
|
| 603 |
+
) as executor:
|
| 604 |
+
future_to_split = {
|
| 605 |
+
executor.submit(
|
| 606 |
+
run_split,
|
| 607 |
+
python=args.python,
|
| 608 |
+
run_prefix=args.run_prefix,
|
| 609 |
+
strategy=args.strategy,
|
| 610 |
+
split=split,
|
| 611 |
+
expected_count=summary["evaluated_counts"][split - 1],
|
| 612 |
+
retries=args.retries,
|
| 613 |
+
dry_run=args.dry_run,
|
| 614 |
+
): split
|
| 615 |
+
for split in splits
|
| 616 |
+
}
|
| 617 |
+
for future in concurrent.futures.as_completed(future_to_split):
|
| 618 |
+
split = future_to_split[future]
|
| 619 |
+
try:
|
| 620 |
+
result = future.result()
|
| 621 |
+
results.append(result)
|
| 622 |
+
print(json.dumps(result), flush=True)
|
| 623 |
+
if tracker and result["status"] != "dry-run":
|
| 624 |
+
tracker.log(
|
| 625 |
+
{
|
| 626 |
+
"split": split,
|
| 627 |
+
"split_success": 1,
|
| 628 |
+
"split_duration_seconds": result[
|
| 629 |
+
"duration_seconds"
|
| 630 |
+
],
|
| 631 |
+
}
|
| 632 |
+
)
|
| 633 |
+
except Exception as exc:
|
| 634 |
+
message = f"split {split}: {exc}"
|
| 635 |
+
failures.append(message)
|
| 636 |
+
print(json.dumps({"split": split, "error": str(exc)}), flush=True)
|
| 637 |
+
if tracker:
|
| 638 |
+
tracker.log({"split": split, "split_success": 0})
|
| 639 |
+
|
| 640 |
+
if failures:
|
| 641 |
+
write_manifest(
|
| 642 |
+
run_prefix=args.run_prefix,
|
| 643 |
+
strategy=args.strategy,
|
| 644 |
+
splits=splits,
|
| 645 |
+
summary=summary,
|
| 646 |
+
scores=None,
|
| 647 |
+
)
|
| 648 |
+
if tracker:
|
| 649 |
+
tracker.finish()
|
| 650 |
+
raise RuntimeError("\n\n".join(failures))
|
| 651 |
+
|
| 652 |
+
if args.dry_run:
|
| 653 |
+
write_manifest(
|
| 654 |
+
run_prefix=args.run_prefix,
|
| 655 |
+
strategy=args.strategy,
|
| 656 |
+
splits=splits,
|
| 657 |
+
summary=summary,
|
| 658 |
+
scores=None,
|
| 659 |
+
)
|
| 660 |
+
if tracker:
|
| 661 |
+
tracker.finish()
|
| 662 |
+
return 0
|
| 663 |
+
|
| 664 |
+
scores = aggregate_strategy(
|
| 665 |
+
run_prefix=args.run_prefix,
|
| 666 |
+
strategy=args.strategy,
|
| 667 |
+
splits=splits,
|
| 668 |
+
expected_counts=summary["evaluated_counts"],
|
| 669 |
+
)
|
| 670 |
+
manifest_path = write_manifest(
|
| 671 |
+
run_prefix=args.run_prefix,
|
| 672 |
+
strategy=args.strategy,
|
| 673 |
+
splits=splits,
|
| 674 |
+
summary=summary,
|
| 675 |
+
scores=scores,
|
| 676 |
+
)
|
| 677 |
+
if tracker:
|
| 678 |
+
tracker.log(
|
| 679 |
+
{
|
| 680 |
+
"overall_llm_score": scores["overall_mean_scores"]["llm_score"],
|
| 681 |
+
"overall_f1_score": scores["overall_mean_scores"]["f1_score"],
|
| 682 |
+
"overall_bleu_score": scores["overall_mean_scores"]["bleu_score"],
|
| 683 |
+
"evaluated_questions": scores["summary"]["total_questions"],
|
| 684 |
+
"paper_llm_target": PAPER_TARGETS[args.strategy]["llm_score"],
|
| 685 |
+
"delta_from_paper": (
|
| 686 |
+
scores["overall_mean_scores"]["llm_score"]
|
| 687 |
+
- PAPER_TARGETS[args.strategy]["llm_score"]
|
| 688 |
+
),
|
| 689 |
+
}
|
| 690 |
+
)
|
| 691 |
+
tracker.finish()
|
| 692 |
+
print(
|
| 693 |
+
json.dumps(
|
| 694 |
+
{
|
| 695 |
+
"status": "complete",
|
| 696 |
+
"scores": scores["overall_mean_scores"],
|
| 697 |
+
"evaluated_questions": scores["summary"]["total_questions"],
|
| 698 |
+
"manifest": str(manifest_path),
|
| 699 |
+
}
|
| 700 |
+
),
|
| 701 |
+
flush=True,
|
| 702 |
+
)
|
| 703 |
+
return 0
|
| 704 |
+
|
| 705 |
+
|
| 706 |
+
if __name__ == "__main__":
|
| 707 |
+
raise SystemExit(main())
|
| 708 |
+
|
| 709 |
+
````
|
| 710 |
+
|
| 711 |
+
|
| 712 |
+
````output
|
| 713 |
+
{"campaign": "glm52-full", "strategy": "semantic", "splits": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], "selected_questions": 1986, "selected_evaluated_questions": 1540, "max_parallel": 3}
|
| 714 |
+
* Trackio project initialized: memora-icml2026-repro
|
| 715 |
+
* Trackio metrics logged to: /Users/conanssam-m4/.cache/huggingface/trackio
|
| 716 |
+
* View dashboard by running in your terminal:
|
| 717 |
+
[1m[38;5;208mtrackio show --project "memora-icml2026-repro"[0m
|
| 718 |
+
* or by running in Python: trackio.show(project="memora-icml2026-repro")
|
| 719 |
+
* Created new run: glm52-full-semantic
|
| 720 |
+
{"split": 2, "status": "completed", "duration_seconds": 528.373, "attempts": 1, "log": "/Users/conanssam-m4/memora-glm/app/locomo/results/reproduction/glm52-full/logs/semantic-split02.log"}
|
| 721 |
+
{"split": 3, "status": "completed", "duration_seconds": 879.392, "attempts": 1, "log": "/Users/conanssam-m4/memora-glm/app/locomo/results/reproduction/glm52-full/logs/semantic-split03.log"}
|
| 722 |
+
{"split": 1, "status": "completed", "duration_seconds": 950.689, "attempts": 1, "log": "/Users/conanssam-m4/memora-glm/app/locomo/results/reproduction/glm52-full/logs/semantic-split01.log"}
|
| 723 |
+
{"split": 4, "status": "completed", "duration_seconds": 1193.757, "attempts": 1, "log": "/Users/conanssam-m4/memora-glm/app/locomo/results/reproduction/glm52-full/logs/semantic-split04.log"}
|
| 724 |
+
{"split": 6, "status": "completed", "duration_seconds": 813.608, "attempts": 1, "log": "/Users/conanssam-m4/memora-glm/app/locomo/results/reproduction/glm52-full/logs/semantic-split06.log"}
|
| 725 |
+
{"split": 5, "status": "completed", "duration_seconds": 1091.521, "attempts": 1, "log": "/Users/conanssam-m4/memora-glm/app/locomo/results/reproduction/glm52-full/logs/semantic-split05.log"}
|
| 726 |
+
{"split": 7, "status": "completed", "duration_seconds": 805.352, "attempts": 1, "log": "/Users/conanssam-m4/memora-glm/app/locomo/results/reproduction/glm52-full/logs/semantic-split07.log"}
|
| 727 |
+
{"split": 8, "status": "completed", "duration_seconds": 1133.963, "attempts": 1, "log": "/Users/conanssam-m4/memora-glm/app/locomo/results/reproduction/glm52-full/logs/semantic-split08.log"}
|
| 728 |
+
{"split": 9, "status": "completed", "duration_seconds": 1198.259, "attempts": 1, "log": "/Users/conanssam-m4/memora-glm/app/locomo/results/reproduction/glm52-full/logs/semantic-split09.log"}
|
| 729 |
+
{"split": 10, "status": "completed", "duration_seconds": 1099.892, "attempts": 1, "log": "/Users/conanssam-m4/memora-glm/app/locomo/results/reproduction/glm52-full/logs/semantic-split10.log"}
|
| 730 |
+
* Run finished. Uploading logs to Trackio (please wait...)
|
| 731 |
+
{"status": "complete", "scores": {"bleu_score": 0.4905, "f1_score": 0.5977, "llm_score": 0.774}, "evaluated_questions": 1540, "manifest": "/Users/conanssam-m4/memora-glm/app/locomo/results/reproduction/glm52-full/semantic_manifest.json"}
|
| 732 |
+
|
| 733 |
+
````
|
| 734 |
+
|
| 735 |
+
|
| 736 |
+
---
|
| 737 |
+
<!-- trackio-cell
|
| 738 |
+
{"type": "markdown", "id": "cell_e1479ac2eb67", "created_at": "2026-07-24T14:23:41+00:00", "title": "Full-scale Semantic result"}
|
| 739 |
+
-->
|
| 740 |
+
## Result: not reproduced under the GLM/local-embedding substitution
|
| 741 |
+
|
| 742 |
+
The full LoCoMo protocol completed on all 10 conversations: 1,986 raw questions and 1,540 evaluated questions (categories 1–4; category 5 skipped by the official evaluator).
|
| 743 |
+
|
| 744 |
+
| Metric | Paper Semantic | This run | Delta |
|
| 745 |
+
|---|---:|---:|---:|
|
| 746 |
+
| LLM-as-a-Judge | 0.849 | 0.7740 | -0.0750 |
|
| 747 |
+
| F1 | 0.552 | 0.5977 | +0.0457 |
|
| 748 |
+
| BLEU | 0.464 | 0.4905 | +0.0265 |
|
| 749 |
+
|
| 750 |
+
The LLM score also falls below the paper full-context baseline of 0.825 by 0.0510. Therefore Claim 2 is not reproduced in this portability setting, although lexical overlap metrics exceed the paper values. This is not an exact-model falsification: GPT-4.1-mini was replaced by GLM-5.2, OpenAI embeddings by local BAAI/bge-small-en-v1.5, and the endpoint did not accept the original seed parameter. The result isolates sensitivity to the substituted model/embedding stack rather than disproving the original same-model result.
|
| 751 |
+
|
| 752 |
+
Primary artifacts: semantic_scores.json, semantic_eval.json, and semantic_manifest.json under app/locomo/results/reproduction/glm52-full/.
|
pages/claim-3-longmemeval-reaches-87-4-average-accuracy-with-up-to-98-token-reduction/page.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Claim 3: LongMemEval reaches 87.4% average accuracy with up to 98% token reduction.
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
---
|
| 5 |
+
<!-- trackio-cell
|
| 6 |
+
{"type": "markdown", "id": "cell_5657509115a9", "created_at": "2026-07-24T05:17:39+00:00", "title": "Claim 3: LongMemEval reaches 87.4% average accuracy with up to 98% token reduction."}
|
| 7 |
+
-->
|
| 8 |
+
Document setup, runs, and results for **Claim 3: LongMemEval reaches 87.4% average accuracy with up to 98% token reduction.**.
|
pages/claim-4-harmonic-memory-uses-a-primary-abstraction-concrete-memory-value-and-cue-anchors/page.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Claim 4: Harmonic memory uses a primary abstraction, concrete memory value, and cue anchors.
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
---
|
| 5 |
+
<!-- trackio-cell
|
| 6 |
+
{"type": "markdown", "id": "cell_27f2555ed4a6", "created_at": "2026-07-24T05:17:39+00:00", "title": "Claim 4: Harmonic memory uses a primary abstraction, concrete memory value, and cue anchors."}
|
| 7 |
+
-->
|
| 8 |
+
Document setup, runs, and results for **Claim 4: Harmonic memory uses a primary abstraction, concrete memory value, and cue anchors.**.
|
pages/claim-5-rag-and-kg-retrieval-are-special-cases-of-the-memora-dual-layer-design/page.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Claim 5: RAG and KG retrieval are special cases of the Memora dual-layer design.
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
---
|
| 5 |
+
<!-- trackio-cell
|
| 6 |
+
{"type": "markdown", "id": "cell_73f8806ef558", "created_at": "2026-07-24T05:17:39+00:00", "title": "Claim 5: RAG and KG retrieval are special cases of the Memora dual-layer design."}
|
| 7 |
+
-->
|
| 8 |
+
Document setup, runs, and results for **Claim 5: RAG and KG retrieval are special cases of the Memora dual-layer design.**.
|
pages/conclusion/page.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Conclusion
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
---
|
| 5 |
+
<!-- trackio-cell
|
| 6 |
+
{"type": "markdown", "id": "cell_538b51da624a", "created_at": "2026-07-24T14:34:58+00:00", "title": "Conclusion"}
|
| 7 |
+
-->
|
| 8 |
+
The official Memora code path was reproduced at full LoCoMo scale with restartable per-conversation execution, preserved memory construction and retrieval logic, and documented provider substitutions. Policy scored 0.7877 LLM / 0.5953 F1 / 0.4889 BLEU; Semantic scored 0.7740 / 0.5977 / 0.4905. The reported LLM-score gains were not portable to GLM-5.2 plus local BGE embeddings, although lexical metrics remained at or above the paper values. The paired result suggests that Policy retrieval helps LLM-judge correctness mainly on single-hop and temporal questions, but does not yield a general lexical advantage and is slightly worse on multi-hop. Claims about the exact GPT-4.1-mini/OpenAI-embedding configuration remain untested by this substitution study.
|
pages/executive-summary/page.md
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
pages/index.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Reproduction: Memora: A Harmonic Memory Representation Balancing Abstraction and Specificity
|
| 2 |
+
|
| 3 |
+
## Pages
|
| 4 |
+
|
| 5 |
+
| Page |
|
| 6 |
+
| --- |
|
| 7 |
+
| [Executive summary](#/executive-summary) |
|
| 8 |
+
| [Claim 1: LoCoMo Policy Retriever reaches 0.863 LLM-as-a-Judge and exceeds full-context.](#/claim-1-locomo-policy-retriever-reaches-0-863-llm-as-a-judge-and-exceeds-full-context) |
|
| 9 |
+
| [Claim 2: LoCoMo Semantic Retriever reaches 0.849 LLM-as-a-Judge and exceeds full-context.](#/claim-2-locomo-semantic-retriever-reaches-0-849-llm-as-a-judge-and-exceeds-full-context) |
|
| 10 |
+
| [Claim 3: LongMemEval reaches 87.4% average accuracy with up to 98% token reduction.](#/claim-3-longmemeval-reaches-87-4-average-accuracy-with-up-to-98-token-reduction) |
|
| 11 |
+
| [Claim 4: Harmonic memory uses a primary abstraction, concrete memory value, and cue anchors.](#/claim-4-harmonic-memory-uses-a-primary-abstraction-concrete-memory-value-and-cue-anchors) |
|
| 12 |
+
| [Claim 5: RAG and KG retrieval are special cases of the Memora dual-layer design.](#/claim-5-rag-and-kg-retrieval-are-special-cases-of-the-memora-dual-layer-design) |
|
| 13 |
+
| [Conclusion](#/conclusion) |
|
trackio-logo-light.png
ADDED
|
trackio-logo.png
ADDED
|
trackio-wordmark-dark.png
ADDED
|
workspace.json
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"schema_version": 1,
|
| 3 |
+
"file_count": 0,
|
| 4 |
+
"total_size": 0,
|
| 5 |
+
"files": [],
|
| 6 |
+
"hub_refs": [],
|
| 7 |
+
"reference_only": true
|
| 8 |
+
}
|