Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
83f4cdc
fix(agentctl): keep full corpus out of lane verification
Sinity Sep 4, 2026
e19bfbe
fix(agentctl): let affected verification await pytest slot
Sinity Sep 4, 2026
71b51a4
fix(agentctl): leave affected verification to GitHub
Sinity Sep 4, 2026
cec8e0e
fix(devtools): scope descriptor-only verification
Sinity Sep 4, 2026
070cbc8
fix(devtools): satisfy verifier test typing
Sinity Sep 4, 2026
d25a2f1
test(devtools): isolate verifier routing contract
Sinity Sep 4, 2026
424a2f2
fix(agentctl): keep manual affected verification in pytest pool
Sinity Sep 4, 2026
423e44a
fix: Run hosted verification pytest inside the workflow job
Sinity Sep 4, 2026
ea41272
fix: Classify every pytest-pool worker and hermetically test the slot
Sinity Sep 4, 2026
2be36f3
fix(ci): keep hosted pytest in the host queue
Sinity Sep 4, 2026
9e56d04
test(devtools): pin the corpus operation's checkout and schedule
Sinity Sep 4, 2026
521ae22
fix(devtools): reap the pytest slot through agentctl
Sinity Sep 4, 2026
b690f13
chore(lane): stop tracking lane publication text
Sinity Sep 4, 2026
ba262ce
docs: describe required affected PR verification
Sinity Sep 5, 2026
de5585b
refactor: route CLI status through operation kernel
Sinity Sep 4, 2026
47c6d03
fix: Invalidate derived session products on late-parent resolution
Sinity Sep 4, 2026
abd32c3
fix: Gate thread and tag-rollup status counts on their product tables
Sinity Sep 4, 2026
b805ee2
fix: Declare the read guard's degradable schema objects
Sinity Sep 4, 2026
7ea964a
fix(storage): apply the chain reduction to replay summary events
Sinity Sep 4, 2026
1dc9bf2
fix: Restore the sessions record projection to SESSIONS_SPEC
Sinity Sep 4, 2026
f563965
fix: Preserve dispatch links across parent replacement and aliases
Sinity Sep 4, 2026
b1934d2
perf(tests): build one archive per module in the CLI snapshot and sch…
Sinity Sep 4, 2026
c26cacb
test(cli): reflect absent optional status relations
Sinity Sep 4, 2026
54a9c4e
fix: preserve exact FTS staleness through search
Sinity Sep 4, 2026
9be123e
test(fts): seed exact freshness before scoped repair
Sinity Sep 4, 2026
577242f
test(fts): preserve exact stale identity evidence
Sinity Sep 5, 2026
642fab2
fix(storage): converge the ops tier instead of refusing it
Sinity Sep 3, 2026
0f79fe2
test(storage): cover stale ops identity convergence
Sinity Sep 4, 2026
b2159cf
refactor: dissolve the insight readiness verdict into convergence debt
Sinity Sep 4, 2026
9bec5f3
fix: gate session-insight status counts on the relations they read
Sinity Sep 4, 2026
8694bd1
test(cli): update planner statistics snapshot
Sinity Sep 5, 2026
66b4d3c
test(storage): follow multi-relation status descriptors
Sinity Sep 5, 2026
381f886
fix(status): surface convergence ledger read failures
Sinity Sep 5, 2026
1655f66
feat: Compile and consume the physical blob disposition plan
Sinity Sep 4, 2026
92ec879
fix: Own AI Studio attachments on same-timestamp document turns
Sinity Sep 4, 2026
10769ce
fix: Apply the conversational-evidence law in the one-shot importer
Sinity Sep 4, 2026
642f78d
test: Cover attachment reacquisition across capture revisions
Sinity Sep 4, 2026
ad0e32f
perf: Size the archive-ingest parse pool from the walk it found
Sinity Sep 4, 2026
68bef3b
fix: Correct embedding preservation safety and scale findings
Sinity Sep 4, 2026
bbaf1fb
test: Cover fresh-archive attachment provenance round trip
Sinity Sep 4, 2026
de09057
test: add query execution envelope lab check
Sinity Sep 4, 2026
d53f325
feat: add query execution envelope lab check
Sinity Sep 4, 2026
15d3d1a
fix: record incompatible envelope environments
Sinity Sep 4, 2026
3db65f5
fix: drop resurrected index fast-forward command spec
Sinity Sep 4, 2026
11f9f1d
fix: refuse unmeasurable envelope samples
Sinity Sep 4, 2026
96a0fcc
fix(storage): retain ambiguous attachment evidence
Sinity Sep 5, 2026
756f967
Merge origin/master into integration/polylogue-reindex-recovery-20260905
Sinity Sep 5, 2026
892eaf8
fix(storage): report an owner-ambiguous orphan instead of raising
Sinity Sep 5, 2026
c36e83c
Merge origin/master (f80542bc5)
Sinity Sep 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions devtools/command_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,17 @@ def to_dict(self) -> dict[str, object]:
use_when="Assert memory budgets around a concrete query or archive-facing command.",
examples=("devtools bench memory --max-rss-mb 1536 -- polylogue --plain analyze",),
),
CommandSpec(
"bench query-envelope",
"benchmarking",
"Measure repeated incident-scale query RSS, PSS, swap, and temp envelopes.",
"devtools.query_execution_envelope",
json_flag=False,
use_when="Run the opt-in live archive proof for repeated aggregate query_units calls and emit a receipt.",
examples=(
"devtools bench query-envelope --archive-root /path/to/archive --receipt .cache/query-envelope.json",
),
),
CommandSpec(
"archive lineage-validation",
"archive",
Expand Down
311 changes: 311 additions & 0 deletions devtools/query_execution_envelope.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
"""Measure repeated incident-scale query resource envelopes.

The command is an opt-in lab check. It opens the supplied archive through the
public query route and never writes to the archive.
"""

from __future__ import annotations

import argparse
import asyncio
import json
import os
import threading
import time
from dataclasses import asdict, dataclass
from pathlib import Path
from shutil import disk_usage
from typing import Any

from polylogue import Polylogue
from polylogue.core.errors import SchemaVersionMismatchError

DEFAULT_EXPRESSION = "actions where tool:shell | group by tool | count"
DEFAULT_ROUNDS = 20
DEFAULT_WARMUP = 3
DEFAULT_BASELINE = 5
DEFAULT_TOLERANCE = 0.25
DEFAULT_MAX_RSS_MB = 1536
DEFAULT_MAX_PSS_MB = 1536
DEFAULT_MAX_SWAP_GROWTH_MB = 64
DEFAULT_MAX_TEMP_GROWTH_MB = 64
MIB = 1024 * 1024


@dataclass(frozen=True, slots=True)
class ResourceSample:
"""One process and temporary-filesystem observation."""

rss_bytes: int
pss_bytes: int
swap_bytes: int
temp_delta_bytes: int


PROC_MEMORY_FIELDS = ("VmRSS", "Pss", "VmSwap")


class ResourceProbeUnavailableError(RuntimeError):
"""procfs did not report a field the declared envelope is measured against."""


def _parse_proc_memory(text: str) -> tuple[int, int, int]:
"""Return RSS, PSS, and swap in bytes from concatenated procfs reports.

A missing field is refused rather than defaulted to zero: a zero sample
satisfies every declared limit, so the envelope would pass without
measuring anything.
"""
values: dict[str, int] = {}
for line in text.splitlines():
key, _, value = line.partition(":")
if key not in PROC_MEMORY_FIELDS:
continue
fields = value.split()
if not fields:
continue
values[key] = int(fields[0]) * 1024
missing = [field for field in PROC_MEMORY_FIELDS if field not in values]
if missing:
raise ResourceProbeUnavailableError(f"procfs did not report {', '.join(missing)}")
return values["VmRSS"], values["Pss"], values["VmSwap"]


def _proc_memory() -> tuple[int, int, int]:
"""Return current RSS, PSS, and swap from procfs for this process."""
try:
status = Path("/proc/self/status").read_text(encoding="ascii")
smaps_rollup = Path("/proc/self/smaps_rollup").read_text(encoding="ascii")
except (OSError, ValueError) as exc:
raise ResourceProbeUnavailableError(f"procfs memory reports are unreadable: {exc}") from exc
return _parse_proc_memory(status + smaps_rollup)


def _temp_used_bytes(temp_root: Path) -> int:
"""Return used bytes on the filesystem containing the temp root."""
try:
usage = disk_usage(temp_root)
except OSError:
return 0
Comment on lines +86 to +89

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refuse an unmeasured temporary-filesystem envelope

When TMPDIR names a missing or unreadable path, disk_usage raises and this helper substitutes zero for used space. Both the baseline and every measured sample then report zero temporary growth, so the command can emit status: succeeded even though one of its four declared resource dimensions was never measured. Propagate a typed probe-unavailable error here, as the procfs probes already do, rather than allowing an environmental measurement failure to satisfy the limit.

Useful? React with 👍 / 👎.

return usage.total - usage.free


async def _query_once(archive: Polylogue, expression: str) -> dict[str, Any]:
envelope = await archive.query_units(expression, limit=100)
return envelope.model_dump(mode="json")


async def measure_query_envelope(
archive_root: Path,
*,
expression: str = DEFAULT_EXPRESSION,
rounds: int = DEFAULT_ROUNDS,
warmup: int = DEFAULT_WARMUP,
baseline_rounds: int = DEFAULT_BASELINE,
tolerance: float = DEFAULT_TOLERANCE,
sample_interval_s: float = 0.05,
max_rss_bytes: int = DEFAULT_MAX_RSS_MB * MIB,
max_pss_bytes: int = DEFAULT_MAX_PSS_MB * MIB,
max_swap_growth_bytes: int = DEFAULT_MAX_SWAP_GROWTH_MB * MIB,
max_temp_growth_bytes: int = DEFAULT_MAX_TEMP_GROWTH_MB * MIB,
) -> dict[str, Any]:
"""Run repeated aggregate reads and return a resource receipt."""
if rounds < 20:
raise ValueError("rounds must be at least 20")
if warmup < 0 or baseline_rounds < 1:
raise ValueError("warmup must be non-negative and baseline_rounds must be positive")
if tolerance < 0 or sample_interval_s < 0:
raise ValueError("tolerance and sample interval must be non-negative")
if min(max_rss_bytes, max_pss_bytes, max_swap_growth_bytes, max_temp_growth_bytes) < 0:
raise ValueError("resource envelope limits must be non-negative")

archive_root = archive_root.resolve()
db_path = archive_root / "index.db"
if not db_path.is_file():
raise FileNotFoundError(db_path)
Comment on lines +122 to +125

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resolve the active index generation before measuring

For a valid recovery state where .index-active-pointer selects the promoted generation but the conventional index.db is stale, this explicit path bypasses resolve_active_index_path; Polylogue treats an explicit db_path as pinned and measures the stale database instead of the public archive generation. The resulting resource receipt can therefore pass for data that is not currently served, so resolve and pin the active index path once before opening the API and use that same path for receipt metadata.

Useful? React with 👍 / 👎.


temp_root = Path(os.environ.get("TMPDIR", "/tmp"))
temp_before = _temp_used_bytes(temp_root)
initial_rss, initial_pss, initial_swap = _proc_memory()
initial = ResourceSample(initial_rss, initial_pss, initial_swap, 0)
peak = initial
stop = threading.Event()

def observe() -> ResourceSample:
nonlocal peak
rss, pss, swap = _proc_memory()
candidate = ResourceSample(rss, pss, swap, max(0, _temp_used_bytes(temp_root) - temp_before))
peak = ResourceSample(
max(peak.rss_bytes, candidate.rss_bytes),
max(peak.pss_bytes, candidate.pss_bytes),
max(peak.swap_bytes, candidate.swap_bytes),
max(peak.temp_delta_bytes, candidate.temp_delta_bytes),
)
Comment on lines +139 to +143

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Serialize updates to the sampled resource peak

The sampler thread and the async caller both execute observe() and perform an unlocked read-modify-write of peak. If one observes a high RSS/PSS/temp value while the other computes from the previous lower peak, the later assignment can overwrite the high sample with lower component values, allowing an absolute envelope check to pass despite a real peak violation. Protect the aggregate with a lock or collect immutable samples and reduce them after the sampler stops.

Useful? React with 👍 / 👎.

return candidate

def sample() -> None:
# The measured loop observes every round on this thread too, so a
# persistent probe failure still reaches the caller.
while not stop.is_set():
try:
observe()
except ResourceProbeUnavailableError:
return
Comment on lines +150 to +153

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Propagate failures from the background resource sampler

If a background sample encounters a transient unreadable or incomplete procfs report after the initial probe, this handler silently terminates the sampler without recording the failure. Later per-round observations can succeed, allowing the command to report succeeded even though between-round peaks were unmeasured for the rest of the run; retain the sampler error and return blocked-env after joining instead of treating it as normal completion.

Useful? React with 👍 / 👎.

time.sleep(sample_interval_s)

sampler = threading.Thread(target=sample, name="query-envelope-sampler", daemon=True)
sampler.start()
started = time.perf_counter()
result_counts: list[int] = []
samples: list[dict[str, Any]] = []
try:
async with Polylogue(archive_root=archive_root, db_path=db_path) as archive:
for phase, count in (("warmup", warmup), ("baseline", baseline_rounds), ("measured", rounds)):
for round_number in range(count):
result_count = len((await _query_once(archive, expression)).get("items", []))
result_counts.append(result_count)
sample_now = observe()
samples.append(
{
"phase": phase,
"round": round_number + 1,
"result_item_count": result_count,
**asdict(sample_now),
}
)
quiescent = observe()
finally:
stop.set()
sampler.join(timeout=2)

elapsed_ms = round((time.perf_counter() - started) * 1000, 3)
baseline = [
ResourceSample(item["rss_bytes"], item["pss_bytes"], item["swap_bytes"], item["temp_delta_bytes"])
for item in samples
if item["phase"] == "baseline"
]
measured = [
ResourceSample(item["rss_bytes"], item["pss_bytes"], item["swap_bytes"], item["temp_delta_bytes"])
for item in samples
if item["phase"] == "measured"
]
baseline_rss = max(sample.rss_bytes for sample in baseline)
baseline_pss = max(sample.pss_bytes for sample in baseline)
baseline_swap = max(sample.swap_bytes for sample in baseline)
baseline_temp = max(sample.temp_delta_bytes for sample in baseline)
final = measured[-3:]
return_checks = {
"rss": all(current.rss_bytes <= max(1, baseline_rss) * (1 + tolerance) for current in final),
"pss": all(current.pss_bytes <= max(1, baseline_pss) * (1 + tolerance) for current in final),
"swap": all(current.swap_bytes <= baseline_swap + max_swap_growth_bytes for current in final),
"temp": all(current.temp_delta_bytes <= baseline_temp + max_temp_growth_bytes for current in final),
}
absolute_checks = {
"rss": peak.rss_bytes <= max_rss_bytes,
"pss": peak.pss_bytes <= max_pss_bytes,
"swap": peak.swap_bytes <= initial_swap + max_swap_growth_bytes,
"temp": peak.temp_delta_bytes <= max_temp_growth_bytes,
}
returned = all(return_checks.values()) and all(absolute_checks.values())
return {
"status": "succeeded" if returned else "failed",
"archive_root": str(archive_root),
"archive_generation": db_path.resolve().parent.name,
"archive_index_bytes": db_path.stat().st_size,
"expression": expression,
"rounds": rounds,
"warmup_rounds": warmup,
"baseline_rounds": baseline_rounds,
"tolerance": tolerance,
"declared_envelope": {
"max_rss_bytes": max_rss_bytes,
"max_pss_bytes": max_pss_bytes,
"max_swap_growth_bytes": max_swap_growth_bytes,
"max_temp_growth_bytes": max_temp_growth_bytes,
"return_tolerance": tolerance,
},
"steady_state_baseline": {
"rss_bytes": baseline_rss,
"pss_bytes": baseline_pss,
"swap_bytes": baseline_swap,
"temp_delta_bytes": baseline_temp,
},
"peak": asdict(peak),
"initial_sample": asdict(initial),
"quiescent_sample": asdict(quiescent),
"final_samples": [asdict(sample) for sample in final],
"samples": samples,
"return_checks": return_checks,
"absolute_checks": absolute_checks,
"result_item_counts": result_counts,
"elapsed_ms": elapsed_ms,
"returned_to_envelope": returned,
"regression_path": (
"Rerun this command against the same promoted generation. A failed status identifies RSS/PSS, "
"swap, temp, or return-to-baseline drift; compare the named check and samples."
),
}


def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--archive-root", type=Path, required=True)
parser.add_argument("--rounds", type=int, default=DEFAULT_ROUNDS)
parser.add_argument("--warmup", type=int, default=DEFAULT_WARMUP)
parser.add_argument("--baseline-rounds", type=int, default=DEFAULT_BASELINE)
parser.add_argument("--tolerance", type=float, default=DEFAULT_TOLERANCE)
parser.add_argument("--sample-interval", type=float, default=0.05)
parser.add_argument("--max-rss-mb", type=int, default=DEFAULT_MAX_RSS_MB)
parser.add_argument("--max-pss-mb", type=int, default=DEFAULT_MAX_PSS_MB)
parser.add_argument("--max-swap-growth-mb", type=int, default=DEFAULT_MAX_SWAP_GROWTH_MB)
parser.add_argument("--max-temp-growth-mb", type=int, default=DEFAULT_MAX_TEMP_GROWTH_MB)
parser.add_argument("--expression", default=DEFAULT_EXPRESSION)
parser.add_argument("--receipt", type=Path)
args = parser.parse_args(argv)
try:
receipt = asyncio.run(
measure_query_envelope(
args.archive_root,
expression=args.expression,
rounds=args.rounds,
warmup=args.warmup,
baseline_rounds=args.baseline_rounds,
tolerance=args.tolerance,
sample_interval_s=args.sample_interval,
max_rss_bytes=args.max_rss_mb * MIB,
max_pss_bytes=args.max_pss_mb * MIB,
max_swap_growth_bytes=args.max_swap_growth_mb * MIB,
max_temp_growth_bytes=args.max_temp_growth_mb * MIB,
)
)
except (SchemaVersionMismatchError, ResourceProbeUnavailableError) as exc:
regression_path = (
"Re-run against the exact promoted generation after its schema lifecycle action completes; "
"do not bypass the archive compatibility check."
if isinstance(exc, SchemaVersionMismatchError)
else "Re-run on a host whose procfs reports VmRSS, Pss, and VmSwap; the envelope is unmeasured here."
)
receipt = {
"status": "blocked-env",
"archive_root": str(args.archive_root.resolve()),
"blocking_error": str(exc),
"regression_path": regression_path,
}
text = json.dumps(receipt, indent=2, sort_keys=True)
print(text)
if args.receipt:
args.receipt.parent.mkdir(parents=True, exist_ok=True)
args.receipt.write_text(text + "\n", encoding="utf-8")
return 2
except (FileNotFoundError, ValueError) as exc:
parser.error(str(exc))
text = json.dumps(receipt, indent=2, sort_keys=True)
print(text)
if args.receipt:
args.receipt.parent.mkdir(parents=True, exist_ok=True)
args.receipt.write_text(text + "\n", encoding="utf-8")
return 0 if receipt["returned_to_envelope"] else 3


if __name__ == "__main__":
raise SystemExit(main())
1 change: 1 addition & 0 deletions docs/devtools.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ These are the commands worth remembering during normal repo work:
| --- | --- |
| `devtools bench memory` | Measure query-memory envelopes on generated fixtures. |
| `devtools bench pipeline` | Run typed pipeline probes against synthetic, staged, or archive-subset inputs. |
| `devtools bench query-envelope` | Measure repeated incident-scale query RSS, PSS, swap, and temp envelopes. |
| `devtools bench slo` | Check read-surface latency budgets in docs/plans/slo-catalog.yaml against benchmark measurements. |

### Archive
Expand Down
38 changes: 38 additions & 0 deletions docs/maintenance.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,44 @@ recorded separately in the receipt so a retry can safely continue an additive
repair. Reindex acceptance runs the same closure check against the candidate
index before promotion.

### `polylogue ops maintenance blob-disposition` — physical namespace disposition

One-time transition tooling for the blob-store maneuver. `plan` is read-only:
it walks the complete physical namespace and gives every object exactly one
disposition proven against a configured source — `source_present`,
`superseded_prefix`, `restore_required`, or `unresolved`. A plan is acceptable
only at zero unresolved members, and its digest binds the archive identity,
the namespace, the denominators, and every member outcome.

```bash
polylogue ops maintenance blob-disposition plan \
--archive-root /path/to/archive \
--output /path/to/disposition-plan.json --output-format json
polylogue ops maintenance blob-disposition apply \
--archive-root /path/to/archive \
--plan /path/to/disposition-plan.json \
--authorized-digest <digest of the reviewed plan> \
--receipt /path/to/new/disposition-receipt.json --active
```

`restore` is the additive half on its own: it publishes sole-copy carriers
into their ordinary spool and deletes nothing, so it does not wait on the
plan reaching zero unresolved. `apply` is a dry rehearsal without `--active`. It makes no classification
judgment: it revalidates every member's own proof immediately before its
effect, restores sole-copy carriers into their ordinary spool before any
deletion, never touches a historical carrier during restoration, and deletes
only unreferenced members through the canonical blob-GC seam. Any drift — a
changed source, a changed object, a new referent, a different digest or
denominator — refuses the whole plan.

Hook-event and browser-capture carriers are proven by the owning production
read route, not by bytes: acquisition derives fields the spool file does not
carry, so byte equality would misreport reproducible material as a sole copy.

Deletion trigger: this command, both maintenance modules, and their tests are
removed with the terminal disposition receipt. The recurring liveness,
publication, GC, and spool-admission laws stay with their owners.

### `polylogue ops maintenance preview` — staleness inventory

Read-only. Produces a per-model inventory of stale, missing, orphan,
Expand Down
8 changes: 7 additions & 1 deletion polylogue/cli/commands/maintenance/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,9 +219,15 @@
"blob_residue_compare_command",
"Compare present blob-residue candidates through the production parse route.",
),
(
"blob-disposition",
"_blob_disposition",
"blob_disposition_group",
"Compile or consume the physical blob namespace disposition plan.",
),
)

_NESTED_GROUP_COMMANDS = frozenset({"archive-root-relocation", "source-continuity-recovery"})
_NESTED_GROUP_COMMANDS = frozenset({"archive-root-relocation", "blob-disposition", "source-continuity-recovery"})


@click.group("maintenance")
Expand Down
Loading