feat: make the two-step reflection observable (draft -> revised -> critique)

You couldn't see her actually correct herself — /self showed only the result.
Now:
- reflect() logs the draft, the revised/committed version, and the self-critique
  to the live log as an expandable "view details" block
- POST /self/reflect runs a reflection in the web process so it lands in /logs
  live (reflections normally run in the dream process, whose logs only go to
  journald); "↻ Reflect now" button on /self triggers it, with a logs ↗ link
- log viewers relabel the expander "view full prompt" -> "view details" (it now
  carries prompts and reflection diffs)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-17 04:53:38 +00:00
parent 3df060a1cd
commit 4c8f7202da
5 changed files with 54 additions and 6 deletions
+7
View File
@@ -125,6 +125,13 @@ def create_app() -> FastAPI:
"""Lyra's current interiority + when it last changed."""
return {"state": self_state.load(), "updated_at": memory.self_state_updated_at()}
@app.post("/self/reflect")
async def self_reflect() -> dict:
"""Run one two-step reflection now, in this process, so the draft ->
revised -> critique lands in the live log (/logs)."""
state = await asyncio.to_thread(self_state.reflect)
return {"ok": True, "mood": state.get("mood")}
@app.get("/stream/logs")
async def stream_logs(request: Request) -> StreamingResponse:
"""Live activity feed: replay the recent buffer, then stream new events."""
+1 -1
View File
@@ -756,7 +756,7 @@
<span class="log-level log-level-${level}">${escapeHtml(level)}</span>
<span class="log-msg">${escapeHtml(event.msg || '')}</span>
${fieldStr ? `<span class="log-fields">${escapeHtml(fieldStr)}</span>` : ''}
${detail ? `<details class="log-detail"><summary>view full prompt</summary><pre>${escapeHtml(detail)}</pre></details>` : ''}
${detail ? `<details class="log-detail"><summary>view details</summary><pre>${escapeHtml(detail)}</pre></details>` : ''}
`;
thinkingContent.appendChild(eventDiv);
+1 -1
View File
@@ -210,7 +210,7 @@
`<span class="msg">${esc(ev.msg || '')}</span>` +
`</div>` +
(fieldStr ? `<div class="fields">${esc(fieldStr)}</div>` : '') +
(detail ? `<details class="detail"><summary>view full prompt</summary><pre>${esc(detail)}</pre></details>` : '');
(detail ? `<details class="detail"><summary>view details</summary><pre>${esc(detail)}</pre></details>` : '');
if (!matches(line)) line.classList.add('hidden');
logEl.appendChild(line);
+18
View File
@@ -25,6 +25,12 @@
.topbar h1 { font-size: 1.05rem; margin: 0; font-weight: 600; }
.topbar a.back { color: var(--accent); text-decoration: none; font-size: .95rem; }
.updated { margin-left: auto; color: var(--fade); font-size: .78rem; }
#reflectBtn {
background: #1b2333; border: 1px solid var(--border); color: var(--accent);
border-radius: 8px; padding: 6px 11px; font-size: .82rem; cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
#reflectBtn:disabled { opacity: .5; cursor: default; }
.dot { width: 9px; height: 9px; border-radius: 50%; background: var(--good); box-shadow: 0 0 8px var(--good); flex: none; opacity: .35; transition: opacity .2s; }
.dot.pulse { opacity: 1; }
@@ -64,6 +70,8 @@
<span class="dot" id="dot"></span>
<h1>🧠 Lyra · Mind</h1>
<a class="back" href="/">← Chat</a>
<a class="back" href="/logs" target="_blank" rel="noopener" title="Watch the live log">logs ↗</a>
<button id="reflectBtn" title="Make her reflect now (draft → self-critique → revise). Watch it in /logs.">↻ Reflect now</button>
<span class="updated" id="updated"></span>
</div>
</header>
@@ -172,6 +180,16 @@
}
}
const reflectBtn = document.getElementById('reflectBtn');
reflectBtn.addEventListener('click', async () => {
reflectBtn.disabled = true;
const old = reflectBtn.textContent;
reflectBtn.textContent = '… thinking';
try { await fetch('/self/reflect', { method: 'POST' }); await refresh(); }
catch (e) { /* ignore */ }
finally { reflectBtn.disabled = false; reflectBtn.textContent = old; }
});
refresh();
setInterval(refresh, 12000);
document.addEventListener('visibilitychange', () => { if (!document.hidden) refresh(); });