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
11 changes: 10 additions & 1 deletion docs/reference/required-work-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,11 @@ An enabled native **full** reference for a recognized decision can include
`first_read_command`. It batches current admission/stop facts, the selected task,
whole Goal/Todo narrative, source availability/revisions and read routes. Detailed
history and optional authoring recipes remain expandable from the same capture.
The preference source's complete `current` view also stays in the initial read:
corrections, retirement/expiry and empty replacements retain their provenance,
revision and advisory authority. This captured pre-work view does not replace the
preference owner's fresh exact-scope read before a later preference-dependent
external action, and neither delivery nor a preference grants authority.
The existing bounded `unfinished_delegations` projection and `read_complete`
flag also remain visible. Adoption or a queued return does not establish
completion. Pending peer results, counts, omitted items and unavailable
Expand Down Expand Up @@ -217,7 +222,11 @@ activation defaults, saved files, Delta and full-read receipts are unchanged.
中文:已启用原生传输的完整引用可给出 `first_read_command`,合并当前准入、停止条件、
任务和完整 Goal/Todo 正文,以及来源状态、版本与读取入口;历史和可选格式说明仍在
同一捕获中按需展开。它是起始选读,不代表已读全部必要来源;依赖具体验收、证据或
执行/结算契约前仍须完整读取。未知 schema、过小引用预算和 warm delta 保留既有路线,
执行/结算契约前仍须完整读取。
偏好来源的完整 `current` 视图也直接呈现:纠偏、退役/过期和空视图替换均保留
原来源、版本及 advisory 权限边界。本次捕获的工作前视图不能替代偏好 owner 要求的
后续偏好相关外部动作前精确作用域新读;偏好及其交付均不授予权限。
未知 schema、过小引用预算和 warm delta 保留既有路线,
关闭原生传输的输出不变;完整恢复和原 SHA 校验均保留。只读的 `--initial-read` 不与
显式选读或索引混用;续页保留短选项和固定的选读摘要,profile 改变则拒绝混读。
首读也给出现有验收材料地址和当前结算义务。仅明确 `required=false` 的附件延后完整
Expand Down
6 changes: 4 additions & 2 deletions loopx/control_plane/quota/progressive_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,8 +234,10 @@ export function initialContextReadPointers(context: JsonObject, decisionFields:
// incomplete observations visible without expanding full request bodies.
include(...source, "content", "read_complete");
include(...source, "content", "unfinished_delegations");
include(...source, "content", "current", "revision");
include(...source, "content", "current", "instruction");
// The preference owner already supplies the complete current scoped view,
// including corrections, retirement, provenance and advisory authority.
// Its revision/instructions alone cannot fulfill that pre-work delivery.
include(...source, "content", "current");
include(...source, "content", "outcome_context");
const sourceRow = object(_source), content = object(sourceRow?.content);
const requirements = object(object(content?.goal_acceptance_contract)?.outcome_requirements);
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 |
| --- | --- | --- | --- | --- |
| `initial_preference_view_omission` | A fulfilled preference hook leaves corrections, retirement or advisory instructions absent from the recommended initial read. | The preference owner's complete current scoped view, actual selected reader/native input, empty and unavailable states, and same-capture continuation/recovery. | A hand-picked projection uses incomplete or stale child field names after the owner has already delivered the current view. | Reuse the complete owned current view in the existing initial selector; retain provenance, advisory authority, exact-scope freshness and immutable capture checks. Cover real File/SQLite lifecycle reads, empty replacements, unknown values and native consumer readback. Preserve changed-profile refusal and fresh recovery; disclose restored bytes as information cost, not compression or model benefit. |
| `immutable_reader_parent_alias` | A warm native view refuses an unchanged reader reference after a verified capture read normalizes its parent path. | Cold descriptor bytes, warm baseline path and full digest, parent aliases versus distinct leaf names, and actual CLI recovery after compaction. | The reader accepts normal parent aliases, but immutable publication compares their literal path spellings. | Compare only equivalent parent spellings at the existing IO owner, retaining the exact leaf and every other metadata value; never rewrite the descriptor. Preserve full-digest, selection, permission and final-symlink checks. Cover both publication orders, distinct files and hardlinks, cold/warm native publication and registry-free current recovery; keep transport repair separate from retention and net-benefit claims. |
| `todo_read_source_authority_label` | A canonical Todo read reports one provider in `authority_read` but labels the public result or projection source as File. | The same read's canonical provider revision, public source, envelope sources and Markdown across full, filtered, exact and compact views. | The shared formatter retained a provider-specific constant after provider routing moved to the canonical reader. | Reuse the existing canonical source authority for presentation and provenance. Exercise real CLI File/SQLite reads, missing results and source failure/recovery; preserve filtering, read-only behavior and fail-closed routing. A display correction does not select, migrate or qualify a provider. |
| `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. |
Expand Down
26 changes: 26 additions & 0 deletions tests/control_plane/test_agent_preferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,29 @@
)


def _assert_initial_preferences(payload, directory, expected):
from loopx.cli_commands.quota_capture import native_context_capture_view
from tests.control_plane.test_context_first_read import read_selection

directory.mkdir(mode=0o700)
sources = payload["interaction_contract"]["agent_channel"]["work_context"]["sources"]
index = next(i for i, row in enumerate(sources) if row.get("kind") == "agent_preferences")
view = native_context_capture_view(payload, directory)
original = Path(view["recovery"]["path"]).read_bytes()
_, values = read_selection(view["first_read_command"])
prefix = f"/interaction_contract/agent_channel/work_context/sources/{index}/content/current"
for field, value in expected.items():
wanted = prefix + "/" + field.replace("~", "~0").replace("/", "~1")
roots = [root for root in values if wanted == root or wanted.startswith(root + "/")]
assert roots, f"Initial read omitted current preference field: {field}"
root = max(roots, key=len)
actual = values[root]
for key in wanted[len(root):].split("/")[1:]:
actual = actual[key.replace("~1", "/").replace("~0", "~")]
assert actual == value
assert Path(view["recovery"]["path"]).read_bytes() == original


@pytest.mark.parametrize("provider", ["file", "sqlite"])
def test_inactive_preferences_are_silent_and_participating_hook_delivers_current_view(tmp_path, monkeypatch, provider):
nested = tmp_path / ("workspace dir" * 12)
Expand Down Expand Up @@ -64,6 +87,7 @@ def call(action, *args):
assert any(x["kind"] == "agent_preferences" for x in reads), quota["turn_start_capability_hook_dispatch"]
assert "designated-reviewer" in json.dumps(quota)
assert quota["interaction_contract"]["agent_channel"]["required_reads"] == []
_assert_initial_preferences(quota, tmp_path / "active-first-read", fresh["current"])
assert quota["capability_gate"]["action"] == off["capability_gate"]["action"]
rc, plan = _run_cli(registry, runtime, "turn", "plan", *scope)
assert rc == 0, plan
Expand Down Expand Up @@ -115,6 +139,7 @@ def call(action, *args):
contents = next(x["content"] for x in next_quota["interaction_contract"]["agent_channel"]["work_context"]["sources"]
if x.get("kind") == "agent_preferences")
assert contents["current"]["items"][0]["statement"] == "Do not delegate review."
_assert_initial_preferences(next_quota, tmp_path / "corrected-first-read", action_view["current"])
rc, replay = call("remember", *args, "--execute")
assert rc == 0 and replay["status"] == "replayed", replay
assert replay["current"]["items"][0]["statement"] == "Do not delegate review."
Expand All @@ -130,6 +155,7 @@ def call(action, *args):
if x.get("kind") == "agent_preferences")
assert contents["current"]["items"][0]["state"] == "retired"
assert contents["current"]["items"][0]["statement"] is None
_assert_initial_preferences(quota, tmp_path / "retired-first-read", later["current"])
rc, history = call("history")
assert rc == 0 and len(history["events"]) == 3, history
alias = tmp_path / "global-registry.json"
Expand Down
22 changes: 22 additions & 0 deletions tests/control_plane/test_context_first_read.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,28 @@ def test_unknown_settlement_plan_is_not_compacted(tmp_path, decision):
assert values["/interaction_contract/cli_channel/settlement_plan"] == plan


@pytest.mark.parametrize("current", [None, {}, {
"schema_version": "agent_preferences_v1", "authority": "advisory_only",
"revision": "empty-current", "instructions": "Replace prior scoped context, including an empty view.",
"items": [], "future_context": {"a/b~c": [None, False, 0, "Keep this complete current value."]},
}])
def test_initial_read_preserves_empty_and_unknown_current_preference_values(tmp_path, decision, current):
source = {"source": "turn_start_capability_hook", "kind": "agent_preferences",
"content": {"ok": current is not None, "status": "read" if current is not None else "unavailable",
"current": current}}
sources = decision["interaction_contract"]["agent_channel"]["work_context"]["sources"]
sources.append(source)
before = json.loads(json.dumps(decision))
view = native_context_capture_view(decision, tmp_path)
page, values = read_selection(view["first_read_command"])
pointer = f"/interaction_contract/agent_channel/work_context/sources/{len(sources) - 1}/content/current"
assert values[pointer] == current
assert not any(key.startswith(pointer + "/") for key in values) # No duplicate delivered descendants.
raw = Path(view["recovery"]["path"]).read_bytes()
assert json.loads(raw) == before == decision
assert capture_page_range(page, raw=raw, digest=page["sha256"]) is None


def test_initial_read_exposes_existing_outcome_addresses_without_loading_all_criteria(tmp_path, decision):
pointer = "/interaction_contract/agent_channel/work_context/sources/0/content/goal_acceptance_contract/outcome_requirements/outcomes/0"
outcome = {"id": "current-work", "supporting_material_pointer": pointer,
Expand Down
Loading