feat(memory): add incremental transcript compaction

This commit is contained in:
somew1nd committed 2026-09-01 17:46:44 -07:00
1 parent cbe8f4f0c6
commit 41ae386a3a
65 files changed
+9971 -815

No files matched your search

+59
View File
@@ -380,3 +380,62 @@ def test_update_step_summary_includes_step_number_in_sse(tmp_path):
assert last_update["step_number"] == 1
def _foreground_engine(tmp_path):
mock_ctx = MagicMock(spec=ArtemisContext)
mock_execution_setup = MagicMock()
mock_execution_setup.traces_path = str(tmp_path)
mock_ctx.execution_setup = mock_execution_setup
mock_ctx.device = None
engine = DataEngine(mock_ctx)
engine.start_session("foreground_app persistence")
return engine
def test_record_step_persists_explicit_foreground_app(tmp_path):
"""M5: the historically dropped foreground_app parameter is persisted."""
engine = _foreground_engine(tmp_path)
engine.record_step(
foreground_app="com.android.settings",
ui_tree=[{"package": "com.other.app", "bounds": "[0,0][10,10]"}],
summary="Explicit parameter wins",
)
for t in list(engine._pending_threads):
t.join()
step = engine.storage.get_steps(engine.current_session_id)[0]
assert step.extra_metadata["foreground_app"] == "com.android.settings"
def test_record_step_derives_foreground_app_from_ui_tree(tmp_path):
"""Without the parameter, the dominant non-overlay package is stamped."""
engine = _foreground_engine(tmp_path)
engine.record_step(
ui_tree=[
{"package": "com.android.systemui", "bounds": "[0,0][10,10]"},
{"package": "com.google.android.deskclock"},
{
"packageName": "com.google.android.deskclock",
"children": [{"package": "com.google.android.deskclock"}],
},
],
summary="Derived from UI tree",
)
for t in list(engine._pending_threads):
t.join()
step = engine.storage.get_steps(engine.current_session_id)[0]
assert step.extra_metadata["foreground_app"] == "com.google.android.deskclock"
def test_record_step_without_app_data_stamps_nothing(tmp_path):
engine = _foreground_engine(tmp_path)
engine.record_step(
ui_tree=[{"package": "com.android.systemui"}],
summary="Only overlay packages",
)
for t in list(engine._pending_threads):
t.join()
step = engine.storage.get_steps(engine.current_session_id)[0]
assert "foreground_app" not in (step.extra_metadata or {})
@@ -0,0 +1,105 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""History-chunk storage (M3 §6.1): append-only versioned rows keyed by step range."""
from uuid import uuid4
import pytest
from artemis.data_engine.models import HistoryChunkRecord
from artemis.data_engine.storage import StorageManager
SESSION = "11111111-1111-1111-1111-111111111111"
@pytest.fixture
def storage(tmp_path):
return StorageManager(tmp_path / "test.db", tmp_path / "traces")
def _chunk(start: int, end: int, version: int = 1, **overrides) -> HistoryChunkRecord:
fields = {
"chunk_id": uuid4(),
"session_id": SESSION,
"start_step_id": f"sid-{start}",
"end_step_id": f"sid-{end}",
"start_step_number": start,
"end_step_number": end,
"source_step_ids": [f"sid-{n}" for n in range(start, end + 1)],
"subgoal_hash": "hash-a",
"version": version,
"status": "pending",
"band1": {},
"band2": None,
"band3": f"- Step {start} (T+00:10): Tapped 'x' at [1, 2] -> executed",
"rendered_text": "[Chunk 1 | ...]",
}
fields.update(overrides)
return HistoryChunkRecord(**fields)
def test_history_chunk_roundtrip(storage):
storage.create_history_chunk(_chunk(1, 4))
chunks = storage.get_history_chunks(SESSION)
assert len(chunks) == 1
rec = chunks[0]
assert (rec.start_step_number, rec.end_step_number) == (1, 4)
assert rec.source_step_ids == [f"sid-{n}" for n in range(1, 5)]
assert rec.subgoal_hash == "hash-a"
assert rec.status == "pending"
assert "T+00:10" in rec.band3
def test_history_chunk_versions_are_append_only_latest_wins(storage):
storage.create_history_chunk(_chunk(1, 4, version=1, status="pending"))
storage.create_history_chunk(
_chunk(
1,
4,
version=2,
status="ready",
band1={"doing": "d", "verified_facts": ["f1"]},
band2=" - Steps 1–4: did things",
)
)
latest = storage.get_history_chunks(SESSION)
assert len(latest) == 1
assert latest[0].version == 2
assert latest[0].status == "ready"
assert latest[0].band1["verified_facts"] == ["f1"]
trail = storage.get_history_chunks(SESSION, all_versions=True)
assert [r.version for r in trail] == [1, 2]
def test_history_chunks_ordered_by_step_range(storage):
storage.create_history_chunk(_chunk(9, 12))
storage.create_history_chunk(_chunk(1, 4))
storage.create_history_chunk(_chunk(5, 8))
latest = storage.get_history_chunks(SESSION)
assert [(r.start_step_number, r.end_step_number) for r in latest] == [
(1, 4),
(5, 8),
(9, 12),
]
def test_history_chunks_are_session_scoped(storage):
storage.create_history_chunk(_chunk(1, 4))
other = "22222222-2222-2222-2222-222222222222"
storage.create_history_chunk(_chunk(1, 4, session_id=other))
assert len(storage.get_history_chunks(SESSION)) == 1
assert len(storage.get_history_chunks(other)) == 1
@@ -0,0 +1,171 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for the versioned, status-carrying step summary write (M0 §6.1)."""
from unittest.mock import MagicMock
from artemis.context import ArtemisContext
from artemis.data_engine.engine import DataEngine
def _make_engine(tmp_path):
mock_ctx = MagicMock(spec=ArtemisContext)
mock_execution_setup = MagicMock()
mock_execution_setup.traces_path = str(tmp_path)
mock_ctx.execution_setup = mock_execution_setup
mock_ctx.device = None
engine = DataEngine(mock_ctx)
engine.start_session("summary versioning test")
return engine
def _flush(engine):
for t in list(engine._pending_threads):
t.join()
def _get_step(engine, step_id):
steps = engine.storage.get_steps(engine.current_session_id)
for step in steps:
if str(step.step_id) == str(step_id):
return step
return None
def test_ready_write_persists_summary_and_metadata(tmp_path):
engine = _make_engine(tmp_path)
step_id = engine.record_step(summary="initial")
_flush(engine)
applied = engine.storage.update_step_summary(
step_id,
"Tapped the login button.",
source="visual_transition",
model="gemini-2.5-flash-lite",
status="ready",
)
assert applied is True
step = _get_step(engine, step_id)
assert step.summary == "Tapped the login button."
assert step.extra_metadata["summary_status"] == "ready"
assert step.extra_metadata["summary_source"] == "visual_transition"
assert step.extra_metadata["summary_version"] == 1
assert step.extra_metadata["summary_model"] == "gemini-2.5-flash-lite"
def test_auto_version_increments_per_write(tmp_path):
engine = _make_engine(tmp_path)
step_id = engine.record_step(summary="initial")
_flush(engine)
engine.storage.update_step_summary(step_id, "v1", status="ready")
engine.storage.update_step_summary(step_id, "v2", status="ready")
step = _get_step(engine, step_id)
assert step.summary == "v2"
assert step.extra_metadata["summary_version"] == 2
def test_stale_explicit_version_is_dropped(tmp_path):
engine = _make_engine(tmp_path)
step_id = engine.record_step(summary="initial")
_flush(engine)
assert engine.storage.update_step_summary(step_id, "newer", version=5, status="ready")
applied = engine.storage.update_step_summary(step_id, "older", version=3, status="ready")
assert applied is False
step = _get_step(engine, step_id)
assert step.summary == "newer"
assert step.extra_metadata["summary_version"] == 5
def test_pending_does_not_downgrade_ready(tmp_path):
engine = _make_engine(tmp_path)
step_id = engine.record_step(summary="initial")
_flush(engine)
engine.storage.update_step_summary(step_id, "done", status="ready")
applied = engine.storage.update_step_summary(step_id, None, status="pending")
assert applied is False
step = _get_step(engine, step_id)
assert step.summary == "done"
assert step.extra_metadata["summary_status"] == "ready"
def test_failed_marks_status_without_touching_summary(tmp_path):
engine = _make_engine(tmp_path)
step_id = engine.record_step(summary="original text")
_flush(engine)
engine.storage.update_step_summary(step_id, None, status="pending", source="visual_transition")
applied = engine.storage.update_step_summary(
step_id, None, status="failed", source="visual_transition"
)
assert applied is True
step = _get_step(engine, step_id)
assert step.summary == "original text"
assert step.extra_metadata["summary_status"] == "failed"
# A failed write must never clobber an already-ready summary.
engine.storage.update_step_summary(step_id, "recovered", status="ready")
applied = engine.storage.update_step_summary(step_id, None, status="failed")
assert applied is False
step = _get_step(engine, step_id)
assert step.extra_metadata["summary_status"] == "ready"
def test_missing_step_returns_false(tmp_path):
from uuid import uuid4
engine = _make_engine(tmp_path)
assert engine.storage.update_step_summary(uuid4(), "text", status="ready") is False
def test_engine_level_write_carries_metadata_and_publishes_ready_only(tmp_path):
engine = _make_engine(tmp_path)
published = []
engine.subscribe(lambda event_type, data: published.append((event_type, data)))
step_id = engine.record_step(summary="initial")
_flush(engine)
published.clear()
# Non-ready statuses persist metadata but publish no SSE.
engine.update_step_summary(step_id, None, status="pending", source="visual_transition")
_flush(engine)
assert not [e for e in published if e[0] == "step_updated"]
step = _get_step(engine, step_id)
assert step.extra_metadata["summary_status"] == "pending"
engine.update_step_summary(
step_id,
"Ready text.",
status="ready",
source="visual_transition",
model="test-model",
)
_flush(engine)
ready_events = [e for e in published if e[0] == "step_updated"]
assert len(ready_events) == 1
assert ready_events[0][1]["summary"] == "Ready text."
step = _get_step(engine, step_id)
assert step.summary == "Ready text."
assert step.extra_metadata["summary_status"] == "ready"
assert step.extra_metadata["summary_model"] == "test-model"