diff --git a/docs/reference/goal-acceptance-observations.md b/docs/reference/goal-acceptance-observations.md index 41b9467201..4c0fd9e9a3 100644 --- a/docs/reference/goal-acceptance-observations.md +++ b/docs/reference/goal-acceptance-observations.md @@ -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 错误保持原有脱敏拒绝。 + ## Executable checks within default acceptance diff --git a/loopx/chat_loopx_mode.py b/loopx/chat_loopx_mode.py index cf69b4c93c..66e531ff25 100644 --- a/loopx/chat_loopx_mode.py +++ b/loopx/chat_loopx_mode.py @@ -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, diff --git a/loopx/control_plane/goals/outcome_requirements.ts b/loopx/control_plane/goals/outcome_requirements.ts index 0045165335..47db417fd6 100644 --- a/loopx/control_plane/goals/outcome_requirements.ts +++ b/loopx/control_plane/goals/outcome_requirements.ts @@ -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 {} @@ -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); @@ -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)!; } diff --git a/skills/loopx-self-repair/references/repair-patterns.md b/skills/loopx-self-repair/references/repair-patterns.md index 810ef10a0f..4364e043a7 100644 --- a/skills/loopx-self-repair/references/repair-patterns.md +++ b/skills/loopx-self-repair/references/repair-patterns.md @@ -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. | diff --git a/tests/control_plane/test_goal_outcome_diagnostics.py b/tests/control_plane/test_goal_outcome_diagnostics.py index 0d087283c1..a4d1833760 100644 --- a/tests/control_plane/test_goal_outcome_diagnostics.py +++ b/tests/control_plane/test_goal_outcome_diagnostics.py @@ -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 diff --git a/tests/control_plane/test_goal_outcome_native_retry.py b/tests/control_plane/test_goal_outcome_native_retry.py index 8437313bc4..065cc7742a 100644 --- a/tests/control_plane/test_goal_outcome_native_retry.py +++ b/tests/control_plane/test_goal_outcome_native_retry.py @@ -155,3 +155,40 @@ def lose_reply(**arguments): "expected_provider_revision": before["provider_revision"], "document": document}) assert rejected["ok"] is False assert cli("inspect")[1] == before + + +def test_native_capacity_detail_guides_new_intent_without_rewriting_envelope(conversation_check): + _, cli, _, _, _, _, adapter = conversation_check + handler = adapter.session.read_tool_handler + request = outcome_request(adapter) + request["document"]["requirements"] = [ + {"id": f"r{i}", "description": "保留证据😀" * 60} for i in range(32) + ] + created = handler(TOOL["name"], request) + assert created["ok"], created + original = cli("inspect")[1] + review = {"action": "refine_outcome", "operation_id": "oversized-review", + "expected_provider_revision": original["provider_revision"], + "document": {"outcome_id": "tour", "review": [ + {"id": f"r{i}", "conclusion": "uncertain", "note": "n" * 512, + "evidence_refs": ["e" * 500] * 8} for i in range(32)]}} + rejected = handler(TOOL["name"], review) + assert rejected["ok"] is False and rejected["error"] == "goal_acceptance_invalid_request", rejected + assert "all outcomes in the Goal, not the source count" in rejected["error_detail"] + assert "outcome review capacity exceeded" in rejected["error_detail"] + assert "new operation id for changed intent" in rejected["next_action"] + assert "保留证据" not in str(rejected) + assert handler(TOOL["name"], review) == rejected + assert cli("inspect")[1] == original + compact = {**review, "document": {"outcome_id": "tour", "review": [ + {**item, "note": "Further validation needed", "evidence_refs": ["artifact:full-report"]} + for item in review["document"]["review"]]}} + assert handler(TOOL["name"], compact)["error"] == "collaboration_request_rejected" + assert cli("inspect")[1] == original, "reserved native intent cannot be silently replaced" + compact["operation_id"] = "compact-review" + applied = handler(TOOL["name"], compact) + assert applied["ok"] and applied["status"] == "applied", applied + assert handler(TOOL["name"], compact)["status"] == "replayed" + outcome = cli("inspect")[1]["goal_acceptance_contract"]["outcome_requirements"]["outcomes"][0] + assert outcome["sources"] == original["goal_acceptance_contract"]["outcome_requirements"]["outcomes"][0]["sources"] + assert [{"id": r["id"], "description": r["description"]} for r in outcome["requirements"]] == request["document"]["requirements"] diff --git a/tests/control_plane_ts/goal_outcome_capacity.test.ts b/tests/control_plane_ts/goal_outcome_capacity.test.ts index 7091809b0d..3e86e77d2f 100644 --- a/tests/control_plane_ts/goal_outcome_capacity.test.ts +++ b/tests/control_plane_ts/goal_outcome_capacity.test.ts @@ -9,7 +9,7 @@ import type {JsonObject} from "../../loopx/control_plane/effect_program.ts"; import {FileAuthorityStore} from "../../loopx/control_plane/coordination/file_authority_store.ts"; import {SqliteAuthorityStore} from "../../loopx/control_plane/coordination/sqlite_authority_store.ts"; import {PostgreSqlAuthorityStore, installPostgreSqlAuthorityStoreSchema} from "../../loopx/control_plane/coordination/postgresql_authority_store.ts"; -import {refineGoalOutcome} from "../../loopx/control_plane/goals/outcome_requirements.ts"; +import {refineGoalOutcome, GoalOutcomeValidationError} from "../../loopx/control_plane/goals/outcome_requirements.ts"; import {inspectGoalAcceptance, refineGoalAcceptanceOutcome} from "../../loopx/control_plane/goals/acceptance_authority.ts"; import {authorityProjectionFixture} from "./authority_projection_fixture.ts"; @@ -115,3 +115,126 @@ for (const provider of ["file", "sqlite", "postgresql"] as const) test(`${provid } } }); + +const original = authorityProjectionFixture(goal, [], [], "native"); +function declared() { + return refineGoalOutcome(original, goal, null, "author", { + outcome_id: "report", owner_agent_id: "author", objective: "Retain the complete result", + requirements: Array.from({length: 32}, (_, i) => ({id: `r${i}`, description: "保留证据😀".repeat(60)})), + source: {ref: "message:original", summary: "Synthetic original request"}, + }); +} + +test("the independent review budget accepts its exact UTF-8 boundary and diagnoses growth", () => { + const state = declared(), head = {...original, goal_outcomes: state}; + const before = structuredClone(head); + const review = state.outcomes[0].requirements.map(r => ({id: r.id, conclusion: "uncertain", + evidence_refs: Array.from({length: 8}, () => "e"), note: "保留证据😀"})); + // Construct the candidate independently: the digest has fixed width, not an + // asserted value copied from production. Fill legal reference fields to the + // frozen byte boundary; character count is deliberately not the oracle. + const candidate = {...state, revision: 2, outcomes: [{...state.outcomes[0], revision: 2, + review: {basis_digest: "0".repeat(64), items: review}}]}; + const declaration = {...candidate, outcomes: candidate.outcomes.map(({review: _review, ...row}) => row)}; + let remaining = 131072 - (bytes(candidate) - bytes(declaration)); + assert.ok(remaining > 0); + for (const item of review) for (let i = 0; i < item.evidence_refs.length && remaining > 0; i++) { + const added = Math.min(1023, remaining); + item.evidence_refs[i] += "x".repeat(added); remaining -= added; + } + assert.equal(remaining, 0); + const exact = refineGoalOutcome(head, goal, null, "author", {outcome_id: "report", review}); + assert.equal(bytes(exact) - bytes(declaration), 131072); + assert.ok(JSON.stringify(exact).length - JSON.stringify(declaration).length < 131072); + review[0].note += "😀"; + assert.throws(() => refineGoalOutcome(head, goal, null, "author", {outcome_id: "report", review}), error => { + assert.ok(error instanceof GoalOutcomeValidationError); + assert.match(error.message, /candidate 131076 UTF-8 bytes/); + assert.match(error.message, /outcome review capacity exceeded/); + assert.match(error.message, /retained 0, limit 131072/); + assert.match(error.message, /all outcomes in the Goal, not the source count/); + assert.ok(!error.message.includes("Synthetic original request")); + return true; + }); + assert.deepEqual(head, before); + assert.equal(refineGoalOutcome(head, goal, null, "author", {outcome_id: "report", review: review.map(item => ({...item, + evidence_refs: ["artifact:full-report"], note: "Further validation needed"}))}).outcomes[0].sources.length, 1); +}); + +test("the aggregate limit counts other outcomes while the source-count limit remains distinct", () => { + let state = declared(); + for (let i = 0; i < 63; i++) state = refineGoalOutcome({...original, goal_outcomes: state}, goal, null, "author", { + outcome_id: "report", source: {ref: `message:origin-${i}`, summary: "Retained original provenance"}}); + const head = {...original, goal_outcomes: state}; + assert.throws(() => refineGoalOutcome(head, goal, null, "author", {outcome_id: "report", + source: {ref: "message:overflow", summary: "Additional request"}}), /outcome source capacity reached/); + // Individually fitting reviews still share the Goal's review allowance. + const large = {...state.outcomes[0], id: "another", sources: [{ref: "message:another", summary: "Another request"}]}; + const combined = {...original, goal_outcomes: {...state, outcomes: [...state.outcomes, large]}}; + const review = large.requirements.map(r => ({id: r.id, conclusion: "uncertain", evidence_refs: ["e".repeat(1024), "f".repeat(1024)], note: "n".repeat(512)})); + assert.ok(refineGoalOutcome({...original, goal_outcomes: {...state, outcomes: [large]}}, goal, null, "author", {outcome_id: "another", review})); + const first = refineGoalOutcome(combined, goal, null, "author", {outcome_id: "report", review}); + assert.throws(() => refineGoalOutcome({...original, goal_outcomes: first}, goal, null, "author", {outcome_id: "another", review}), error => { + assert.ok(error instanceof GoalOutcomeValidationError); + assert.match(error.message, /outcome review capacity exceeded/); + assert.match(error.message, /all outcomes in the Goal/); + const retained = bytes(first) - bytes({...first, outcomes: first.outcomes.map(({review: _review, ...row}) => row)}); + assert.ok(retained > 0 && error.message.includes(`retained ${retained}, limit 131072`)); + return true; + }); +}); + +// The shared owner must reject before any real PostgreSQL transaction commits. +// Run with the existing isolated integration database, never an active Goal. +test("postgresql: rejected capacity update retains state and recovers once", {skip: !process.env.LOOPX_TEST_POSTGRES_URL}, async t => { + const pool = new Pool({connectionString: process.env.LOOPX_TEST_POSTGRES_URL, max: 2}); + t.after(() => pool.end()); + const database = {connect: async () => { + const client = await pool.connect(); + return {query: async (sql: string, values?: readonly unknown[]) => client.query(sql, values ? [...values] : undefined), + release: () => client.release()}; + }}; + await installPostgreSqlAuthorityStoreSchema(database, `postgresql:${"b".repeat(32)}`); + const store = new PostgreSqlAuthorityStore(database, {tenant_id: `capacity-${randomUUID()}`, goal_id: goal}); + const state = declared(); + await store.commitAuthority({operation_id: "seed", expected_provider_revision: null, + next_projection: {...original, goal_outcomes: state}, events: [], receipts: []}); + const before = await store.loadAuthority(); + assert.equal(before.status, "loaded"); + if (before.status !== "loaded") throw new Error("synthetic authority unavailable"); + const review = state.outcomes[0].requirements.map(r => ({id: r.id, conclusion: "uncertain", + evidence_refs: Array.from({length: 8}, () => "e".repeat(500)), note: "n".repeat(512)})); + const request = {goal_id: goal, actor_agent_id: "author", operation_id: "review", + expected_provider_revision: before.provider_revision, refinement: {outcome_id: "report", review}}; + await assert.rejects(() => refineGoalAcceptanceOutcome(store, request), /all outcomes in the Goal/); + assert.deepEqual(await store.loadAuthority(), before); + for (const item of review) {item.evidence_refs = ["artifact:full-report"]; item.note = "Further validation needed";} + assert.equal((await refineGoalAcceptanceOutcome(store, request)).status, "applied"); + const after = await store.loadAuthority(); + assert.equal(after.status, "loaded"); + if (after.status !== "loaded") throw new Error("synthetic authority unavailable"); + assert.deepEqual(after.head.goal_outcomes.outcomes[0].requirements, state.outcomes[0].requirements); + assert.deepEqual(after.head.goal_outcomes.outcomes[0].sources, state.outcomes[0].sources); + assert.equal((await refineGoalAcceptanceOutcome(store, request)).status, "replayed"); + assert.deepEqual(await store.loadAuthority(), after); +}); + + +test("declaration boundary diagnostics do not suggest reducing the separate review envelope", () => { + const head = crowded(), before = structuredClone(head); + const source = {ref: "exact-boundary", summary: "x"}; + const probe = refineGoalOutcome(head, goal, null, "author", {outcome_id: "outcome-0", source}); + source.summary += "x".repeat(131072 - bytes(probe)); + const exact = refineGoalOutcome(head, goal, null, "author", {outcome_id: "outcome-0", source}); + assert.equal(bytes(exact), 131072); + assert.throws(() => refineGoalOutcome(head, goal, null, "author", { + outcome_id: "outcome-0", source: {...source, summary: source.summary + "😀"}, + }), error => { + assert.ok(error instanceof GoalOutcomeValidationError); + assert.match(error.message, /outcome declaration capacity exceeded: candidate 131076 UTF-8 bytes, retained 131000, limit 131072/); + assert.match(error.message, /Shortening reviews cannot reduce this declaration budget/); + assert.ok(!error.message.includes("Compact only proposed review")); + return true; + }); + assert.deepEqual(head, before); +});