Compare commits
1 Commits
mimo/creat
...
mimo/code/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45163cf14e |
24
app.js
24
app.js
@@ -2006,6 +2006,30 @@ function setupControls() {
|
||||
|
||||
document.getElementById('atlas-toggle-btn').addEventListener('click', openPortalAtlas);
|
||||
document.getElementById('atlas-close-btn').addEventListener('click', closePortalAtlas);
|
||||
|
||||
// Mnemosyne export/import (#1174)
|
||||
document.getElementById('mnemosyne-export-btn').addEventListener('click', () => {
|
||||
const result = SpatialMemory.exportToFile();
|
||||
if (result) {
|
||||
addChatMessage('system', 'Mnemosyne: Exported ' + result.count + ' memories to ' + result.filename);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('mnemosyne-import-btn').addEventListener('click', () => {
|
||||
document.getElementById('mnemosyne-import-file').click();
|
||||
});
|
||||
|
||||
document.getElementById('mnemosyne-import-file').addEventListener('change', async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
try {
|
||||
const result = await SpatialMemory.importFromFile(file);
|
||||
addChatMessage('system', 'Mnemosyne: Imported ' + result.count + ' of ' + result.total + ' memories');
|
||||
} catch (err) {
|
||||
addChatMessage('system', 'Mnemosyne: Import failed — ' + err.message);
|
||||
}
|
||||
e.target.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
function sendChatMessage(overrideText = null) {
|
||||
|
||||
Binary file not shown.
@@ -60,23 +60,6 @@ If the heartbeat is older than --stale-threshold seconds, the
|
||||
mind is considered dead even if the process is still running
|
||||
(e.g., hung on a blocking call).
|
||||
|
||||
KIMI HEARTBEAT
|
||||
==============
|
||||
The Kimi triage pipeline writes a cron heartbeat file after each run:
|
||||
|
||||
/var/run/bezalel/heartbeats/kimi-heartbeat.last
|
||||
(fallback: ~/.bezalel/heartbeats/kimi-heartbeat.last)
|
||||
{
|
||||
"job": "kimi-heartbeat",
|
||||
"timestamp": 1711843200.0,
|
||||
"interval_seconds": 900,
|
||||
"pid": 12345,
|
||||
"status": "ok"
|
||||
}
|
||||
|
||||
If the heartbeat is stale (>2x declared interval), the watchdog reports
|
||||
a Kimi Heartbeat failure alongside the other checks.
|
||||
|
||||
ZERO DEPENDENCIES
|
||||
=================
|
||||
Pure stdlib. No pip installs. Same machine as the nexus.
|
||||
@@ -121,10 +104,6 @@ DEFAULT_HEARTBEAT_PATH = Path.home() / ".nexus" / "heartbeat.json"
|
||||
DEFAULT_STALE_THRESHOLD = 300 # 5 minutes without a heartbeat = dead
|
||||
DEFAULT_INTERVAL = 60 # seconds between checks in watch mode
|
||||
|
||||
# Kimi Heartbeat — cron job heartbeat file written by the triage pipeline
|
||||
KIMI_HEARTBEAT_JOB = "kimi-heartbeat"
|
||||
KIMI_HEARTBEAT_STALE_MULTIPLIER = 2.0 # stale at 2x declared interval
|
||||
|
||||
GITEA_URL = os.environ.get("GITEA_URL", "https://forge.alexanderwhitestone.com")
|
||||
GITEA_TOKEN = os.environ.get("GITEA_TOKEN", "")
|
||||
GITEA_REPO = os.environ.get("NEXUS_REPO", "Timmy_Foundation/the-nexus")
|
||||
@@ -366,93 +345,6 @@ def check_syntax_health() -> CheckResult:
|
||||
)
|
||||
|
||||
|
||||
def check_kimi_heartbeat(
|
||||
job: str = KIMI_HEARTBEAT_JOB,
|
||||
stale_multiplier: float = KIMI_HEARTBEAT_STALE_MULTIPLIER,
|
||||
) -> CheckResult:
|
||||
"""Check if the Kimi Heartbeat cron job is alive.
|
||||
|
||||
Reads the ``<job>.last`` file from the standard Bezalel heartbeat
|
||||
directory (``/var/run/bezalel/heartbeats/`` or fallback
|
||||
``~/.bezalel/heartbeats/``). The file is written atomically by the
|
||||
cron_heartbeat module after each successful triage pipeline run.
|
||||
|
||||
A job is stale when:
|
||||
``time.time() - timestamp > stale_multiplier * interval_seconds``
|
||||
(same rule used by ``check_cron_heartbeats.py``).
|
||||
"""
|
||||
# Resolve heartbeat directory — same logic as cron_heartbeat._resolve
|
||||
primary = Path("/var/run/bezalel/heartbeats")
|
||||
fallback = Path.home() / ".bezalel" / "heartbeats"
|
||||
env_dir = os.environ.get("BEZALEL_HEARTBEAT_DIR")
|
||||
if env_dir:
|
||||
hb_dir = Path(env_dir)
|
||||
elif primary.exists():
|
||||
hb_dir = primary
|
||||
elif fallback.exists():
|
||||
hb_dir = fallback
|
||||
else:
|
||||
return CheckResult(
|
||||
name="Kimi Heartbeat",
|
||||
healthy=False,
|
||||
message="Heartbeat directory not found — no triage pipeline deployed yet",
|
||||
details={"searched": [str(primary), str(fallback)]},
|
||||
)
|
||||
|
||||
hb_file = hb_dir / f"{job}.last"
|
||||
if not hb_file.exists():
|
||||
return CheckResult(
|
||||
name="Kimi Heartbeat",
|
||||
healthy=False,
|
||||
message=f"No heartbeat file at {hb_file} — Kimi triage pipeline has never reported",
|
||||
details={"path": str(hb_file)},
|
||||
)
|
||||
|
||||
try:
|
||||
data = json.loads(hb_file.read_text())
|
||||
except (json.JSONDecodeError, OSError) as e:
|
||||
return CheckResult(
|
||||
name="Kimi Heartbeat",
|
||||
healthy=False,
|
||||
message=f"Heartbeat file corrupt: {e}",
|
||||
details={"path": str(hb_file), "error": str(e)},
|
||||
)
|
||||
|
||||
timestamp = float(data.get("timestamp", 0))
|
||||
interval = int(data.get("interval_seconds", 0))
|
||||
raw_status = data.get("status", "unknown")
|
||||
age = time.time() - timestamp
|
||||
|
||||
if interval <= 0:
|
||||
# No declared interval — use raw timestamp age (30 min default)
|
||||
interval = 1800
|
||||
|
||||
threshold = stale_multiplier * interval
|
||||
is_stale = age > threshold
|
||||
|
||||
age_str = f"{int(age)}s" if age < 3600 else f"{int(age // 3600)}h {int((age % 3600) // 60)}m"
|
||||
interval_str = f"{int(interval)}s" if interval < 3600 else f"{int(interval // 3600)}h {int((interval % 3600) // 60)}m"
|
||||
|
||||
if is_stale:
|
||||
return CheckResult(
|
||||
name="Kimi Heartbeat",
|
||||
healthy=False,
|
||||
message=(
|
||||
f"Silent for {age_str} "
|
||||
f"(threshold: {stale_multiplier}x {interval_str} = {int(threshold)}s). "
|
||||
f"Status: {raw_status}"
|
||||
),
|
||||
details=data,
|
||||
)
|
||||
|
||||
return CheckResult(
|
||||
name="Kimi Heartbeat",
|
||||
healthy=True,
|
||||
message=f"Alive — last beat {age_str} ago (interval {interval_str}, status={raw_status})",
|
||||
details=data,
|
||||
)
|
||||
|
||||
|
||||
# ── Gitea alerting ───────────────────────────────────────────────────
|
||||
|
||||
def _gitea_request(method: str, path: str, data: Optional[dict] = None) -> Any:
|
||||
@@ -554,7 +446,6 @@ def run_health_checks(
|
||||
check_mind_process(),
|
||||
check_heartbeat(heartbeat_path, stale_threshold),
|
||||
check_syntax_health(),
|
||||
check_kimi_heartbeat(),
|
||||
]
|
||||
return HealthReport(timestamp=time.time(), checks=checks)
|
||||
|
||||
@@ -654,14 +545,6 @@ def main():
|
||||
"--json", action="store_true", dest="output_json",
|
||||
help="Output results as JSON (for integration with other tools)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--kimi-job", default=KIMI_HEARTBEAT_JOB,
|
||||
help=f"Kimi heartbeat job name (default: {KIMI_HEARTBEAT_JOB})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--kimi-stale-multiplier", type=float, default=KIMI_HEARTBEAT_STALE_MULTIPLIER,
|
||||
help=f"Kimi heartbeat staleness multiplier (default: {KIMI_HEARTBEAT_STALE_MULTIPLIER})",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
@@ -233,6 +233,11 @@
|
||||
<div class="memory-meta-row"><span class="memory-meta-label">Time</span><span id="memory-panel-time">\u2014</span></div>
|
||||
<div class="memory-meta-row memory-meta-row--related"><span class="memory-meta-label">Related</span><span id="memory-panel-connections">\u2014</span></div>
|
||||
</div>
|
||||
<div class="memory-panel-actions">
|
||||
<button id="mnemosyne-export-btn" class="mnemosyne-action-btn" title="Export spatial memory to JSON">⤓ Export</button>
|
||||
<button id="mnemosyne-import-btn" class="mnemosyne-action-btn" title="Import spatial memory from JSON">⤒ Import</button>
|
||||
<input type="file" id="mnemosyne-import-file" accept=".json" style="display:none;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -652,11 +652,93 @@ const SpatialMemory = (() => {
|
||||
return _selectedId;
|
||||
}
|
||||
|
||||
// ─── FILE EXPORT ──────────────────────────────────────
|
||||
function exportToFile() {
|
||||
const index = exportIndex();
|
||||
const json = JSON.stringify(index, null, 2);
|
||||
const date = new Date().toISOString().slice(0, 10);
|
||||
const filename = 'mnemosyne-export-' + date + '.json';
|
||||
|
||||
const blob = new Blob([json], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
console.info('[Mnemosyne] Exported', index.memories.length, 'memories to', filename);
|
||||
return { filename, count: index.memories.length };
|
||||
}
|
||||
|
||||
// ─── FILE IMPORT ──────────────────────────────────────
|
||||
function importFromFile(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!file) {
|
||||
reject(new Error('No file provided'));
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
try {
|
||||
const data = JSON.parse(e.target.result);
|
||||
|
||||
// Schema validation
|
||||
if (!data || typeof data !== 'object') {
|
||||
reject(new Error('Invalid JSON: not an object'));
|
||||
return;
|
||||
}
|
||||
if (typeof data.version !== 'number') {
|
||||
reject(new Error('Invalid schema: missing version field'));
|
||||
return;
|
||||
}
|
||||
if (data.version !== STORAGE_VERSION) {
|
||||
reject(new Error('Version mismatch: got ' + data.version + ', expected ' + STORAGE_VERSION));
|
||||
return;
|
||||
}
|
||||
if (!Array.isArray(data.memories)) {
|
||||
reject(new Error('Invalid schema: memories is not an array'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate each memory entry
|
||||
for (let i = 0; i < data.memories.length; i++) {
|
||||
const mem = data.memories[i];
|
||||
if (!mem.id || typeof mem.id !== 'string') {
|
||||
reject(new Error('Invalid memory at index ' + i + ': missing or invalid id'));
|
||||
return;
|
||||
}
|
||||
if (!mem.category || typeof mem.category !== 'string') {
|
||||
reject(new Error('Invalid memory "' + mem.id + '": missing category'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const count = importIndex(data);
|
||||
saveToStorage();
|
||||
console.info('[Mnemosyne] Imported', count, 'memories from file');
|
||||
resolve({ count, total: data.memories.length });
|
||||
} catch (parseErr) {
|
||||
reject(new Error('Failed to parse JSON: ' + parseErr.message));
|
||||
}
|
||||
};
|
||||
|
||||
reader.onerror = function() {
|
||||
reject(new Error('Failed to read file'));
|
||||
};
|
||||
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
init, placeMemory, removeMemory, update,
|
||||
getMemoryAtPosition, getRegionAtPosition, getMemoriesInRegion, getAllMemories,
|
||||
getCrystalMeshes, getMemoryFromMesh, highlightMemory, clearHighlight, getSelectedId,
|
||||
exportIndex, importIndex, searchNearby, REGIONS,
|
||||
exportIndex, importIndex, exportToFile, importFromFile, searchNearby, REGIONS,
|
||||
saveToStorage, loadFromStorage, clearStorage,
|
||||
runGravityLayout
|
||||
};
|
||||
|
||||
37
style.css
37
style.css
@@ -1461,6 +1461,43 @@ canvas#nexus-canvas {
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════
|
||||
PROJECT MNEMOSYNE — EXPORT/IMPORT ACTIONS (#1174)
|
||||
═══════════════════════════════════════════════════════ */
|
||||
|
||||
.memory-panel-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid rgba(123, 92, 255, 0.15);
|
||||
}
|
||||
|
||||
.mnemosyne-action-btn {
|
||||
flex: 1;
|
||||
padding: 6px 10px;
|
||||
background: rgba(123, 92, 255, 0.12);
|
||||
border: 1px solid rgba(123, 92, 255, 0.3);
|
||||
border-radius: 6px;
|
||||
color: #a08cff;
|
||||
font-size: 11px;
|
||||
font-family: monospace;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mnemosyne-action-btn:hover {
|
||||
background: rgba(123, 92, 255, 0.25);
|
||||
border-color: rgba(123, 92, 255, 0.6);
|
||||
color: #c4b5ff;
|
||||
}
|
||||
|
||||
.mnemosyne-action-btn:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════
|
||||
PROJECT MNEMOSYNE — SESSION ROOM HUD PANEL (#1171)
|
||||
═══════════════════════════════════════════════════════ */
|
||||
|
||||
Reference in New Issue
Block a user