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
41 changes: 31 additions & 10 deletions docs/reference/protocols/quota-replan-planning-selection-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,23 +89,37 @@ An enabled stride review can become stale between delivery and writeback.
For a context-ID mismatch within the same obligation, the semantic writeback
owner keeps `stride_review_basis_mismatch` and returns
`host_action=read_current_replan_context` with
`replan_transition.context_recovery`: the received ID, complete current
`replan_context`, current `stride_control`, and ordered recovery guidance.
There is no automatic acceptance or reference replacement. Read the returned
Goal acceptance, evidence and followthrough, perform applicable source reads,
and reassess the decision before retrying the original writeback with its same
`replan_transition.context_recovery`. The JSON CLI saves the exact complete
write-time `replan_context`, `stride_control` and recovery guidance in a private,
immutable file. Its response carries received/current IDs, the file and digest,
and generic index/selection commands. It does not activate context delivery or
advance a native baseline. Native semantic-owner responses and non-POSIX CLI
recovery remain complete inline.
There is no automatic acceptance or reference replacement. Read the applicable
Goal, acceptance, constraints, stride contract and decision-relevant facts;
supporting evidence and history are available on demand. Whole-file traversal
is not a prerequisite. Reassess before retrying the original writeback with its same
Goal/Agent/Turn/obligation and settlement identity. The rejection emits no
unbound retry command; it commits no writeback or quota spend. Another relevant
change can reject the retry again. Other validation failures retain their
change can reject the retry again. If private publication fails, the compact
response identifies unavailable transport; repair it and retry the unchanged
original writeback for current recovery, preserving any earlier committed
refinement receipt. It neither substitutes an old basis nor repeats admission.
Other validation failures retain their
existing repair route, and stride-off behavior is unchanged.

启用 stride 的评审可能在投递与写回之间过期。同一 obligation 内发生 context ID
不匹配时,语义写回 owner 保留 `stride_review_basis_mismatch`,给出
`host_action=read_current_replan_context`,并返回 `replan_transition.context_recovery`:收到的 ID、完整的当前 `replan_context`、
当前 `stride_control` 与按序恢复指引。不会自动接受提案或替换引用。消费者先读
返回的 Goal 验收、证据和后续执行事实,完成适用的来源读取并重新判断,再使用
`host_action=read_current_replan_context`。JSON CLI 将完整的写入时 `replan_context`、
`stride_control` 与恢复指引保存在私有不可变文件中;`replan_transition.context_recovery`
返回收到/当前 ID、文件路径与摘要,以及通用索引/选读命令。不会启用差分交付、
推进原生基线;语义 owner 与非 POSIX CLI 仍返回完整内联响应。不会自动接受提案或替换引用。消费者先读
适用的 Goal、验收、约束、stride 合同和决策必要事实,按需读取证据与历史,不要求
遍历整份文件;重新判断后,再使用
原 Goal/Agent/Turn/obligation 和结算身份重试原写回。拒绝响应不再给出丢失身份的
通用重试命令,不写入进展或扣额;相关事实再次变化仍可再次拒绝。其他校验失败
通用重试命令,不写入进展或扣额;相关事实再次变化仍可再次拒绝。私有落盘失败时,
紧凑响应说明传输不可用;修复后重试原写回以取得当前恢复文件,保留先前已提交的
refinement 回执,不替换成旧依据或重复准入。其他校验失败
保留原恢复路径,stride 关闭时的行为不变。

This does not classify another agent's outcome as irrelevant. Shared acceptance
Expand All @@ -116,6 +130,13 @@ The File/SQLite CLI test covers a source change, readback of the changed facts,
reassessment, accepted writeback and an idempotent single spend; it does not
qualify installed-host adoption or the complete unified-contract migration.

Autonomous-replan settlement guidance uses `--delivery-boundary semantic_closeout`,
including Todo-bound reviews whose work remains open. Ordinary in-flight work
retains its existing boundary; the projection does not grant completion or spend.

自主重规划的结算指引使用 `--delivery-boundary semantic_closeout`,包括工作仍未完成的
Todo 绑定评审。普通未完成工作保留原边界;投影不授予完成或扣额权限。

这不把其他 Agent 的 outcome 自动判为无关。共享验收和要求状态的变化仍属于评审
依据;只有明确的依赖范围契约通过验收,才能缩小失效范围。同 Turn 恢复成功不
证明模型 token 减少或效果改善。File/SQLite CLI 测试覆盖来源变化、新事实读回、
Expand Down
27 changes: 27 additions & 0 deletions loopx/cli_commands/context_machine.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import os
from pathlib import Path
import stat
import tempfile
from typing import Any

from ..control_plane.turn_driver.codex_context import context_delivery_for_arguments

Expand Down Expand Up @@ -40,6 +42,31 @@ def _private_transport_root(runtime_root: Path) -> str:
return str(root)


def capture_context_recovery(value: dict[str, Any], *, runtime_root: Path) -> dict[str, Any]:
"""Publish exact write-time facts through the existing generic file reader.

This does not present a delta, advance a native baseline or rerun admission.
A failed publication leaves the rejected operation and any earlier effects
unchanged; retry that same operation after repairing its private transport.
"""
from ..control_plane.turn_driver.decision_capture import (
capture_context_projection, context_capture_read_command,
)

try:
directory = Path(tempfile.mkdtemp(prefix="recovery-", dir=_private_transport_root(runtime_root)))
reference = capture_context_projection(value, directory=directory, filename="required-context.json")
path, digest = Path(reference["path"]), reference["sha256"]
return {"path": str(path), "sha256": digest,
"index_command": context_capture_read_command(path, digest, list_fields=True, list_depth=2),
"select_command": context_capture_read_command(path, digest, select=["<pointer>"])}
except (OSError, ValueError) as error:
return {"available": False, "error_kind": type(error).__name__,
"instruction": "Repair the private context transport, then retry the original unchanged operation "
"to obtain its current recovery reference. Do not rerun admission, substitute an old capture, "
"or repeat an already committed effect."}


def apply_context_machine_options(
args: argparse.Namespace, *, registry_path: Path, runtime_root_arg: str | None,
heartbeat: bool = False,
Expand Down
12 changes: 12 additions & 0 deletions loopx/cli_commands/project_lifecycle_refresh_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import argparse
import json
import os
from collections.abc import Callable, Mapping, Sequence
from pathlib import Path

Expand Down Expand Up @@ -663,6 +664,17 @@ def handle_refresh_state_command(
agent_id=args.agent_id,
runtime_root=args.runtime_root,
)
recovery = transition.get("context_recovery")
if fmt == "json" and os.name == "posix" and isinstance(recovery, dict):
from .context_machine import capture_context_recovery

reference = capture_context_recovery(recovery,
runtime_root=resolve_runtime_root(load_registry(registry_path), args.runtime_root))
transition["context_recovery"] = {
"received_context_id": recovery["received_context_id"],
"current_context_id": recovery["replan_context"]["context_id"],
"instruction": recovery["instruction"], **reference,
}
payload["replan_transition"] = transition
if transition["next_cli_actions"]:
payload["error"] += " Required transition: " + "; ".join(
Expand Down
12 changes: 7 additions & 5 deletions loopx/control_plane/goals/vision_checkpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,7 @@ function prepareVisionRefresh(request: VisionRefreshPrepareRequest): JsonObject
}

const [pathDelta, pathDeltaUsage] = normalizeGoalPathDelta(updatePacket.path_delta);
const visionUsage = Object.values(fieldUsage).reduce((total, used) => total + used, 0);
Object.assign(fieldUsage, pathDeltaUsage);
const [strideDecision, strideUsage] = normalizeStrideDecision(updatePacket.stride_decision, boundedPublicText);
const allowedStridePaths = request.allow_terminal_stride_path
Expand All @@ -675,11 +676,12 @@ function prepareVisionRefresh(request: VisionRefreshPrepareRequest): JsonObject
Object.assign(fieldUsage, fallbackUsage);
const totalUsage = Object.values(fieldUsage).reduce((total, used) => total + used, 0);
if (totalUsage > GOAL_VISION_TOTAL_LIMIT) {
throw visionBudgetError(
"total_agent_vision",
totalUsage,
GOAL_VISION_TOTAL_LIMIT,
);
const sections = {vision_patch: visionUsage, path_delta: pathDeltaUsage,
stride_decision: strideUsage, fallback_declarations: fallbackUsage};
const usage = Object.entries(sections).map(([name, fields]) => name + "=" +
(typeof fields === "number" ? fields : Object.values(fields).reduce((total, used) => total + used, 0))).join(", ");
throw new EffectRuntimeRequestError(`${GOAL_VISION_BUDGET_ERROR}: total_agent_vision uses ${totalUsage} chars; ` +
`limit is ${GOAL_VISION_TOTAL_LIMIT}; usage: ${usage}; shorten counted text`, GOAL_VISION_BUDGET_ERROR);
}

const todoDelta: string[] = [];
Expand Down
7 changes: 4 additions & 3 deletions loopx/control_plane/quota/settlement_plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ export function turnScopedCliSettlementPlan(params: JsonObject): SettlementPlan
throw new Error("settlement plan GoalRef is invalid or does not match identity");
}
const inFlight = params.delivery_boundary === "in_flight_continuation";
const autonomousReplan = params.autonomous_replan === undefined ? false
: requireBoolean(params.autonomous_replan, "autonomous_replan");
const autonomousReplan = (params.autonomous_replan === undefined ? false
: requireBoolean(params.autonomous_replan, "autonomous_replan")) || identity.binding_kind === "autonomous_replan";
const validation: SettlementStep = {
kind: "validation", owner: "agent", idempotency_key_ref: "$.identity.effect_id",
expected_receipt: "validation_receipt",
Expand All @@ -80,7 +80,8 @@ export function turnScopedCliSettlementPlan(params: JsonObject): SettlementPlan
: "validation succeeded; " + VISION_MATERIAL_CLOSEOUT_HINT +
" Pass a new packet with --agent-vision-json.") +
" Route elimination needs evidence; failure alone is not progress. " +
"outcome_progress may use in_flight_continuation; outcome_gap requires " +
(autonomousReplan ? "Autonomous replan uses --delivery-boundary semantic_closeout even while its Todo remains open; "
: "outcome_progress may use in_flight_continuation; ") + "outcome_gap requires " +
"semantic_closeout plus blocked + blocker/evidence IDs and continuation checks. " +
"For an admitted open continuous Monitor whose observation is unavailable, " +
"use this original identity for typed blocked outcome_gap; it settles without " +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ def build_autonomous_replan_cli_actions(
f"{cli_prefix} --format json refresh-state --goal-id {goal_id} "
"--progress-scope agent_lane "
"--classification bounded_replan_progress "
"--delivery-boundary semantic_closeout "
f"{delivery_args}{semantic_delta_args}"
f"{settlement_args}{scoped_cli_args}"
)
Expand Down
5 changes: 3 additions & 2 deletions loopx/control_plane/work_items/replan_semantics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,8 +542,9 @@ export function projectReplanSemantics(value: unknown): JsonObject {
received_context_id: strideDecision!.reference_context_id,
replan_context: structuredClone(context),
stride_control: strideControlContract(context, obligation.obligation_id),
instruction: "Read the complete returned replan_context and stride_control, including Goal acceptance, evidence, followthrough and applicable source reads. " +
"Reassess the decision against those facts before replacing its reference_context_id; copying the new id alone is not a review. " +
instruction: "The exact current review basis is available for progressive reading. Read the applicable Goal, acceptance, constraints, " +
"stride contract and decision-relevant facts; consult evidence and history when needed, without draining the whole file. " +
"Reassess against those facts before replacing reference_context_id; copying the new id alone is not a review. " +
"Retry the original writeback with the same Goal, Agent, Turn and replan obligation and its original settlement identity. " +
"This response grants no new execution or spend authority and commits no writeback. Do not start another Turn or blindly rerun admission. " +
"If the basis changes again, the writer will reject again and return the then-current context.",
Expand Down
36 changes: 36 additions & 0 deletions tests/control_plane/test_context_machine_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,42 @@
)


@pytest.mark.parametrize("unsafe", [None, "symlink", "public"])
@pytest.mark.skipif(os.name != "posix", reason="Private POSIX file transport; other CLI hosts retain inline recovery")
def test_rejected_context_snapshot_uses_private_generic_transport_without_native_activation(tmp_path, unsafe):
from loopx.cli_commands.context_machine import capture_context_recovery
from loopx.control_plane.turn_driver.decision_capture import read_context_capture_file

runtime = tmp_path / "runtime"
runtime.mkdir()
root = runtime / "context-delivery"
if unsafe == "symlink":
target = tmp_path / "other-owner"
target.mkdir()
root.symlink_to(target, target_is_directory=True)
elif unsafe == "public":
root.mkdir(mode=0o755)
value = {"current": {"goal": "Retain scope", "stop": "No new authority"},
"optional_evidence": "完整材料" * 20000}
result = capture_context_recovery(value, runtime_root=runtime)
if unsafe:
assert result["available"] is False and result["error_kind"] == "ValueError"
assert "path" not in result and "optional_evidence" not in result
assert (root.is_symlink() if unsafe == "symlink" else root.stat().st_mode & 0o077)
assert list((target if unsafe == "symlink" else root).iterdir()) == []
else:
path, raw, saved = read_context_capture_file(result["path"], result["sha256"], private=True)
assert saved == value and len(raw) > 200000
assert path.stat().st_mode & 0o077 == 0
assert path.parent.stat().st_mode & 0o077 == 0
assert "--list" in result["index_command"] and "--select" in result["select_command"]
# One immutable observation per rejection; publication cannot replace a
# different write-time basis or create/advance a native session baseline.
other = capture_context_recovery({**value, "current": {"goal": "Changed scope"}}, runtime_root=runtime)
assert other["path"] != result["path"] and other["sha256"] != result["sha256"]
assert read_context_capture_file(result["path"], result["sha256"], private=True)[2] == value
assert sorted(p.name for p in root.iterdir()) == sorted([path.parent.name, Path(other["path"]).parent.name])

def configure(registry, runtime, root, mode):
config = root / "context-setting.json"
config.write_text(json.dumps({"schema_version": CONTEXT_DELIVERY_MACHINE_SCHEMA, "mode": mode}))
Expand Down
16 changes: 15 additions & 1 deletion tests/control_plane/test_effective_turn_replan.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Receipt-backed cadence over an open Todo, through the real CLI and runtime."""

import json
import os
import shlex
import subprocess
import sys
Expand Down Expand Up @@ -469,7 +470,20 @@ def call(*args: str, expected_code: int = 0) -> dict:
assert transition["reason_code"] == "stride_review_basis_mismatch"
assert transition["host_action"] == "read_current_replan_context"
assert transition["next_cli_actions"] == [] # No unbound repeat of the rejected write.
recovery = transition["context_recovery"]
reference = transition["context_recovery"]
# A rejected write must not eagerly import historical acceptance/evidence
# into the model. Its exact write-time basis remains independently readable.
if os.name == "posix":
assert len(json.dumps(foreign, ensure_ascii=False).encode()) < 4096
from loopx.control_plane.turn_driver.decision_capture import read_context_capture_file
saved, _, recovery = read_context_capture_file(reference["path"], reference["sha256"], private=True)
assert saved.stat().st_mode & 0o077 == 0
selected = call("quota", "context-read", "--capture-file", reference["path"],
"--capture-sha256", reference["sha256"], "--select", "/replan_context/core_goal/objective")
assert selected["ok"] and selected["eof"]
assert json.loads(selected["content"])["selections"][0]["value"] == recovery["replan_context"]["core_goal"]["objective"]
else:
recovery = reference
current = recovery["replan_context"]
assert recovery["received_context_id"] == rejected_reference
assert current["obligation_id"] == obligation["obligation_id"]
Expand Down
1 change: 1 addition & 0 deletions tests/control_plane/test_vision_budget_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ def test_full_budget_roundtrips_without_erasing_replan_or_partial_writes(tmp_pat
dry_run=False, autonomous_replan_recorded=False)
assert rejected.returncode == 1
assert "total_agent_vision uses 1801 chars; limit is 1800" in fixture.payload(rejected)["error"]
assert "usage: vision_patch=1120, path_delta=681, stride_decision=0, fallback_declarations=0" in fixture.payload(rejected)["error"]
assert index.read_bytes() == before_index
assert state.read_bytes() == before_state

Expand Down
7 changes: 5 additions & 2 deletions tests/control_plane_ts/quota_settlement_plan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,10 @@ test("Todo-bound semantic planning shares its exact autonomous authoring contrac
assert.deepEqual(turnScopedCliSettlementPlan({...input, autonomous_replan: false}), ordinary);
const replan = turnScopedCliSettlementPlan({...input, autonomous_replan: true});
assert.deepEqual(replan.steps[1].vision_authoring, visionAuthoringContract(true));
assert.deepEqual({...replan, steps: replan.steps.map((step, i) => i === 1
? {...step, vision_authoring: ordinary.steps[1].vision_authoring} : step)}, ordinary);
assert.match(replan.steps[1].precondition, /Autonomous replan uses --delivery-boundary semantic_closeout/);
assert.doesNotMatch(replan.steps[1].precondition, /outcome_progress may use in_flight_continuation/);
assert.deepEqual(replan.identity, ordinary.identity);
for (const i of [0, 2, 3]) assert.deepEqual(replan.steps[i], ordinary.steps[i]);
for (const autonomous_replan of [null, "true", 1]) {
assert.throws(() => turnScopedCliSettlementPlan({...input, autonomous_replan}), /must be a boolean/);
}
Expand All @@ -107,6 +109,7 @@ test("autonomous replan never manufactures a Todo completion step", () => {
assert.deepEqual(plan.steps.map(step => step.kind), ["validation", "durable_writeback", "quota_spend"]);
assert.equal(plan.steps[0].command_template, undefined);
assert.equal(plan.identity.binding_kind, "autonomous_replan");
assert.match(plan.steps[1].precondition, /Autonomous replan uses --delivery-boundary semantic_closeout/);
assert.ok(plan.steps.every(step => step.vision_authoring === undefined));
assert.match(plan.steps[1].precondition, /^validation succeeded/);
assert.match(plan.steps[1].precondition, /Route elimination needs evidence/);
Expand Down
2 changes: 1 addition & 1 deletion tests/control_plane_ts/vision_checkpoint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ test("structured replans have a bounded 1800-character budget including path evi
}})), /path_delta.observed_reality uses 321 chars; limit is 320/);
assert.throws(() => buildVisionCheckpoint(prepareRequest({agent_vision_packet: {
...packet, path_delta: {...packet.path_delta, changed: [character.repeat(35)]},
}})), /total_agent_vision uses 1801 chars; limit is 1800/);
}})), /total_agent_vision uses 1801 chars; limit is 1800; usage: vision_patch=1120, path_delta=681, stride_decision=0, fallback_declarations=0/);
}
});

Expand Down
Loading