feat: preserve Today timing ledger across handoff (Closes #1276)
CI / lint (pull_request) Successful in 3m48s
CI / build-release (pull_request) Successful in 8s
CI / browser-journey (pull_request) Successful in 5m42s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-22 19:38:30 +00:00
1 parent e0d8ab9f10
commit 548b95ddb6
9 files changed
+267 -20

No files matched your search

+16 -1
View File
@@ -656,18 +656,33 @@ class TodayRecap(BaseModel):
items: list[TodayRecapItem] = Field(min_length=1, max_length=20)
class TodaySessionEntry(BaseModel):
identity: str = Field(min_length=1, max_length=500)
elapsed_ms: int = Field(ge=0, le=7 * 24 * 60 * 60 * 1000)
class TodaySessionUpdate(BaseModel):
base_revision: int = Field(ge=0)
device_id: str = Field(min_length=1, max_length=100)
identity: str = Field(max_length=500)
elapsed_ms: int = Field(ge=0, le=7 * 24 * 60 * 60 * 1000)
entries: list[TodaySessionEntry] | None = Field(default=None, max_length=20)
running: bool
break_deadline_at: int | None = Field(default=None, ge=0, le=10_000_000_000_000)
@model_validator(mode="after")
def validate_break_is_paused(self):
def validate_session_state(self):
if self.running and self.break_deadline_at is not None:
raise ValueError("break must remain paused")
if self.entries is not None:
identities = [entry.identity for entry in self.entries]
if len(set(identities)) != len(identities):
raise ValueError("Today session entry identities must be unique")
active = [entry for entry in self.entries if entry.identity == self.identity]
if self.identity and (len(active) != 1 or active[0].elapsed_ms != self.elapsed_ms):
raise ValueError("active Today session must match its ledger entry")
if not self.identity and self.entries:
raise ValueError("inactive Today session ledger must be empty")
return self
+45 -4
View File
@@ -1001,9 +1001,31 @@ class TodayStore:
def _empty_session() -> dict:
return {
"revision": 0, "device_id": "", "identity": "",
"elapsed_ms": 0, "running": False, "break_deadline_at": None, "updated_at": None,
"elapsed_ms": 0, "entries": [], "running": False,
"break_deadline_at": None, "updated_at": None,
}
@staticmethod
def _normalize_session_entries(entries: list[dict]) -> list[dict]:
if not isinstance(entries, list) or len(entries) > 20:
raise ValueError("Today session entries are limited to 20 items")
normalized = []
seen = set()
for item in entries:
identity = item.get("identity", "") if isinstance(item, dict) else ""
elapsed_ms = item.get("elapsed_ms") if isinstance(item, dict) else None
if not isinstance(identity, str) or not identity.strip() or len(identity) > 500:
raise ValueError("Today session entry identity is required and bounded")
identity = identity.strip()
if identity in seen:
raise ValueError("Today session entry identities must be unique")
if (not isinstance(elapsed_ms, int) or isinstance(elapsed_ms, bool)
or elapsed_ms < 0 or elapsed_ms > 7 * 24 * 60 * 60 * 1000):
raise ValueError("Today session elapsed time is invalid")
seen.add(identity)
normalized.append({"identity": identity, "elapsed_ms": elapsed_ms})
return normalized
def _session_snapshot(self, row, login: str) -> tuple[dict, bool]:
if row is None:
return self._empty_session(), False
@@ -1011,17 +1033,28 @@ class TodayStore:
payload, legacy = self._cipher.open(row[1], binding=f"session:{login}")
if not isinstance(payload, dict):
raise PrivateStateEncryptionError("private state could not be decrypted")
if "entries" not in payload:
payload["entries"] = ([{
"identity": payload.get("identity", ""),
"elapsed_ms": int(payload.get("elapsed_ms", 0)),
}] if payload.get("identity") else [])
legacy = True
return {"revision": int(row[0]), **payload, "updated_at": row[6]}, legacy
return {
session = {
"revision": int(row[0]), "device_id": row[1], "identity": row[2],
"elapsed_ms": int(row[3]), "running": bool(row[4]),
"break_deadline_at": row[5], "updated_at": row[6],
}, True
}
session["entries"] = ([{
"identity": session["identity"], "elapsed_ms": session["elapsed_ms"],
}] if session["identity"] else [])
return session, True
def _sealed_session(self, login: str, session: dict) -> str:
return self._cipher.seal({
"device_id": session["device_id"], "identity": session["identity"],
"elapsed_ms": session["elapsed_ms"], "running": session["running"],
"entries": session["entries"],
"break_deadline_at": session["break_deadline_at"],
}, binding=f"session:{login}")
@@ -1044,6 +1077,7 @@ class TodayStore:
def update_session(
self, login: str, *, base_revision: int, device_id: str,
identity: str, elapsed_ms: int, running: bool, break_deadline_at: int | None = None,
entries: list[dict] | None = None,
) -> dict:
login = self._normalize_login(login)
updated_at = self.clock()
@@ -1058,9 +1092,16 @@ class TodayStore:
if base_revision != current_revision:
raise TodaySessionConflict(current_session)
revision = current_revision + 1
normalized_entries = self._normalize_session_entries(
entries if entries is not None else
([{"identity": identity, "elapsed_ms": elapsed_ms}] if identity else [])
)
active_entries = [entry for entry in normalized_entries if entry["identity"] == identity]
if identity and (len(active_entries) != 1 or active_entries[0]["elapsed_ms"] != elapsed_ms):
raise ValueError("active Today session must match its ledger entry")
session = {
"revision": revision, "device_id": device_id, "identity": identity,
"elapsed_ms": elapsed_ms, "running": bool(running),
"elapsed_ms": elapsed_ms, "entries": normalized_entries, "running": bool(running),
"break_deadline_at": break_deadline_at, "updated_at": updated_at,
}
connection.execute(