* feat(history): add bounded chronological read projections (#209) * fix(history): validate internal continuation positions (#209) * feat(history): expose fenced history windows and run details (#209) * fix(history): release retired projection connections (#209) * feat(history): resolve reported outputs through current authorization (#209) * fix(history): reject invalid inspection targets (#209) * feat(history): add scoped history state and current-file adapter (#209) * fix(history): preserve slideshow and clear stale reentry actions (#209) * feat(history): render recorded executions and frozen run details (#209) * fix(history): recover receipts and refresh capture evidence (#209) * test(history): verify chronological history across restore and host modes (#209) * fix(history): enforce chronological marker visual parity (#209) * fix(history): resolve final chronological review findings Address #209 recovery, bounded visit restoration, exact receipt availability, and portable regression coverage. * test(history): repair Windows fixtures and review feedback Address #209 and PR #227 native path, cleanup, media and review-comment expectations. Document Windows portability preflight without changing production behavior. * ci: raise the viewerjs job budget for the larger browser suite The cross-browser workflows suite grew from 396 to 496 tests on this branch, which pushed the whole viewerjs job to about 25.5 minutes against the 25 minute cap set in #181. Two exact-head runs on this branch timed out without any test failing: run 39173019283 was killed on test 474 of 496, and run 39175125474 printed "496 passed (12.8m)" and was then cancelled during Playwright shutdown. Raise only the job budget. Browser projects, workers and per-test timeouts are unchanged. Splitting the browsers into parallel jobs is tracked separately.
128 lines
4.4 KiB
Python
128 lines
4.4 KiB
Python
"""Bounded exact-path saved display within the caller's history snapshot."""
|
|
|
|
import os
|
|
import sqlite3
|
|
|
|
try:
|
|
from ..utils.inspection_response import MAX_LIBRARY_BYTES, _library_projection
|
|
except ImportError:
|
|
from utils.inspection_response import MAX_LIBRARY_BYTES, _library_projection
|
|
|
|
|
|
def _display_bytes(value: bytes | None) -> str | None:
|
|
if value is None:
|
|
return None
|
|
return value.decode("utf-8", "replace")
|
|
|
|
|
|
def _tags(
|
|
conn: sqlite3.Connection, owner: int, *, pair: bool
|
|
) -> tuple[list[str], bool]:
|
|
table, column = (
|
|
("prompt_pair_tags", "pair_id") if pair else ("prompt_tags", "prompt_id")
|
|
)
|
|
tags = []
|
|
used = 0
|
|
cursor = conn.execute(
|
|
f"SELECT substr(CAST(t.name AS BLOB),1,?) FROM {table} pt "
|
|
f"JOIN tags t ON t.id=pt.tag_id WHERE pt.{column}=? LIMIT 129",
|
|
(MAX_LIBRARY_BYTES + 1, owner),
|
|
)
|
|
for row in cursor:
|
|
used += len(row[0])
|
|
if used > MAX_LIBRARY_BYTES or len(tags) == 128:
|
|
return [], True
|
|
tags.append(_display_bytes(row[0]))
|
|
return tags, False
|
|
|
|
|
|
def _saved_projection(conn: sqlite3.Connection, row: sqlite3.Row) -> tuple[dict, dict]:
|
|
saved = {"prompt_id": row["prompt_id"]}
|
|
pairs = list(
|
|
conn.execute(
|
|
"SELECT pp.id,pp.positive_prompt_id,"
|
|
"substr(CAST(pp.title AS BLOB),1,?) AS title,"
|
|
"substr(CAST(pp.category AS BLOB),1,?) AS category "
|
|
"FROM image_prompt_pairs ip JOIN prompt_pairs pp ON pp.id=ip.pair_id "
|
|
"WHERE ip.image_id=? LIMIT 2",
|
|
(MAX_LIBRARY_BYTES + 1, MAX_LIBRARY_BYTES + 1, row["id"]),
|
|
)
|
|
)
|
|
if len(pairs) > 1:
|
|
return saved, _library_projection({"status": "limited"})
|
|
pair = pairs[0] if pairs else None
|
|
title = pair["title"] if pair is not None else None
|
|
category = pair["category"] if pair is not None else row["category"]
|
|
tags, limited = _tags(
|
|
conn,
|
|
pair["id"] if pair is not None else row["prompt_id"],
|
|
pair=pair is not None,
|
|
)
|
|
limited = limited or any(
|
|
value is not None and len(value) > MAX_LIBRARY_BYTES
|
|
for value in (title, category)
|
|
)
|
|
if pair is not None:
|
|
saved["pair"] = {
|
|
"id": pair["id"],
|
|
"positive_prompt_id": pair["positive_prompt_id"],
|
|
}
|
|
library = _library_projection(
|
|
{
|
|
"status": "linked",
|
|
"positive_id": row["prompt_id"],
|
|
"pair_id": pair["id"] if pair is not None else None,
|
|
"title": _display_bytes(title),
|
|
"category": _display_bytes(category),
|
|
"tags": tags,
|
|
"metadata_scope": "pair" if pair is not None else "positive",
|
|
}
|
|
)
|
|
if limited:
|
|
library = _library_projection({"status": "limited", "tags": []})
|
|
return saved, library
|
|
|
|
|
|
def read_history_gallery(
|
|
conn: sqlite3.Connection, paths: tuple[str, ...]
|
|
) -> dict[str, dict]:
|
|
"""Read at most two rows/path and two pair associations/unique row; no writes."""
|
|
if not conn.in_transaction:
|
|
raise sqlite3.ProgrammingError("Saved history display requires caller snapshot")
|
|
if (
|
|
type(paths) is not tuple
|
|
or len(paths) > 100
|
|
or any(
|
|
type(path) is not str
|
|
or not os.path.isabs(path)
|
|
or path != os.path.normcase(os.path.normpath(path))
|
|
or "\0" in path
|
|
for path in paths
|
|
)
|
|
):
|
|
raise ValueError("Invalid history gallery paths")
|
|
result = {}
|
|
for path in dict.fromkeys(paths):
|
|
rows = list(
|
|
conn.execute(
|
|
"SELECT gi.id,gi.prompt_id,"
|
|
"substr(CAST(p.category AS BLOB),1,?) AS category "
|
|
"FROM generated_images gi JOIN prompts p ON p.id=gi.prompt_id "
|
|
"WHERE gi.file_path=? OR gi.image_path=? LIMIT 2",
|
|
(MAX_LIBRARY_BYTES + 1, path, path),
|
|
)
|
|
)
|
|
match = "none" if not rows else "unique" if len(rows) == 1 else "ambiguous"
|
|
saved, library = None, _library_projection(
|
|
{"status": "none" if not rows else "ambiguous"}
|
|
)
|
|
if match == "unique":
|
|
saved, library = _saved_projection(conn, rows[0])
|
|
result[path] = {
|
|
"gallery_match": match,
|
|
"gallery_image_id": rows[0]["id"] if match == "unique" else None,
|
|
"saved": saved,
|
|
"library": library,
|
|
}
|
|
return result
|