Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions docs/status-data-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -2299,6 +2299,13 @@ coverage; legacy runtime goals are extra. An unknown `--goal-id` is reported as
the consumer rule are in the
[projection envelope contract](reference/contracts/projection-envelope-contract.md).

An unreadable optional cache (including invalid UTF-8 or malformed JSON) is a
cache miss with `miss_reason=unreadable`, not a status-collection failure.
`status` and `quota should-run` collect live source facts instead. Reading does
not repair or delete the damaged cache; `--write-projection-cache` explicitly
replaces it with the newly collected projection. Canonical source errors still
retain their normal diagnostics and admission rules.

## Decision Freshness Summary

`decision_freshness_summary` is an optional checkpointed-decision projection over
Expand Down
29 changes: 29 additions & 0 deletions examples/control_plane/status-projection-cache-smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,35 @@ def main() -> int:
assert_cache_expired(expired_quota)
assert_next_action(expired_quota, successor_id)

# A damaged optional cache cannot hide the current canonical successor.
for previous, command in (
(expired_status, ("status",)),
(expired_quota, (
"quota", "should-run", "--goal-id", GOAL_ID,
"--agent-id", AGENT_ID, "--available-capability", "shell",
)),
):
path = Path(str(cache_metadata(previous)["path"]))
damaged = b'{"payload":"\xe2\x82'
path.write_bytes(damaged)
args = (*command, "--scan-path", str(public_doc), "--limit", "5")
recovered = run_cli(registry_path, *args, "--use-projection-cache")
assert recovered["ok"] is True, recovered
assert cache_metadata(recovered)["hit"] is False, recovered
assert cache_metadata(recovered)["miss_reason"] == "unreadable", recovered
assert successor_id in json.dumps(recovered), recovered
assert path.read_bytes() == damaged
if command[0] == "quota":
assert_next_action(recovered, successor_id)

rewritten = run_cli(
registry_path, *args, "--use-projection-cache", "--write-projection-cache",
)
assert_cache_written(rewritten)
reused = run_cli(registry_path, *args, "--use-projection-cache")
assert_cache_hit(reused)
assert successor_id in json.dumps(reused), reused

public_doc.write_text(
f"Public leak probe must still be caught by default quota scan: {PRIVATE_DOC_MARKER}\n",
encoding="utf-8",
Expand Down
2 changes: 1 addition & 1 deletion loopx/control_plane/runtime/status_projection_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ def load_status_projection_cache(
return None, metadata
try:
cache_record = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
metadata["miss_reason"] = "unreadable"
metadata["error"] = str(exc)
return None, metadata
Expand Down
1 change: 1 addition & 0 deletions skills/loopx-self-repair/references/repair-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ teaches a reusable control-plane lesson.

| Pattern | Symptoms | Evidence To Read | Likely Root | Durable Repair |
| --- | --- | --- | --- | --- |
| `status_cache_decode_failure` | Opt-in cached status or quota readback fails with a UTF-8 decoding error while live collection works. | Cache miss metadata and a disposable malformed-cache reproduction through both CLI consumers. | The shared cache reader handles JSON and filesystem errors but lets text decoding abort live fallback. | Classify invalid UTF-8 as an unreadable cache miss in the existing IO owner; preserve damaged bytes on reads, collect live facts, and rewrite only with the explicit cache-write option. Test current Todo selection and successful reuse after repair; never suppress canonical source failures. |
| `scheduler_deadline_bucket_reset_gap` | A generated tighter deadline ACK fails with `progression_regression`, even after the host applies the requested RRULE. | Saved failed ACK, originating Turn, prior/current progression buckets and reset tokens, current host readback, and exact generated argv. | The deadline changes the effective progression while an override reset profile omits those buckets, so index zero still carries the old identity. | Bind effective buckets through the existing reset-profile digest; keep time within a bucket stable. Preserve native monotonic, host-readback, receipt-freshness, CAS and replay guards. Reproduce 30-minute/index-one to 15-minute/index-zero through disposable File/SQLite CLI and separately qualify the installed artifact; never edit active scheduler state or infer runtime adoption from source tests. |
| `quiet_receipt_turn_start_context_loss` | A delegated request is delivered, but a quiet heartbeat reports no required inbox read and leaves it unassessed. | Original inbox delivery and assessment readback, initial hook dispatch, the captured final guard packet, and the automatic no-spend receipt reconstruction path. | The CLI observes pending context, then rebuilds the decision after stall writeback without forwarding that same Turn's hook dispatch. | Forward the original dispatch through the existing live-decision owner during reconstruction; preserve pending, empty and failed observations without re-running hooks or granting delivery/spend. Cover first and replayed stall receipts through final capture and CLI projection, including negative authority checks. Assess and link the original request, execute its admitted work, and report the actual result through its native return route; delivery or adoption alone is not completion. |
| `benchmark_planning_clock_projection_gap` | A resumed task closes work using an earlier Host deadline even though a continuation was explicitly authorized; readable history and an old ready plan are mistaken for current executable work. | Original task constraints, Host phase receipts, saved model input, current authorized continuation, actual quota/frontier readback and terminal dispositions; keep private trajectories outside public artifacts. | Historical Host timing and closure context survived recovery without the new continuation intent; ordinary planning should not inject Host countdowns. | Keep Host deadlines, remaining time and execution reserves out of ordinary model input; retain external enforcement and task-owned time constraints. At the explicitly authorized archive-recovery boundary, preserve old history and closure records, identify earlier Host clocks as historical in the new continuation request, and reuse Goal/Todo/replan with a stable operation identity. Read back actionable work or review before calling continuation ready; an old plan or exit code zero is insufficient. Do not renew budgets on retry, reopen without authorization, clear a user stop, filter task deadlines or bypass evaluation cooldowns. Cover clock-free fresh/resumed input, idempotent recovery and legitimate stopping; qualify real model adoption and score gains separately. |
Expand Down
18 changes: 18 additions & 0 deletions tests/test_projection_envelope.py
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,24 @@ def test_cached_status_keeps_observed_at_and_restamps_staleness(tmp_path: Path,
assert "(cached)" in render_status_markdown(cached)


@pytest.mark.parametrize("content", [b"\xff", b'{"payload":"\xe2\x82', b'{"payload":'])
def test_unreadable_cache_is_a_non_mutating_miss(tmp_path: Path, content: bytes) -> None:
args = _cache_args(_registry(tmp_path))
path = status_projection_cache_path(
args["runtime_root"], status_projection_cache_key(**args)
)
path.parent.mkdir(parents=True)
path.write_bytes(content)

cached, metadata = load_status_projection_cache(max_age_seconds=3600, **args)

assert cached is None
assert metadata["hit"] is False
assert metadata["miss_reason"] == "unreadable"
assert metadata["error"]
assert path.read_bytes() == content


def test_cache_entry_without_an_envelope_is_a_miss(tmp_path: Path) -> None:
registry_path = _registry(tmp_path)
args = _cache_args(registry_path)
Expand Down
Loading