diff --git a/chat/app.py b/chat/app.py
index 1d47509..0acbf69 100644
--- a/chat/app.py
+++ b/chat/app.py
@@ -12,6 +12,7 @@ from chat.services.background import BackgroundWorker
# Trigger handler registration:
import chat.state.entities # noqa: F401
import chat.state.edges # noqa: F401
+import chat.state.manual_edit # noqa: F401
import chat.state.memory # noqa: F401
import chat.state.world # noqa: F401
diff --git a/chat/state/manual_edit.py b/chat/state/manual_edit.py
new file mode 100644
index 0000000..ff6b247
--- /dev/null
+++ b/chat/state/manual_edit.py
@@ -0,0 +1,73 @@
+"""Handler for ``manual_edit`` events (T25, §6.4 final paragraph).
+
+A ``manual_edit`` event captures a user override of a projected field — its
+payload snapshots both the prior value and the new value so any edit can
+be reversed by emitting an inverse ``manual_edit`` later. This module
+applies the new value to the appropriate target table; the snapshot of
+``prior_value`` is taken by the route handler before this fires.
+
+Phase 1 covers three target kinds:
+- ``edge_affinity`` and ``edge_trust`` — slider edits on a specific edge,
+ clamped to 0..100.
+- ``memory_significance`` — dropdown edit, clamped to 0..3.
+- ``memory_pov_summary`` — textarea edit (string).
+
+Other §6.4 editable fields (activity verb / attention / posture, edge
+summary, knowledge_facts list manipulation) are deferred to Phase 1.5.
+
+Pin toggles intentionally use the existing ``memory_pin_changed`` event
+(registered in :mod:`chat.state.memory`) rather than ``manual_edit`` so
+the projection writes both ``pinned`` and ``auto_pinned`` atomically.
+"""
+
+from __future__ import annotations
+
+from sqlite3 import Connection
+
+from chat.eventlog.log import Event
+from chat.eventlog.projector import on
+
+
+def _clamp(value: int, lo: int, hi: int) -> int:
+ return max(lo, min(hi, value))
+
+
+@on("manual_edit")
+def _apply_manual_edit(conn: Connection, e: Event) -> None:
+ p = e.payload
+ kind = p["target_kind"]
+ target_id = p["target_id"]
+ new_value = p["new_value"]
+
+ if kind == "edge_affinity":
+ conn.execute(
+ "UPDATE edges SET affinity = ? "
+ "WHERE source_id = ? AND target_id = ?",
+ (
+ _clamp(int(new_value), 0, 100),
+ target_id["source_id"],
+ target_id["target_id"],
+ ),
+ )
+ elif kind == "edge_trust":
+ conn.execute(
+ "UPDATE edges SET trust = ? "
+ "WHERE source_id = ? AND target_id = ?",
+ (
+ _clamp(int(new_value), 0, 100),
+ target_id["source_id"],
+ target_id["target_id"],
+ ),
+ )
+ elif kind == "memory_significance":
+ conn.execute(
+ "UPDATE memories SET significance = ? WHERE id = ?",
+ (_clamp(int(new_value), 0, 3), int(target_id)),
+ )
+ elif kind == "memory_pov_summary":
+ conn.execute(
+ "UPDATE memories SET pov_summary = ? WHERE id = ?",
+ (str(new_value), int(target_id)),
+ )
+ # Unknown target_kind: silently no-op for v1. Future kinds (activity
+ # fields, edge summary, knowledge_facts) extend the dispatch above.
diff --git a/chat/templates/_drawer.html b/chat/templates/_drawer.html
index 637a8d3..3ed1940 100644
--- a/chat/templates/_drawer.html
+++ b/chat/templates/_drawer.html
@@ -40,6 +40,18 @@
{{ host_bot.name }} → you
Affinity: {{ edge_b2y.affinity }}/100 · Trust: {{ edge_b2y.trust }}/100
+
{% if edge_b2y.summary %}
{{ edge_b2y.summary }}
{% endif %}
{% if edge_b2y.knowledge %}
Knowledge ({{ edge_b2y.knowledge|length }})
@@ -68,6 +80,12 @@
{{ ['·','•','★','★★'][m.significance|default(0)] }}
{{ m.pov_summary }}
+
{% endfor %}
@@ -84,6 +102,24 @@
{{ ['·','•','★','★★'][m.significance|default(0)] }}
{{ m.pov_summary[:200] }}{% if m.pov_summary|length > 200 %}…{% endif %}
+
+
{% endfor %}
diff --git a/chat/web/drawer.py b/chat/web/drawer.py
index 6c5a1e9..580c956 100644
--- a/chat/web/drawer.py
+++ b/chat/web/drawer.py
@@ -1,21 +1,37 @@
-"""Read-only chat drawer (T24).
+"""Chat drawer — read view (T24) and inline edits (T25).
-Returns an HTML partial rendered into the chat shell's `