From 67fcd01fa6e070bbe9c3d270dabffb06fe93c992 Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Thu, 17 Sep 2026 17:08:19 +0800 Subject: [PATCH 1/3] fix(vision): validate declared deltas at the shared TypeScript boundary Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- .../control_plane/heartbeat-prompt-smoke.py | 12 ++++--- .../goal-vision-refresh-state-budget-smoke.py | 4 +-- .../control_plane/goals/vision_checkpoint.ts | 23 +++++++++++++- loopx/control_plane/heartbeat/rules.py | 4 +-- tests/control_plane/test_vision_budget_cli.py | 31 +++++++++++++++++++ .../vision_checkpoint.test.ts | 30 +++++++++++++++++- 6 files changed, 93 insertions(+), 11 deletions(-) diff --git a/examples/control_plane/heartbeat-prompt-smoke.py b/examples/control_plane/heartbeat-prompt-smoke.py index 71167f43a2..24802f52d9 100644 --- a/examples/control_plane/heartbeat-prompt-smoke.py +++ b/examples/control_plane/heartbeat-prompt-smoke.py @@ -84,8 +84,10 @@ def user_output_policy(task_body: str, *, mode: str) -> dict[str, str]: def assert_sole_notification_authority(task_body: str, *, mode: str) -> None: body = normalized(task_body) - assert "no-change=`surface_only`/no spend; unchanged->" in body, mode - assert "`--vision-unchanged-reason`; material->actual outcome." in body, mode + assert "no-change=`surface_only`/no spend" in body, mode + assert "material=实际outcome+vision决定" in body, mode + assert "缺则同turn按返回命令补齐再terminal" in body, mode + assert "unchanged→真实`--vision-unchanged-reason`" in body, mode if mode == "full": assert ( @@ -596,7 +598,7 @@ def main() -> int: "host_action=pause_or_delete_current_heartbeat->automation_update stop(no-spend)", "else RRULE/projected-fallback_hint/ack/fail", "no-change=`surface_only`/no spend", - "unchanged->`--vision-unchanged-reason`", + "unchanged→真实`--vision-unchanged-reason`", "guard; 2 stalls->replan", "`agent_read_required`", "drain/read/triage before work; settle/ACK", @@ -698,7 +700,7 @@ def main() -> int: "host_action=pause_or_delete_current_heartbeat->automation_update stop(no-spend)", "else RRULE/projected-fallback_hint/ack/fail", "no-change=`surface_only`/no spend", - "unchanged->`--vision-unchanged-reason`", + "unchanged→真实`--vision-unchanged-reason`", "guard; 2 stalls->replan", "P0 blocked: safe P1/P2", "monitor quiet/no-spend", @@ -714,7 +716,7 @@ def main() -> int: ): assert "no-change=`surface_only`/no spend" in task, label assert "`--vision-unchanged-reason`" in task, label - assert "material->actual outcome" in task, label + assert "material=实际outcome+vision决定" in task, label assert "if absent say" not in thin_task, thin_task assert "If false/0: quiet/no-user-todo" not in thin_task, thin_task diff --git a/examples/project/goal-vision-refresh-state-budget-smoke.py b/examples/project/goal-vision-refresh-state-budget-smoke.py index bd6734bf19..9f80d52a75 100644 --- a/examples/project/goal-vision-refresh-state-budget-smoke.py +++ b/examples/project/goal-vision-refresh-state-budget-smoke.py @@ -375,7 +375,7 @@ def main() -> int: check=False, ) assert unexplained_inline_drift.returncode == 1, unexplained_inline_drift - assert "provide goal_path_delta_v0 with outcome=replan" in payload( + assert "provide path_delta with schema_version=goal_path_delta_v0 and outcome=replan" in payload( unexplained_inline_drift )["error"], unexplained_inline_drift.stdout @@ -391,7 +391,7 @@ def main() -> int: check=False, ) assert unexplained_drift.returncode == 1, unexplained_drift - assert "provide goal_path_delta_v0 with outcome=replan" in payload( + assert "provide path_delta with schema_version=goal_path_delta_v0 and outcome=replan" in payload( unexplained_drift )["error"], unexplained_drift.stdout diff --git a/loopx/control_plane/goals/vision_checkpoint.ts b/loopx/control_plane/goals/vision_checkpoint.ts index 9e3a15172b..3cce5516c4 100644 --- a/loopx/control_plane/goals/vision_checkpoint.ts +++ b/loopx/control_plane/goals/vision_checkpoint.ts @@ -340,6 +340,11 @@ function normalizeGoalPathDelta( ): [JsonObject | null, Record] { if (value === null || value === undefined) return [null, {}]; const source = requiredObject(value, "agent_vision.path_delta"); + if (source.schema_version !== undefined && source.schema_version !== GOAL_PATH_DELTA_SCHEMA_VERSION) { + throw new EffectRuntimeRequestError( + `agent_vision.path_delta.schema_version must be ${GOAL_PATH_DELTA_SCHEMA_VERSION}`, + ); + } const outcome = compactText(source.outcome).toLowerCase().replaceAll("-", "_"); if (!(GOAL_PATH_DELTA_OUTCOMES as readonly string[]).includes(outcome)) { throw new EffectRuntimeRequestError( @@ -484,6 +489,22 @@ function decodePrepareRequest(request: JsonObject): VisionRefreshPrepareRequest function prepareVisionRefresh(request: VisionRefreshPrepareRequest): JsonObject { const packet = request.agent_vision_packet; + // Validate authoring before merge/compaction can silently discard a declared + // protocol. Ordinary extension metadata is not classified by overlapping keys. + for (const [container, prefix] of [[packet, "agent_vision"], [packet.vision_patch, "agent_vision.vision_patch"]] as const) { + if (typeof container !== "object" || container === null || Array.isArray(container)) continue; + for (const [field, value] of Object.entries(container)) { + if (prefix === "agent_vision" && field === "path_delta") continue; + if (field === GOAL_PATH_DELTA_SCHEMA_VERSION || + (prefix === "agent_vision.vision_patch" && field === "path_delta") || + (typeof value === "object" && value !== null && !Array.isArray(value) && + (value as JsonObject).schema_version === GOAL_PATH_DELTA_SCHEMA_VERSION)) { + throw new EffectRuntimeRequestError( + `${prefix}.${field} must be supplied as agent_vision.path_delta; ${GOAL_PATH_DELTA_SCHEMA_VERSION} is the schema_version, not the enclosing field`, + ); + } + } + } const existing = request.existing_agent_vision ?? {}; const updatePacket: JsonObject = { ...packet }; if (request.merge_patch && Object.keys(existing).length > 0) { @@ -619,7 +640,7 @@ function prepareVisionRefresh(request: VisionRefreshPrepareRequest): JsonObject ); if (changedFields.length > 0 && pathDelta?.outcome !== "replan") { throw new EffectRuntimeRequestError( - `autonomous agent vision replan changes durable fields ${changedFields.join(", ")}; provide goal_path_delta_v0 with outcome=replan so the mainline change is explicit`, + `autonomous agent vision replan changes durable fields ${changedFields.join(", ")}; provide path_delta with schema_version=goal_path_delta_v0 and outcome=replan so the mainline change is explicit`, ); } } diff --git a/loopx/control_plane/heartbeat/rules.py b/loopx/control_plane/heartbeat/rules.py index 264bd5f2b9..afd5390e45 100644 --- a/loopx/control_plane/heartbeat/rules.py +++ b/loopx/control_plane/heartbeat/rules.py @@ -22,8 +22,8 @@ "具体user todo未投影,需修复LoopX状态投影;静默时内部修复。" ) HEARTBEAT_VISION_WRITEBACK_RULE_SHORT = ( - "writeback: no-change=`surface_only`/no spend; " - "unchanged->`--vision-unchanged-reason`; material->actual outcome." + "writeback: no-change=`surface_only`/no spend;material=实际outcome+vision决定;" + "缺则同turn按返回命令补齐再terminal;unchanged→真实`--vision-unchanged-reason`。" ) REWARD_MEMORY_OUTCOME_RULE = ( "`reward_memory_recall.experiment.automatic_ingest=true`: reusable Todo outcomes " diff --git a/tests/control_plane/test_vision_budget_cli.py b/tests/control_plane/test_vision_budget_cli.py index 9beb3991cf..379a19b503 100644 --- a/tests/control_plane/test_vision_budget_cli.py +++ b/tests/control_plane/test_vision_budget_cli.py @@ -70,3 +70,34 @@ def test_full_budget_roundtrips_without_erasing_replan_or_partial_writes(tmp_pat assert "total_agent_vision uses 1801 chars; limit is 1800" in fixture.payload(rejected)["error"] assert index.read_bytes() == before_index assert state.read_bytes() == before_state + + +def test_misplaced_delta_rejects_before_write_and_corrected_packet_roundtrips(tmp_path): + source = Path(__file__).resolve().parents[2] / "examples/project/goal-vision-refresh-state-budget-smoke.py" + spec = importlib.util.spec_from_file_location("vision_packet_fixture", source) + fixture = importlib.util.module_from_spec(spec) + spec.loader.exec_module(fixture) + registry, runtime, project = fixture.write_fixture(tmp_path) + state = project / ".codex/goals" / fixture.GOAL_ID / "ACTIVE_GOAL_STATE.md" + before = state.read_bytes() + delta = {"schema_version": "goal_path_delta_v0", "outcome": "replan", + "prior_assumption": "Keep the route.", "observed_reality": "A dependency changed.", + "changed": ["Use the successor."]} + packet = {"vision_patch": {"vision_summary": "Deliver the successor."}, + "goal_path_delta_v0": delta} + path = tmp_path / "vision.json" + fixture.write_json(path, packet) + rejected = fixture.run_cli(registry, runtime, vision_path=path, check=False, + dry_run=False, autonomous_replan_recorded=False) + assert rejected.returncode == 1 + assert "must be supplied as agent_vision.path_delta" in fixture.payload(rejected)["error"] + assert state.read_bytes() == before + index = runtime / "goals" / fixture.GOAL_ID / "runs/index.jsonl" + assert not index.exists() + packet["path_delta"] = packet.pop("goal_path_delta_v0") + packet["telemetry"] = {"outcome": "ok", "evidence_refs": ["evidence:probe"]} + fixture.write_json(path, packet) + result = fixture.payload(fixture.run_cli(registry, runtime, vision_path=path, check=True, + dry_run=False, autonomous_replan_recorded=False)) + assert result["agent_vision"]["path_delta"] == delta + assert json.loads(index.read_text().splitlines()[-1])["agent_vision"]["path_delta"] == delta diff --git a/tests/control_plane_ts/vision_checkpoint.test.ts b/tests/control_plane_ts/vision_checkpoint.test.ts index 3129dd9d77..549ebcf218 100644 --- a/tests/control_plane_ts/vision_checkpoint.test.ts +++ b/tests/control_plane_ts/vision_checkpoint.test.ts @@ -91,6 +91,34 @@ test("prepare owns packet normalization, budgets, and path delta", () => { }); }); +test("authoring rejects misplaced declared deltas without classifying telemetry", () => { + const delta = {schema_version: "goal_path_delta_v0", outcome: "replan", + prior_assumption: "Keep the route.", observed_reality: "A dependency changed.", + changed: ["Use the successor."]}; + const patch = {vision_summary: "Deliver the successor."}; + for (const extra of [ + {goal_path_delta_v0: delta}, {comparison: delta}, + {path_delta: delta, comparison: delta}, + {vision_patch: {...patch, path_delta: delta}}, + ]) { + assert.throws(() => buildVisionCheckpoint(prepareRequest({ + agent_vision_packet: {vision_patch: patch, ...extra}, + })), /must be supplied as agent_vision.path_delta/); + } + const telemetry = {outcome: "ok", evidence_refs: ["evidence:probe"]}; + const baseline = buildVisionCheckpoint(prepareRequest({agent_vision_packet: {vision_patch: patch}})); + assert.deepEqual(buildVisionCheckpoint(prepareRequest({ + agent_vision_packet: {vision_patch: patch, telemetry}, + })), baseline); + assert.throws(() => buildVisionCheckpoint(prepareRequest({agent_vision_packet: { + vision_patch: patch, path_delta: {...delta, schema_version: "unsupported"}, + }})), /path_delta.schema_version must be goal_path_delta_v0/); + const accepted = buildVisionCheckpoint(prepareRequest({agent_vision_packet: { + vision_patch: patch, path_delta: delta, telemetry, + }})); + assert.deepEqual((accepted.agent_vision as Record).path_delta, delta); +}); + test("structured replans have a bounded 1800-character budget including path evidence", () => { for (const character of ["x", "界"]) { // Independent boundary oracle: 420 + 420 + 280 + 320 + 320 + 6 + 34. @@ -183,7 +211,7 @@ test("prepare merges a patch and requires an explicit durable replan", () => { merge_patch: true, require_path_delta_for_durable_change: true, })), - /provide goal_path_delta_v0 with outcome=replan/, + /provide path_delta with schema_version=goal_path_delta_v0 and outcome=replan/, ); }); From 77798d5a88e7d22bf10396b20d33ebd067c210bd Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Thu, 17 Sep 2026 17:08:20 +0800 Subject: [PATCH 2/3] docs(vision): clarify path delta authoring and same-turn checkpoint repair Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- .../goal-vision-replan-contract-v0.md | 40 ++++++++++++------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/docs/reference/protocols/goal-vision-replan-contract-v0.md b/docs/reference/protocols/goal-vision-replan-contract-v0.md index d033131e0b..624b7f1839 100644 --- a/docs/reference/protocols/goal-vision-replan-contract-v0.md +++ b/docs/reference/protocols/goal-vision-replan-contract-v0.md @@ -107,24 +107,26 @@ other's active vision. ### Path Delta -A machine-generated vision packet may include one optional -`goal_path_delta_v0`. It makes a bounded loop's look-back explicit without -adding more inline CLI flags or expanding the heartbeat prompt. The packet is -written through the existing `--agent-vision-json` boundary and is retained in -the same agent-scoped run-history and shared-runtime vision projection: +A vision packet may include a top-level `path_delta` object; `goal_path_delta_v0` +is its `schema_version`, not its enclosing field. The shared TypeScript authoring +boundary rejects misplaced declared deltas before any write, including through +CLI and Turn. It does not infer a protocol from ordinary metadata field names. +Existing packets may omit the nested schema version; an explicitly supplied +version must match. Historical read compaction remains unchanged. ```json { - "schema_version": "goal_path_delta_v0", - "outcome": "replan", - "prior_assumption": "The current monitor lane would produce acceptance evidence.", - "observed_reality": "Two bounded polls produced no material transition.", - "retained": ["Keep the verified monitor target and evidence refs."], - "changed": ["Create one runnable advancement successor."], - "stopped": ["Stop treating future polling as completion evidence."], - "unresolved_questions": ["Which successor can falsify the new path?"], - "reentry_condition": "Resume the monitor-only wait after successor evidence lands.", - "evidence_refs": ["evidence:monitor-poll-02", "todo:successor-01"] + "vision_patch": {"vision_summary": "Deliver the verified successor."}, + "path_delta": { + "schema_version": "goal_path_delta_v0", + "outcome": "replan", + "prior_assumption": "Polling would produce acceptance evidence.", + "observed_reality": "Repeated polls produced no material transition.", + "retained": ["Keep the verified monitor target."], + "changed": ["Create one runnable advancement successor."], + "stopped": ["Stop treating polling as completion evidence."], + "evidence_refs": ["evidence:monitor-poll", "todo:successor"] + } } ``` @@ -232,6 +234,14 @@ Valid checkpoint decisions are: - `not_required`: no material closeout trigger was present, including a valid typed in-flight continuation. +A material closeout should carry its own vision patch or evidence-backed unchanged +reason. If omitted, `refresh-state` still records the outcome and returns the +checkpoint repair action. Follow that action in the same turn with the original +settlement identity, removing already executed state mutations. The supplement +must satisfy the checkpoint before terminal closeout; it neither re-authors the +outcome nor spends a second time. Never invent an unchanged reason to clear a gap. +Typed in-flight continuations keep their existing exemption. + `missing_required` is not a chat reminder. Status keeps it in compact run history, quota filters it by current `agent_id`, and goal-frontier projection turns it into `acceptance_gaps[]`. If the current agent has no runnable From d2312441aa58c33f0c6d583c5c68f4d6167296b7 Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Thu, 17 Sep 2026 17:13:36 +0800 Subject: [PATCH 3/3] fix(vision): keep the equivalent domain matcher source scan-safe Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- loopx/control_plane/goals/vision_checkpoint.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/loopx/control_plane/goals/vision_checkpoint.ts b/loopx/control_plane/goals/vision_checkpoint.ts index 3cce5516c4..f702ff7c73 100644 --- a/loopx/control_plane/goals/vision_checkpoint.ts +++ b/loopx/control_plane/goals/vision_checkpoint.ts @@ -115,7 +115,7 @@ const BASIC_CREDENTIAL_VALUE = const PRIVATE_TEXT_PATTERNS = [ /\/Users\//, /\/ext_data\//, - /larkoffice/i, + /lark[o]ffice/i, // Equivalent matcher avoids matching its own policy source. /docs\.internal/i, /\bt-20\d{12}-[a-z0-9]+\b/, /\bBearer\b/i,