Skip to content
Merged
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
5 changes: 3 additions & 2 deletions loopx/control_plane/work_items/stride_policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ function comparisonCandidates(value: unknown): JsonObject[] {
/** Agent interpretations are structured claims, not certified evidence or authority. */
export function normalizeStrideDecision(value: unknown, bounded: BoundedText = text):
[JsonObject | null, Record<string, number>] {
if (value === undefined) return [null, {}];
// Compact run history records absent optional decisions as JSON null.
// Absence is not a proposal and cannot satisfy an enabled stride review.
if (value == null) return [null, {}];
const input = requireJsonObject(value, "stride_decision");
// Off-mode transport and historical readback retain both earlier v1 shapes.
// An enabled fresh review requires the current concrete-plan schema.
Expand Down Expand Up @@ -194,7 +196,6 @@ export function normalizeStrideDecision(value: unknown, bounded: BoundedText = t

/** Historical read codec only, never a selectable strategy or new proposal. */
export function normalizeRecordedStrideDecision(value: unknown): JsonObject | null {
if (value == null) return null;
return normalizeStrideDecision(value)[0];
}
function normalizeHistoricalStrideDecision(value: unknown, bounded: BoundedText): [JsonObject, Record<string, number>] {
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 |
| --- | --- | --- | --- | --- |
| `vision_history_null_stride_decision` | Reusing the current Agent's complete Vision from `history` in `refresh-state --agent-vision-json` fails with `stride_decision must be an object`. | Compare the authored packet, compact history and status readback; retain the original operation identity and verify that rejection appended nothing. | The compact index represents an absent optional decision as JSON null while the write codec accepts only an omitted field. | Normalize null and omission as absence in the existing typed stride owner, shared by read and write codecs. Keep malformed proposals rejected and enabled stride reviews dependent on a current valid decision. Prove real File/SQLite history-to-writeback recovery, retained cumulative intent, unchanged-state refusals and off/on policy boundaries; do not rewrite historical receipts or count absence as a decision. |
| `async_command_completion_metadata_lost` | A code cell appears complete with empty stdout, then the agent searches saved files or reissues a stateful command without checking whether its child is still running. | The original tool result's running handle, terminal exit code and stderr; caller projection code; the same command's completion; original operation/capture identity. Keep empty success, empty failure and a pending command distinct. | The caller forwarded only stdout and dropped the command tool's completion metadata; a completed wrapper was mistaken for a completed child. Empty output alone cannot establish this cause in a historical trace whose inner result is unavailable. | Preserve the tool result envelope, retain a running handle and collect the same execution's terminal result. If the handle was lost, follow original-receipt or digest-bound capture recovery before any retry. Keep admission/lease/settlement fences and original identities; qualify with pending, silent-success and silent-failure controls. See targeted diagnostics; no timeout, quota, packet or authority change is implied. |
| `same_packet_json_type_alias` | Quota sharing replaces a distinct Vision or authoring observation with a reference because Boolean/number or signed-zero values compare equal in the host language. | Original captured values, projected body/ref, exact scalar types and zero signs, equal-content/key-order controls, actual CLI recovery and unchanged receipt/spend counts. | Language-level value equality was used as lossless JSON identity, while another sharing path already retained original serialized types. | Reuse one local serialization identity at the existing compatibility owner; share only complete equal JSON bodies and retain unique values/instructions. Preserve exact references, source input and existing reader/settlement ownership. Cover ordinary/native File/SQLite reads and digest refusal, and count transport rather than adding a late full-payload RPC. Keep installed/model adoption and whole-C1 benefit separate. |
| `original_conversation_cli_goal_instance_drift` | `manager-inbox original-conversation` accepts an explicit `--goal-instance-id` but returns success for a stale instance, or follows the currently registered generation instead. | The physical CLI argv, selected registry profile, explicit versus omitted instance, current Goal reference, and real HTTP/source/queue/return readback in disposable File/SQLite state. | The CLI branch dropped the selector before the existing collaboration owner; current-instance inference concealed the missing caller reference. An unsupported source profile is a separate boundary, not proof that forwarding alone enables it. | Validate and forward the explicit Goal reference through the existing owner. Preserve omitted-selector compatibility and keep stale-instance, source/grant and audience checks authoritative. Cover real CLI discovery, request and control, unchanged inferred discovery, stale-instance rejection without queued work, and original recipient/result return; do not promote profiles or infer grants to make a live request pass. |
Expand Down
34 changes: 31 additions & 3 deletions tests/control_plane/test_vision_budget_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import importlib.util
import json
from pathlib import Path
import subprocess
import sys

import pytest

Expand All @@ -13,8 +15,9 @@

@pytest.mark.parametrize("provider", ["file", "sqlite"])
@pytest.mark.parametrize("complete_prior", [False, True])
@pytest.mark.parametrize("reader", ["status", "history"])
def test_projected_autonomous_example_updates_evidence_without_changing_goal(
tmp_path, monkeypatch, provider, complete_prior,
tmp_path, monkeypatch, provider, complete_prior, reader,
):
from canonical_authority_fixture import isolate_sqlite_runtime

Expand Down Expand Up @@ -54,8 +57,18 @@ def test_projected_autonomous_example_updates_evidence_without_changing_goal(
if complete_prior:
# Follow the returned authoring procedure using the complete current
# Agent packet from a separate read, not fields reconstructed by a model.
current = fixture.run_status(registry, runtime)
goal = next(g for g in current["run_history"]["goals"] if g["id"] == fixture.GOAL_ID)
if reader == "status":
history = fixture.run_status(registry, runtime)["run_history"]
else:
# Compact history is a public readback too. Its index retains null
# for absent optional decisions, unlike the full status projection.
result = subprocess.run([
sys.executable, "-m", "loopx.cli", "--registry", str(registry),
"--runtime-root", str(runtime), "--format", "json", "history",
"--goal-id", fixture.GOAL_ID, "--agent-id", fixture.AGENT_ID,
], cwd=fixture.REPO_ROOT, capture_output=True, text=True, check=True)
history = fixture.payload(result)
goal = next(g for g in history["goals"] if g["id"] == fixture.GOAL_ID)
current_packet = copy.deepcopy(goal["latest_runs"][0]["agent_vision"])
assert current_packet["agent_id"] == fixture.AGENT_ID
current_packet["vision_patch"].update(packet["vision_patch"])
Expand All @@ -68,6 +81,7 @@ def test_projected_autonomous_example_updates_evidence_without_changing_goal(
assert result.returncode == 0, accepted.get("error") or accepted
assert accepted["ok"] and accepted["appended"]
assert accepted["agent_vision"]["path_delta"]["outcome"] == "replan"
assert "stride_decision" not in accepted["agent_vision"]
assert original_goal in state.read_text()

# A separate CLI read must preserve every existing field, not merely echo
Expand Down Expand Up @@ -105,6 +119,20 @@ def test_projected_autonomous_example_updates_evidence_without_changing_goal(
assert retained["agent_vision"]["path_delta"]["outcome"] == disposition
assert original_goal in state.read_text()

if complete_prior and reader == "history":
before = index.read_bytes()
for invalid in (False, 0, "", [], {}):
malformed = copy.deepcopy(packet)
malformed["stride_decision"] = invalid
fixture.write_json(document, malformed)
rejected = fixture.run_cli(registry, runtime, vision_path=document,
check=False, dry_run=False, autonomous_replan_recorded=False)
failure = fixture.payload(rejected)
assert rejected.returncode == 1
assert "stride_decision" in failure["error"]
assert failure["appended"] is False
assert index.read_bytes() == before


@pytest.mark.parametrize("character", ["x", "界"])
def test_full_budget_roundtrips_without_erasing_replan_or_partial_writes(tmp_path, character):
Expand Down
25 changes: 25 additions & 0 deletions tests/control_plane_ts/stride_policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,31 @@ const request = {schema_version: VISION_REFRESH_REQUEST_SCHEMA, phase: "prepare"
agent_id: "agent-a", merge_patch: false, existing_agent_vision: null,
require_path_delta_for_durable_change: false, agent_vision_packet: packet};

test("an absent recorded decision roundtrips without satisfying an enabled stride review", () => {
for (const value of [undefined, null]) {
const vision = buildVisionCheckpoint({...request,
agent_vision_packet: {...packet, stride_decision: value}}).agent_vision as Record<string, unknown>;
assert.equal(Object.hasOwn(vision, "stride_decision"), false);
for (const kind of ["vision_acceptance_gap", ...decisionReviewTriggers]) {
const obligation = {obligation_id: proposal.reference_obligation_id,
triggers: [{kind}], replan_context: {
stride_strategy: "evidence_conditioned_v1", context_id: contextId}};
const observation_delta = {delta_kinds: ["new_surface"], evidence_novel: true};
const missing = projectReplanSemantics({operation: "qualify", obligation,
observation_delta, agent_vision: vision});
assert.equal(missing.accepted, false, kind);
assert.equal(missing.reason_code, "stride_decision_required", kind);
assert.equal(projectReplanSemantics({operation: "qualify", obligation: {
...obligation, replan_context: {stride_strategy: "off"}},
observation_delta, agent_vision: vision}).accepted, true, kind);
}
}
for (const value of [false, 0, "", [], {}]) {
assert.throws(() => buildVisionCheckpoint({...request,
agent_vision_packet: {...packet, stride_decision: value}}));
}
});

test("previous-outcome claims need subsequent evidence; old comparison evidence remains usable", () => {
const previous = "replan-decision-" + "b".repeat(24);
const context = {evidence: [{evidence_ref: "evidence:before"}, {evidence_ref: "evidence:after"}],
Expand Down
Loading