knowledge_store: committer parameter on record and transaction

This commit is contained in:
CREDO23 2026-07-28 22:04:52 +02:00
parent aa27c2fb47
commit cca2b9480a
4 changed files with 42 additions and 9 deletions

View file

@ -17,7 +17,10 @@ class Revision:
"""One recorded point in a workspace's history (a whole-tree snapshot)."""
id: str
#: Whose content change this is (the acting user).
author: str
#: Who recorded it (the agent for agent turns; equals author otherwise).
committer: str
message: str
created_at: datetime
@ -64,10 +67,12 @@ class VersionedContentEngine(ABC):
removes: Iterable[str],
message: str,
author: str,
committer: str | None = None,
) -> str | None:
"""Append ``writes`` and ``removes`` to history as one revision.
Returns the revision id, or ``None`` when nothing changed.
``committer`` defaults to ``author``. Returns the revision id, or
``None`` when nothing changed.
"""
@abstractmethod

View file

@ -62,6 +62,7 @@ class GitContentEngine(VersionedContentEngine):
removes: Iterable[str],
message: str,
author: str,
committer: str | None = None,
) -> str | None:
self._ensure_exists()
repo = Repo(str(self._path))
@ -78,12 +79,11 @@ class GitContentEngine(VersionedContentEngine):
if not self._has_pending_changes(repo):
return None
author_bytes = author.encode()
revision = porcelain.commit(
repo,
message=message.encode(),
author=author_bytes,
committer=author_bytes,
author=author.encode(),
committer=(committer or author).encode(),
)
return revision.decode()
finally:
@ -289,6 +289,7 @@ class GitContentEngine(VersionedContentEngine):
return Revision(
id=commit.id.decode(),
author=commit.author.decode(),
committer=commit.committer.decode(),
message=commit.message.decode().strip(),
created_at=datetime.fromtimestamp(commit.commit_time, tz=UTC),
)

View file

@ -38,14 +38,19 @@ class KnowledgeStore:
return cls(workspace_id, engine)
@asynccontextmanager
async def transaction(self, *, message: str, author: str):
async def transaction(
self, *, message: str, author: str, committer: str | None = None
):
"""Atomic unit of work: verbs staged in the scope become one revision
on clean exit; an exception records nothing."""
on clean exit; an exception records nothing.
``author`` is whose content change this is; ``committer`` (default
``author``) is who recorded it the agent identity for agent turns."""
tx = Transaction()
yield tx
async with workspace_write_lock(self._workspace_id):
tx.revision = await asyncio.to_thread(
self._record_revision, tx, message, author
self._record_revision, tx, message, author, committer
)
async def read_as_of(self, revision: str, path: str) -> bytes:
@ -100,10 +105,14 @@ class KnowledgeStore:
return self._engine.compute_content_id(data)
def _record_revision(
self, tx: Transaction, message: str, author: str
self, tx: Transaction, message: str, author: str, committer: str | None
) -> str | None:
"""Resolve the transaction into one change set and record it."""
writes, removes = tx.resolve(self._engine.read)
return self._engine.record(
writes=writes, removes=removes, message=message, author=author
writes=writes,
removes=removes,
message=message,
author=author,
committer=committer,
)

View file

@ -89,6 +89,24 @@ class TestHistoryQueries:
assert rev.author == AUTHOR
assert rev.created_at.tzinfo is not None
def test_revision_distinguishes_author_from_committer(self, engine):
agent = "SurfSense Agent <agent@surfsense>"
engine.record(
writes={"a.xml": b"1"},
removes=[],
message="a1",
author=AUTHOR,
committer=agent,
)
rev = engine.list_revisions()[0]
assert rev.author == AUTHOR
assert rev.committer == agent
def test_committer_defaults_to_author(self, engine):
engine.record(writes={"a.xml": b"1"}, removes=[], message="a1", author=AUTHOR)
rev = engine.list_revisions()[0]
assert rev.committer == AUTHOR
def test_list_changes_reports_kinds_and_content_ids(self, engine):
first = engine.record(
writes={"a.xml": b"a1", "b.xml": b"b1"},