"""Per-user Synapse reaction settings + instruction version history (postgres).
Reaction instructions are the contract between the user and the agent: the
dispatcher meta-pass and every reaction run read them before acting. The agent
itself may edit the document (self-improvement) — every edit lands in
synapse_instruction_versions with an `edited_by` label, so the history doubles
as the audit log of the agent touching its own rules.
"""
from datetime import datetime, timezone
from typing import Literal
from pydantic import BaseModel
PushTarget = Literal["app", "app_synapse", "synapse"]
CompletionNotify = Literal["always", "important", "never"]
_VERSIONS_KEEP = 100 # versions kept per user; older ones are trimmed
class SynapseSettings(BaseModel):
user_id: str
reactions_enabled: bool = False
push_target: PushTarget = "app"
completion_notify: CompletionNotify = "important"
instructions: str = ""
updated_at: datetime | None = None
class InstructionVersion(BaseModel):
content: str
edited_by: str
reason: str | None = None
created_at: datetime
class SynapseSettingsStore:
def __init__(self, pool):
self._pool = pool
async def get(self, user_id: str) -> SynapseSettings:
row = await self._pool.fetchrow(
"SELECT user_id, reactions_enabled, push_target, completion_notify, instructions, updated_at "
"FROM synapse_settings WHERE user_id = $1",
user_id,
)
if row is None:
return SynapseSettings(user_id=user_id)
return SynapseSettings(
user_id=row["user_id"],
reactions_enabled=row["reactions_enabled"],
push_target=row["push_target"],
completion_notify=row["completion_notify"],
instructions=row["instructions"],
updated_at=row["updated_at"],
)
async def save(self, settings: SynapseSettings) -> None:
now = datetime.now(timezone.utc)
await self._pool.execute(
"""
INSERT INTO synapse_settings
(user_id, reactions_enabled, push_target, completion_notify, instructions, updated_at)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (user_id) DO UPDATE SET
reactions_enabled = EXCLUDED.reactions_enabled,
push_target = EXCLUDED.push_target,
completion_notify = EXCLUDED.completion_notify,
instructions = EXCLUDED.instructions,
updated_at = EXCLUDED.updated_at
""",
settings.user_id, settings.reactions_enabled, settings.push_target,
settings.completion_notify, settings.instructions, now,
)
async def save_instructions(
self, user_id: str, content: str, edited_by: Literal["user", "navi"],
reason: str | None = None,
) -> None:
"""Update the instructions document and record the edit in the history.
One transaction: the version row and the settings row never disagree.
"""
now = datetime.now(timezone.utc)
async with self._pool.acquire() as conn, conn.transaction():
await conn.execute(
"""
INSERT INTO synapse_settings (user_id, instructions, updated_at)
VALUES ($1, $2, $3)
ON CONFLICT (user_id) DO UPDATE SET
instructions = EXCLUDED.instructions,
updated_at = EXCLUDED.updated_at
""",
user_id, content, now,
)
await conn.execute(
"""
INSERT INTO synapse_instruction_versions (user_id, content, edited_by, reason, created_at)
VALUES ($1, $2, $3, $4, $5)
""",
user_id, content, edited_by, reason, now,
)
await conn.execute(
"""
DELETE FROM synapse_instruction_versions
WHERE user_id = $1 AND id NOT IN (
SELECT id FROM synapse_instruction_versions
WHERE user_id = $1 ORDER BY created_at DESC, id DESC LIMIT $2
)
""",
user_id, _VERSIONS_KEEP,
)
async def list_versions(self, user_id: str, limit: int = 20) -> list[InstructionVersion]:
rows = await self._pool.fetch(
"SELECT content, edited_by, reason, created_at "
"FROM synapse_instruction_versions WHERE user_id = $1 "
"ORDER BY created_at DESC, id DESC LIMIT $2",
user_id, limit,
)
return [InstructionVersion(**r) for r in rows]