607d0971c4
Memories grow per-flag witness checkboxes (you / host / guest) that
auto-submit on change via HTMX. The new POST route emits a manual_edit
event with target_kind=memory_witness and a {flag, value} payload;
prior_value mirrors the same shape so an inverse edit restores the
flag. The drawer's recent-memories query now selects the three
witness columns alongside the existing fields so the template can
render checkbox state without a second query per row.
143 lines
5.6 KiB
Python
143 lines
5.6 KiB
Python
"""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 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.
|
|
- ``memory_pov_summary`` — textarea edit (string). Also reused by T27's
|
|
scene-close pipeline to rewrite per-turn raw narratives into a proper
|
|
per-POV scene summary.
|
|
- ``edge_summary`` — string overwrite of the directed edge's ``summary``
|
|
field. Driven by T27 from the classifier's ``relationship_summary``
|
|
output combined with the prior summary.
|
|
|
|
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.
|
|
|
|
T72.3 adds a per-flag witness toggle:
|
|
- ``memory_witness`` — flip one of ``witness_you`` / ``witness_host`` /
|
|
``witness_guest`` on a memory row. Payload's ``new_value`` is a dict
|
|
``{"flag": "you"|"host"|"guest", "value": 0|1}`` and ``prior_value``
|
|
mirrors the same shape so an inverse edit can restore the flag.
|
|
|
|
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
|
|
|
|
import json
|
|
from sqlite3 import Connection
|
|
|
|
from chat.eventlog.log import Event
|
|
from chat.eventlog.projector import on
|
|
|
|
_VALID_WITNESS_FLAGS = {"you", "host", "guest"}
|
|
|
|
|
|
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)),
|
|
)
|
|
elif kind == "edge_summary":
|
|
# ``target_id`` here is a {"source_id", "target_id"} pair like
|
|
# the affinity/trust edits, since edges are keyed by the
|
|
# directed pair, not a single rowid.
|
|
conn.execute(
|
|
"UPDATE edges SET summary = ? "
|
|
"WHERE source_id = ? AND target_id = ?",
|
|
(
|
|
str(new_value),
|
|
target_id["source_id"],
|
|
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"],
|
|
),
|
|
)
|
|
elif kind == "memory_witness":
|
|
# T72.3: toggle one of the three witness flags on a memory row.
|
|
# ``new_value`` is the dict ``{"flag", "value"}``; ``prior_value``
|
|
# mirrors the same shape so an inverse edit restores the flag.
|
|
flag = new_value["flag"]
|
|
if flag in _VALID_WITNESS_FLAGS:
|
|
conn.execute(
|
|
f"UPDATE memories SET witness_{flag} = ? WHERE id = ?",
|
|
(1 if int(new_value["value"]) else 0, int(target_id)),
|
|
)
|
|
# Unknown target_kind: silently no-op for v1. Future kinds (activity
|
|
# fields, etc.) extend the dispatch above.
|