feat: drawer edits for edge_trust / edge_summary / memory_pov_summary / knowledge_facts (T72.1)

Adds the four POST routes whose state-layer support was already
dispatched by the manual_edit projector (edge_trust, edge_summary,
memory_pov_summary) plus a new edge_knowledge_fact dispatch branch for
add/remove fact list manipulation. Drawer template gains editable
textareas, sliders, and add/remove fact controls. Remove semantics on
knowledge_fact match by string (not index) so concurrent edge_update
events appending facts between drawer renders don't desync the form.
This commit is contained in:
Joseph Doherty
2026-04-26 17:24:24 -04:00
parent 789b9bd042
commit 21404a373b
4 changed files with 685 additions and 18 deletions
+37 -4
View File
@@ -6,7 +6,7 @@ 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 four target kinds:
Phase 1 covers five 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.
@@ -17,8 +17,12 @@ Phase 1 covers four target kinds:
field. Driven by T27 from the classifier's ``relationship_summary``
output combined with the prior summary.
Other §6.4 editable fields (activity verb / attention / posture,
knowledge_facts list manipulation) are deferred to Phase 1.5.
T72.1 (Phase 2.5) adds one list-shaped edit:
- ``edge_knowledge_fact`` — add/remove a single fact on an edge's
``knowledge_json`` list. Payload carries an ``action`` of ``"add"`` or
``"remove"`` and a ``fact`` string; remove matches the first occurrence
by string equality so the route handler doesn't have to track fact
indices across re-renders.
Pin toggles intentionally use the existing ``memory_pin_changed`` event
(registered in :mod:`chat.state.memory`) rather than ``manual_edit`` so
@@ -27,6 +31,7 @@ the projection writes both ``pinned`` and ``auto_pinned`` atomically.
from __future__ import annotations
import json
from sqlite3 import Connection
from chat.eventlog.log import Event
@@ -87,5 +92,33 @@ def _apply_manual_edit(conn: Connection, e: Event) -> None:
target_id["target_id"],
),
)
elif kind == "edge_knowledge_fact":
# T72.1: add or remove a single fact on an edge's knowledge list.
# ``target_id`` is the {"source_id", "target_id"} edge pair;
# ``new_value`` carries ``{"action": "add"|"remove", "fact": str}``.
# Remove matches by string equality (first occurrence) so callers
# don't have to thread a fact_index through re-rendered drawers.
action = new_value["action"]
fact = str(new_value["fact"])
row = conn.execute(
"SELECT knowledge_json FROM edges "
"WHERE source_id = ? AND target_id = ?",
(target_id["source_id"], target_id["target_id"]),
).fetchone()
if row is not None:
knowledge = json.loads(row[0])
if action == "add":
knowledge.append(fact)
elif action == "remove" and fact in knowledge:
knowledge.remove(fact)
conn.execute(
"UPDATE edges SET knowledge_json = ? "
"WHERE source_id = ? AND target_id = ?",
(
json.dumps(knowledge),
target_id["source_id"],
target_id["target_id"],
),
)
# Unknown target_kind: silently no-op for v1. Future kinds (activity
# fields, knowledge_facts list manipulation) extend the dispatch above.
# fields, etc.) extend the dispatch above.