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
49 changes: 45 additions & 4 deletions docs/reference/goal-acceptance-observations.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,20 +101,61 @@ Goal 完成;要求/工作变化使依据过期,产物修改后须重新核
Native `goal-acceptance` and outcome-update `refresh-state` rejections retain
their stable error code. A known outcome validation rule also supplies optional
`error_detail`, such as `source ref must be an opaque identifier`, in JSON and
Markdown. This detail contains fixed rule text, never the rejected value or
provider/IO context. Validation rejection writes nothing: after confirming that
result, correct the document and retry its operation with the still-current
Markdown. This detail contains fixed rule text and validator-measured byte
counts, never the rejected value or provider/IO context. Validation rejection
writes nothing: after confirming that result, correct the document and retry
its operation with the still-current
provider revision. An uncertain write still follows the original receipt and
recovery rules. Successful operations and unmarked failures keep their existing
behavior.

中文:原生 `goal-acceptance` 和成果更新 `refresh-state` 拒绝保留稳定错误码。
已知成果校验规则还会在 JSON 与 Markdown 中提供可选 `error_detail`,例如
`source ref must be an opaque identifier`。详情只包含固定规则,不回显被拒值或
`source ref must be an opaque identifier`。详情只包含固定规则及校验器实测字节数,不回显被拒值或
provider/IO 上下文。校验拒绝不写入状态;确认这一结果后,修正文档并沿用原操作和
仍有效的 provider revision 重试。写入结果不确定时继续遵循原有回执与恢复规则。
成功操作和未标记错误保持既有行为。

Supporting outcomes share two independent 131072-byte compact UTF-8 JSON
budgets across the Goal: declarations excluding current reviews, and current
reviews measured as the remaining bytes of the same normalized snapshot. A
rejection identifies the failing budget and reports its candidate, retained and
limit bytes. Retained means that budget before this patch in the current Goal
lifetime. A fresh lifetime has zero retained bytes; no current reviews means
zero retained review bytes. These are storage observations, not model tokens, packet size or measured execution cost. The
64-source limit per outcome remains independent. Omitting `source` on an
ordinary review avoids adding provenance but cannot bypass either byte budget.

For review overflow, shorten only the proposed review notes and evidence
references while preserving each conclusion and its provenance; keep full
evidence in its owning material. For declaration overflow, shortening reviews
cannot help. Preserve original requirements and origins and report the capacity
blocker if no lossless update fits. Never prune history, raise a limit or create
a replacement outcome to make the same work fit. Retain the failed response;
no automatic compaction or retry occurs.

中文:整个 Goal 的累计成果共享两个独立的 131072 字节紧凑 UTF-8 JSON 预算:
不含当前复核的声明,以及同一规范化快照中当前复核增加的字节。拒绝时指出超限的
预算,并给出该预算的拟写入、保留状态和上限字节数;保留状态指本次修改前、当前
Goal 生命周期中的对应字节,新生命周期或尚无复核时相应值为零。这是存储体积,
不能代替模型 token、packet 大小或实测执行成本;每项成果 64 个来源的限制仍独立。
普通复核省略 `source` 可以避免追加来源,但不能绕过任一种字节预算。

复核超限时,仅精简拟写入复核的重复说明和证据引用,保留每项结论及溯源,完整
证据留在原材料中。声明超限时,缩短复核没有作用。保留原要求与来源,无法无损
写入时报告容量阻塞。不得清理历史、提高上限或新建替代成果规避限制;保留失败
响应。系统不自动压缩或重试。

The existing native conversation tool also retains this owner-qualified detail.
Its request envelope is immutable: after a confirmed rejection, a corrected
document needs a new operation id and current readback. An unchanged retry of an
uncertain write still recovers the original operation. Generic provider/IO errors
continue to use the existing sanitized rejection.

中文:既有原生对话工具也保留这种 owner 校验诊断。对话请求封套不可改写;确认
拒绝后,修正文档须使用新操作 ID 和当前读回。结果不确定时仍以原样请求恢复原操作。
普通 provider/IO 错误保持原有脱敏拒绝。

<a id="owner-authorized-contract-v0"></a>

## Executable checks within default acceptance
Expand Down
11 changes: 11 additions & 0 deletions loopx/chat_loopx_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -1065,12 +1065,23 @@ def dispatch(name, arguments):
emit("collaboration.execution", summary)
return {"ok": True, **result}

from .control_plane.goals.acceptance import GoalAcceptanceRejected

def tool(name, arguments):
try:
# Dispatch admission and pause share the same fence. Already
# launched children remain independently governed.
with self._lock(session_id):
return dispatch(name, arguments)
except GoalAcceptanceRejected as exc:
return {
"ok": False,
"error": exc.code,
**exc.payload,
"next_action": "Read current outcomes and correct the rejected document without dropping retained requirements or provenance. "
"Use a new operation id for changed intent: the conversation keeps the original request immutable. "
"For an uncertain write, recover the original unchanged request first.",
}
except (ValueError, KeyError, OSError):
return {
"ok": False,
Expand Down
31 changes: 22 additions & 9 deletions loopx/control_plane/goals/outcome_requirements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ interface OutcomeState extends JsonObject {
schema_version: typeof GOAL_OUTCOMES_SCHEMA; goal_id: string;
goal_ref: WireExactGoalRef | null; revision: number; outcomes: Outcome[];
}
/** Only fixed rule text and validator-owned labels belong in public detail.
/** Only fixed rule text, validator-owned labels and measured byte counts belong in public detail.
* Caller values and arbitrary provider/IO errors must never use this marker. */
export class GoalOutcomeValidationError extends AuthorityStoreProtocolError {}

Expand Down Expand Up @@ -113,6 +113,13 @@ export function bindGoalOutcomesStateOwner(head: JsonObject, goalId: string, goa
return previous === null ? null : {...previous, goal_ref: exact};
}

/** Same stored snapshot, two independently bounded envelopes. */
function outcomeBytes(state: OutcomeState): {declaration: number; review: number} {
const declaration = {...state, outcomes: state.outcomes.map(({review: _review, ...outcome}) => outcome)};
const declarationBytes = Buffer.byteLength(JSON.stringify(declaration), "utf8");
return {declaration: declarationBytes, review: Buffer.byteLength(JSON.stringify(state), "utf8") - declarationBytes};
}

export function refineGoalOutcome(head: JsonObject, goalId: string, goalRef: WireExactGoalRef | null,
actor: string | null, value: unknown): OutcomeState {
let previous = readState(head, goalId);
Expand Down Expand Up @@ -173,14 +180,20 @@ export function refineGoalOutcome(head: JsonObject, goalId: string, goalRef: Wir
}
const state: OutcomeState = {schema_version: GOAL_OUTCOMES_SCHEMA, goal_id: goalId, goal_ref: goalRef, revision: (previous?.revision ?? 0) + 1,
outcomes: [...(previous?.outcomes.filter(o => o.id !== outcomeId) ?? []), outcome]};
// A retained declaration must leave room for its next evidence review.
// Keep both envelopes bounded without changing the single stored snapshot,
// provenance, receipt history or the projection consumed by existing callers.
const declaration = {...state, outcomes: state.outcomes.map(({review: _review, ...outcome}) => outcome)};
const declarationBytes = Buffer.byteLength(JSON.stringify(declaration), "utf8");
requireValue(declarationBytes <= 131072, "outcome declaration capacity exceeded; retain source detail in its owning material");
requireValue(Buffer.byteLength(JSON.stringify(state), "utf8") - declarationBytes <= 131072,
"outcome review capacity exceeded; retain detailed evidence in its owning material");
// Preserve declaration/review limits; diagnostics count the failing
// envelope, not the combined snapshot or an individual outcome/source list.
const candidate = outcomeBytes(state);
if (candidate.declaration > 131072 || candidate.review > 131072) {
const retained = previous === null ? {declaration: 0, review: 0} : outcomeBytes(previous);
const budget = candidate.declaration > 131072 ? "declaration" : "review";
const recovery = budget === "review"
? "Compact only proposed review notes/evidence references without losing conclusions or provenance; keep full evidence in its owning material. "
: "Preserve retained requirements/origins; keep supporting detail in its owning material. Shortening reviews cannot reduce this declaration budget. ";
throw new GoalOutcomeValidationError(
`outcome ${budget} capacity exceeded: candidate ${candidate[budget]} UTF-8 bytes, retained ${retained[budget]}, limit 131072; ` +
"this budget covers all outcomes in the Goal, not the source count; declaration and review budgets are independent. " + recovery +
"Do not remove requirements/origins or create a replacement outcome to bypass the limit. If no lossless update fits, report the capacity blocker.");
}
return readState({...head, goal_outcomes: state}, goalId)!;
}

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 @@ -6,6 +6,7 @@ teaches a reusable control-plane lesson.
| Pattern | Symptoms | Evidence To Read | Likely Root | Durable Repair |
| --- | --- | --- | --- | --- |
| `outcome_review_source_capacity` | Repeated milestone reviews fail with `goal_acceptance_invalid_request` and `outcome sources exceeds its supported capacity` or `outcome source capacity reached`. | The exact failed operation/receipt, inspected original outcome sources and requirements, and its review evidence; verify whether refinement committed before a later writeback failure. | Requirement provenance is used as an append-only progress log; older native/CLI contracts required a source even for a review. | Keep the original outcome and all origins. Existing-outcome reviews/work links omit source (native: source_summary); put observations in review evidence/notes and owning materials. Creation/new requirements still need real provenance. On an older build, use an inspected existing source with exactly the same ref/summary for unchanged requirements only. Never prune origins, inflate limits, invent a new outcome to escape capacity, or silently relabel a new request. Preserve failed receipts and exact-operation recovery; qualify repeated reviews at the source limit and ownership/CAS/replay negative cases through real backends. |
| `outcome_state_byte_capacity` | Outcome refinement fails with declaration or review capacity exceeded, even with one source or a review-only patch. | Preserve the exact response and operation identity; inspect the failing budget and its candidate/retained/limit UTF-8 bytes, then the affected current outcome. A source-count failure is a different constraint. | Declarations and current reviews have independent aggregate budgets across all supporting outcomes. An expanded review can exceed its budget without adding any source. Packet bytes and model tokens do not measure either budget. | For review overflow, compact only proposed review notes/evidence references while retaining every conclusion and its provenance; keep full evidence in its owning material. Shortening reviews cannot repair declaration overflow. Do not change original requirements/origins, delete history, inflate limits or create a replacement outcome to evade either budget. If no lossless update fits, report the capacity blocker. Validation rejection writes no outcome state. After readback, CLI correction can reuse the rejected operation; native conversation envelopes require a new operation id for changed intent. For uncertain writes, inspect the original receipt first and retain unchanged retry intent. Qualify both budget boundaries, rejection/no mutation, correction and replay through real CLI/backends. |
| `premerge_binary_visual_coverage` | A first-screen PNG makes premerge fail with invalid UTF-8, although its text checks are clean. | Exact changed paths, text scan coverage, raster evidence and the existing exact-head review boundary. | The premerge adapter feeds binary images into a text-only scan and loses the distinction between text readability and visual privacy. | Keep the text scanner unchanged; explicitly list changed images as outside text coverage and retain the existing manual-review hold, including image-only changes. Unknown binary/text decode failures remain failures. Inspect pixels, metadata and provenance in the ordinary exact-head review; never silently skip images, certify them with a text pass, add a bypass flag or demand another user presentation approval. |
| `planning_visibility_requires_declared_complexity` | A flat runnable backlog hides the Goal or broader work view because no dependency, blocked work or acceptance gap has been registered. | Same-source default/opt-out planning packets, original Goal, bounded work summary, omission counts and full-read routes. | A supplemental-context condition is mistaken for evidence that no strategic planning is needed; byte reduction removes useful visibility. | Show Goal and bounded work context by default at applicable preselection boundaries, including flat backlogs. Preserve omission counts, complete-read routes and action authority. Reduce exact duplication instead of hiding work; qualify input-cost changes with matched fixtures and keep model benefit unproven until measured. |
| `stale_objective_repeated_as_current` | Current planning repeatedly projects an obsolete objective despite newer direct user direction. | Recorded source versus owner-applied objective revision, current canonical basis and each peer's adoption. | Fresh file bytes are mistaken for current intent; a registry/display edit cannot establish peer adoption. | Prepare an evidence-linked operating-objective revision through the existing Goal update surface. Preserve original commitments and use owner apply. Reconcile each peer's work and record its own exact adoption before new execution; do not revise for ordinary progress/diagnostics/age, impersonate peers, or interrupt in-flight settlement. Reuse a pending proposal; adoption is once per objective revision, not per Turn or peer/work write. Verify native tool invocation, current projection and stale-execution refusal, not only serializer output. |
Expand Down
56 changes: 56 additions & 0 deletions tests/control_plane/test_goal_outcome_diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,59 @@ def test_unqualified_effect_reason_is_not_promoted_to_public_detail(monkeypatch)
assert str(failure.value) == "goal_acceptance_effect_failed"
assert private_reason not in str(failure.value)
assert not getattr(failure.value, "payload", {}).get("error_detail")


@pytest.mark.parametrize("entrypoint", ["refine", "refresh"])
def test_review_capacity_diagnosis_and_lossless_review_recovery(acceptance_goal, entrypoint):
import re

project, _, cli, run = acceptance_goal
declaration = refinement("message:original")
declaration["requirements"] = [
{"id": f"r{i}", "description": "保留证据😀" * 60} for i in range(32)
]
_, args = arguments(project, cli, "refine", declaration, "declare")
code, declared = run(*args)
assert code == 0, declared
before = cli("inspect")[1]
markdown = (project / "state.md").read_bytes()
review = [{"id": f"r{i}", "conclusion": "uncertain", "note": "n" * 512,
"evidence_refs": ["e" * 500] * 8} for i in range(32)]
path, args = arguments(project, cli, entrypoint, {"outcome_id": "export", "review": review}, "review")
code, rejected = run(*args)
assert code == 1 and rejected["error"] == "goal_acceptance_invalid_request", rejected
detail = rejected["error_detail"]
counts = re.search(r"candidate (\d+) UTF-8 bytes, retained (\d+), limit (\d+)", detail)
assert counts, rejected
candidate, retained, limit = map(int, counts.groups())
assert retained == 0 and limit < candidate and limit == 131072
assert "outcome review capacity exceeded" in detail
assert "declaration and review budgets are independent" in detail
assert "all outcomes in the Goal, not the source count" in detail
assert "report the capacity blocker" in detail
assert declaration["source"]["summary"] not in json.dumps(rejected)
assert "保留证据" not in json.dumps(rejected, ensure_ascii=False)
assert cli("inspect")[1] == before
assert (project / "state.md").read_bytes() == markdown
if entrypoint == "refresh":
assert rejected["appended"] is False

# Only new review wording/references change. Every original requirement and
# source survives; missing evidence remains uncertain, not a fabricated pass.
compact = [{**item, "note": "Further validation needed", "evidence_refs": ["artifact:full-report"]} for item in review]
document = json.loads(path.read_text())
(document if entrypoint == "refine" else document["refinement"])["review"] = compact
path.write_text(json.dumps(document))
code, recovered = run(*args)
assert code == 0, recovered
result = recovered if entrypoint == "refine" else recovered["goal_outcome_update"]
assert result["status"] == "applied"
current = cli("inspect")[1]
outcome = current["goal_acceptance_contract"]["outcome_requirements"]["outcomes"][0]
assert outcome["sources"] == [declaration["source"]]
assert [{"id": r["id"], "description": r["description"]} for r in outcome["requirements"]] == declaration["requirements"]
assert all(r["agent_review"]["conclusion"] == "uncertain" for r in outcome["requirements"])
code, replay = run(*args)
assert code == 0, replay
assert (replay if entrypoint == "refine" else replay["goal_outcome_update"])["status"] == "replayed"
assert cli("inspect")[1] == current
Loading
Loading