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
30 changes: 30 additions & 0 deletions docs/architecture/rfcs/goal-outcome-refinement-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,36 @@ envelope. Explicit sources remain additive and immutable; overflow fails with
guidance rather than dropping provenance or raising the limit. A new declaration
cannot use the review path to evade provenance, ownership or check authority.

Declarations and current evidence reviews have independent 128 KiB UTF-8 JSON
budgets in the same canonical snapshot. The declaration budget includes Goal
identity, original objectives, requirements, work links and every source; it
excludes only the optional `review` fields. The review budget counts their exact
additional serialized bytes. This explicitly raises the combined outcome-state
ceiling from 128 to 256 KiB; it is a resource tradeoff, not a historical budget
pass. Per-field/count limits, operation history and provider transport limits
remain unchanged. Neither envelope can borrow from the other.

The previous shared cap admitted a 131,000-byte declaration but prevented its
next complete evidence review. Keeping that cap would require rejecting valid
progress or moving retained records into a new reference/hydration protocol.
The bounded extra envelope keeps existing synchronous consumers, one atomic
snapshot, receipts and recovery intact. No compression, external authority,
provenance pruning or migration is introduced. Oversized reviews fail before
commit, retain the prior snapshot and direct detailed evidence to its owning
material. Existing stored states remain readable by older versions, but an
older writer may reject further refinement above its combined cap; rollback
must not delete declarations or historical receipts to make them fit.

The synthetic 20-outcome/100-requirement case grows from 131,000 to 131,753
stored bytes after review; its full outcome projection is 154,503 bytes. On the
same already-admissible 19,454-byte input, base and candidate produce identical
state/projection. An alternating 100-sample owner-refinement plus projection
measurement gives base/candidate p50 1.025/1.034 ms and p95 1.387/1.417 ms. This
is a local pure-owner cost observation, not a CLI/provider SLO or long-horizon
benefit. Real File/SQLite CLI and native conversation tests cover the crowded
case; File/SQLite/PostgreSQL authority tests cover restart, replay, stale bases
and atomic overflow rejection. No installed-state expansion is claimed here.

The existing admitted-work context carries the complete outcome declaration;
when inline content is present, no duplicate acceptance inspection is required.
Changes can accompany ordinary `refresh-state --goal-outcome-update-json`, or one
Expand Down
21 changes: 21 additions & 0 deletions docs/architecture/rfcs/goal-outcome-refinement-v0.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,27 @@ active 含义;显式非活动状态仍拒绝细化。
重试信封。显式来源保持追加和不可改写;超限返回修复指导,不丢来源、不提高
上限。新增声明不能借自查路径绕过来源、归属或验收权限。

声明和当前证据自查在同一 canonical 快照内各有 128 KiB UTF-8 JSON 预算。
声明预算包含 Goal 身份、原始目标、要求、工作关联及全部来源,只排除可选的
`review` 字段;自查预算按这些字段增加的实际序列化字节计算。这明确将成果状态
合计上限从 128 提高到 256 KiB,属于资源取舍,不能把历史超限改称通过。
各字段/数量上限、操作历史及 provider 传输限制不变,两项预算不能相互借用。

原共享上限允许保存 131,000 字节的声明,却阻止其下一次完整证据自查。
保留该上限意味着拒绝有效进展,或把保留记录移入新的引用/加载协议。
增加有界自查预算保留既有同步消费者、单一原子快照、回执和恢复;不引入压缩、
外部权威、来源裁剪或迁移。过大自查在提交前失败,保留原快照,并提示将详细
证据留在所属材料。旧版本仍能读取这些状态,但旧写入器可能因合计超限拒绝
后续细化;回滚不能删除声明或历史回执来迁就旧上限。

合成的 20 项成果/100 项要求场景在自查后由 131,000 增至 131,753 存储字节,
完整成果投影为 154,503 字节。对原本可准入的同一份 19,454 字节输入,base 与
候选的状态及投影相同。交替采集 100 次 owner 细化加投影,base/候选 p50 为
1.025/1.034 ms,p95 为 1.387/1.417 ms。这只是本机纯 owner 成本观察,
不代表 CLI/provider SLO 或长程收益。真实 File/SQLite CLI 和原生会话测试
覆盖容量临界场景;File/SQLite/PostgreSQL authority 测试覆盖重启、重放、
旧基线及超限原子拒绝。此处不声称已扩展安装态容量。

既有准入工作上下文携带完整成果声明;内联内容完整时,无需额外查询验收。
变化可合并到普通 `refresh-state --goal-outcome-update-json`,或一次原生
`refine_outcome` 调用。无变化不额外调用、不增加往返,也不重复登记自查。
Expand Down
9 changes: 8 additions & 1 deletion loopx/control_plane/goals/outcome_requirements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,14 @@ 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]};
requireValue(Buffer.byteLength(JSON.stringify(state), "utf8") <= 131072, "outcome declaration capacity exceeded; retain source detail in its owning material");
// 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");
return readState({...head, goal_outcomes: state}, goalId)!;
}

Expand Down
95 changes: 95 additions & 0 deletions tests/control_plane/test_goal_outcome_capacity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"""Full declarations remain reviewable through real CLI and conversation paths."""
import json
from pathlib import Path
import subprocess

from test_goal_acceptance_cli import acceptance_goal as acceptance_goal
from test_goal_acceptance_prepared_action import conversation_check as conversation_check


def seed_crowded(project, actor):
"""Synthetic disposable authority only; exercise production readers/writers."""
state = {"schema_version": "loopx_goal_outcomes_v0", "goal_id": "goal-acceptance",
"goal_ref": None, "revision": 1, "outcomes": [
{"id": f"result-{i}", "owner_agent_id": actor, "objective": f"Deliver result {i}",
"revision": 1, "todo_ids": [],
"requirements": [{"id": f"r{r}", "description": f"Preserve requirement {r}"} for r in range(5)],
"sources": [{"ref": f"origin-{i}", "summary": "Owner requested this result"}]} for i in range(20)]}
def size():
return len(json.dumps(state, ensure_ascii=False, separators=(",", ":")).encode())
i = 0
while size() < 130400:
state["outcomes"][i % 20]["sources"].append({"ref": f"source-{i}", "summary": "x" * 500})
i += 1
source = {"ref": "boundary", "summary": "x"}
state["outcomes"][0]["sources"].append(source)
available = 131000 - size()
assert 0 <= available < 512
source["summary"] += "x" * available
assert size() == 131000
repo = Path(__file__).resolve().parents[2]
module = repo / "loopx/control_plane/coordination/local_authority_provider.ts"
script = f"import {{openLocalAuthorityStore}} from {json.dumps(module.as_uri())};" + """
let input = ''; for await (const chunk of process.stdin) input += chunk;
const {root, state} = JSON.parse(input);
const store = await openLocalAuthorityStore(root, state.goal_id);
const prior = await store.loadAuthority();
if (prior.status !== 'loaded') throw new Error('fixture authority missing');
const result = await store.commitAuthority({operation_id: 'seed-capacity-fixture',
expected_provider_revision: prior.provider_revision, events: [], receipts: [],
next_projection: {...prior.head, goal_outcomes: state}});
if (result.status !== 'applied') throw new Error('fixture commit failed');
"""
subprocess.run(["node", "--no-warnings", "--experimental-sqlite", "--experimental-strip-types",
"--input-type=module", "-e", script], input=json.dumps({"root": str(project.parent / "runtime"), "state": state}),
text=True, capture_output=True, check=True, timeout=45)
return {row["id"]: row["sources"] for row in state["outcomes"]}


def review_document():
return {"outcome_id": "result-19", "review": [{"id": f"r{i}", "conclusion": "uncertain",
"evidence_refs": ["artifact:inspection"], "note": "Independent verification remains pending."} for i in range(5)]}


def assert_reviewed(result, origins):
rows = result["goal_acceptance_contract"]["outcome_requirements"]["outcomes"]
assert {row["id"]: row["sources"] for row in rows} == origins
reviewed = next(row for row in rows if row["id"] == "result-19")
assert all(r["agent_review"]["conclusion"] == "uncertain" and not r["planning_satisfied"] for r in reviewed["requirements"])
assert all(r["status"] == "unverified" for r in reviewed["requirements"])


def test_crowded_cli_review_preserves_snapshot_and_retry(acceptance_goal):
project, document, cli, _ = acceptance_goal
origins = seed_crowded(project, "agent-a")
before = cli("inspect")[1]
document.write_text(json.dumps(review_document()))
args = ("refine", "--agent-id", "agent-a", "--document", str(document), "--operation-id", "review-capacity",
"--expected-provider-revision", before["provider_revision"], "--execute")
code, result = cli(*args)
assert code == 0, result
assert_reviewed(result, origins)
current = cli("inspect")[1]
assert_reviewed(current, origins)
assert cli(*args)[1]["status"] == "replayed"
assert cli("inspect")[1] == current


def test_crowded_native_review_uses_same_owner_and_preserves_stop(conversation_check):
project, cli, controller, sid, _, _, adapter = conversation_check
from loopx.chat_loopx_mode import TOOL
origins = seed_crowded(project, "coordinator")
handler = adapter.session.read_tool_handler
before = handler(TOOL["name"], {"action": "outcomes"})
request = {"action": "refine_outcome", "operation_id": "review-capacity-native",
"expected_provider_revision": before["provider_revision"], "document": review_document()}
result = handler(TOOL["name"], request)
assert result["ok"], result
assert_reviewed(result, origins)
assert handler(TOOL["name"], request)["status"] == "replayed"
current = cli("inspect")[1]
assert_reviewed(current, origins)
mode = controller.store.load_session(sid)["loopx_mode"]
controller.store.update_session(sid, loopx_mode={**mode, "paused": True})
assert handler(TOOL["name"], request)["error"] == "conversation_execution_inactive"
assert cli("inspect")[1] == current
117 changes: 117 additions & 0 deletions tests/control_plane_ts/goal_outcome_capacity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import assert from "node:assert/strict";
import test from "node:test";
import {mkdtemp, rm} from "node:fs/promises";
import {tmpdir} from "node:os";
import {join} from "node:path";
import {Pool} from "pg";
import {randomUUID} from "node:crypto";
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 {inspectGoalAcceptance, refineGoalAcceptanceOutcome} from "../../loopx/control_plane/goals/acceptance_authority.ts";
import {authorityProjectionFixture} from "./authority_projection_fixture.ts";

const goal = "outcome-capacity";
const bytes = (value: unknown) => Buffer.byteLength(JSON.stringify(value), "utf8");
function crowded() {
const state = {schema_version: "loopx_goal_outcomes_v0", goal_id: goal, goal_ref: null, revision: 1,
outcomes: Array.from({length: 20}, (_, i) => ({id: `outcome-${i}`, owner_agent_id: "author", objective: `Deliver result ${i}`,
revision: 1, requirements: Array.from({length: 5}, (_, r) => ({id: `r${r}`, description: `Retain requirement ${r}`})),
todo_ids: [], sources: [{ref: `origin-${i}`, summary: `Owner requested result ${i}`}]}))};
for (let i = 0; bytes(state) < 130400; i++) {
state.outcomes[i % 20].sources.push({ref: `source-${i}`, summary: `Retained origin ${i}: ${"x".repeat(480)}`});
}
// Leave less room than even one complete review, while keeping the legacy
// declaration valid and every source's independently bounded original text.
const source = {ref: "boundary", summary: "x"};
state.outcomes[0].sources.push(source);
const available = 131000 - bytes(state);
assert.ok(available >= 0 && available < 512);
source.summary += "x".repeat(available);
assert.equal(bytes(state), 131000);
return authorityProjectionFixture(goal, [], [], "native", {goal_outcomes: state});
}
const review = {outcome_id: "outcome-19", review: Array.from({length: 5}, (_, i) => ({id: `r${i}`,
conclusion: "uncertain", evidence_refs: ["artifact:current-check"], note: "Independent acceptance is still pending."}))};

test("a full retained declaration can receive a complete review without losing origins", () => {
const head = crowded(), original = structuredClone(head);
const state = refineGoalOutcome(head, goal, null, "author", review);
assert.ok(bytes(state) > 131072, "reproduces the previous combined-cap rejection");
assert.ok(bytes(state) <= 262144, "declarations and reviews remain bounded together");
const before = (original.goal_outcomes as JsonObject).outcomes as JsonObject[];
for (const item of state.outcomes) {
const prior = before.find(row => row.id === item.id)!;
for (const field of ["objective", "owner_agent_id", "requirements", "todo_ids", "sources"]) assert.deepEqual(item[field], prior[field]);
}
assert.deepEqual(state.outcomes.find(o => o.id === review.outcome_id)!.review!.items, review.review);
assert.deepEqual(head, original, "pure refinement never mutates its input");
assert.throws(() => refineGoalOutcome({...head, goal_outcomes: state}, goal, null, "author", {
outcome_id: "outcome-0", source: {ref: "overflow", summary: "x".repeat(512)},
}), /declaration capacity exceeded/, "review allowance cannot expand declaration capacity");
});

test("review capacity measures UTF-8 bytes across all retained reviews", () => {
let head = authorityProjectionFixture(goal, [], [], "native");
const fullReview = Array.from({length: 32}, (_, i) => ({id: `r${i}`, conclusion: "uncertain", evidence_refs: [], note: "界".repeat(512)}));
for (let i = 0; i < 3; i++) {
head = {...head, goal_outcomes: refineGoalOutcome(head, goal, null, "author", {
outcome_id: `outcome-${i}`, owner_agent_id: "author", objective: `Deliver result ${i}`,
requirements: fullReview.map(({id}) => ({id, description: `Keep ${id}`})), source: {ref: `origin-${i}`, summary: "Original request"},
})};
}
for (let i = 0; i < 2; i++) head = {...head, goal_outcomes: refineGoalOutcome(head, goal, null, "author", {outcome_id: `outcome-${i}`, review: fullReview})};
const before = structuredClone(head);
assert.throws(() => refineGoalOutcome(head, goal, null, "author", {outcome_id: "outcome-2", review: fullReview}), /review capacity exceeded/);
assert.deepEqual(head, before);
// Replacing current evidence retains the original declarations and journal
// semantics; the current-review budget is not an append-only history budget.
const small = fullReview.map(row => ({...row, note: "Uncertain"}));
head = {...head, goal_outcomes: refineGoalOutcome(head, goal, null, "author", {outcome_id: "outcome-0", review: small})};
assert.equal(refineGoalOutcome(head, goal, null, "author", {outcome_id: "outcome-2", review: fullReview}).outcomes.length, 3);
});

for (const provider of ["file", "sqlite", "postgresql"] as const) test(`${provider}: crowded review survives restart and replay; overflow is atomic`,
{skip: provider === "postgresql" && !process.env.LOOPX_TEST_POSTGRES_URL}, async t => {
const root = await mkdtemp(join(tmpdir(), "outcome-capacity-")); t.after(() => rm(root, {recursive: true, force: true}));
const pool = provider === "postgresql" ? new Pool({connectionString: process.env.LOOPX_TEST_POSTGRES_URL, max: 2}) : null;
if (pool) t.after(() => pool.end());
const database = pool ? {connect: async () => {
const client = await pool.connect();
return {query: async (sql: string, values?: readonly unknown[]) => client.query(sql, values ? [...values] : undefined),
release: () => client.release()};
}} : null;
if (database) await installPostgreSqlAuthorityStoreSchema(database, `postgresql:${"b".repeat(32)}`);
const scope = {tenant_id: `outcome-capacity-${randomUUID()}`, goal_id: goal};
const open = () => database ? new PostgreSqlAuthorityStore(database, scope)
: provider === "file" ? new FileAuthorityStore(root, goal) : new SqliteAuthorityStore(root, goal);
const store = open(), head = crowded();
await store.commitAuthority({operation_id: "seed", expected_provider_revision: null, next_projection: head, events: [], receipts: []});
const prior = await inspectGoalAcceptance(store, goal);
const request = {goal_id: goal, actor_agent_id: "author", operation_id: "review", expected_provider_revision: prior.provider_revision, refinement: review};
const preview = await refineGoalAcceptanceOutcome(store, {...request, dry_run: true});
assert.equal(preview.status, "planned");
assert.deepEqual(await inspectGoalAcceptance(store, goal), prior);
assert.equal((await refineGoalAcceptanceOutcome(store, request)).status, "applied");
const after = await inspectGoalAcceptance(open(), goal);
const outcomes = (after.goal_acceptance_contract as JsonObject).outcome_requirements as JsonObject;
const rows = outcomes.outcomes as JsonObject[];
assert.equal(rows.length, 20);
const current = rows.find(row => row.id === review.outcome_id)!;
assert.ok((current.requirements as JsonObject[]).every(r => r.status === "unverified" && r.planning_satisfied === false));
assert.equal((await refineGoalAcceptanceOutcome(open(), request)).status, "replayed");
assert.deepEqual(await inspectGoalAcceptance(open(), goal), after);
assert.equal((await refineGoalAcceptanceOutcome(store, {...request, operation_id: "stale"})).status, "conflict");
await assert.rejects(() => refineGoalAcceptanceOutcome(store, {...request, operation_id: "overflow", expected_provider_revision: after.provider_revision,
refinement: {outcome_id: "outcome-0", source: {ref: "overflow", summary: "x".repeat(512)}}}), /declaration capacity exceeded/);
assert.deepEqual(await inspectGoalAcceptance(open(), goal), after);
const loaded = await open().loadAuthority(); assert.equal(loaded.status, "loaded");
if (loaded.status === "loaded") {
const original = (head.goal_outcomes as JsonObject).outcomes as JsonObject[];
for (const row of (loaded.head.goal_outcomes as JsonObject).outcomes as JsonObject[]) {
assert.deepEqual(row.sources, original.find(o => o.id === row.id)!.sources);
}
}
});
Loading