Skip to content

Commit fdf693c

Browse files
authored
Merge pull request #4637 from huangruiteng/codex/control-plane-outcome-reconcile-20260917
2 parents 2303ff0 + d231244 commit fdf693c

7 files changed

Lines changed: 119 additions & 27 deletions

File tree

‎docs/reference/protocols/goal-vision-replan-contract-v0.md‎

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -107,24 +107,26 @@ other's active vision.
107107

108108
### Path Delta
109109

110-
A machine-generated vision packet may include one optional
111-
`goal_path_delta_v0`. It makes a bounded loop's look-back explicit without
112-
adding more inline CLI flags or expanding the heartbeat prompt. The packet is
113-
written through the existing `--agent-vision-json` boundary and is retained in
114-
the same agent-scoped run-history and shared-runtime vision projection:
110+
A vision packet may include a top-level `path_delta` object; `goal_path_delta_v0`
111+
is its `schema_version`, not its enclosing field. The shared TypeScript authoring
112+
boundary rejects misplaced declared deltas before any write, including through
113+
CLI and Turn. It does not infer a protocol from ordinary metadata field names.
114+
Existing packets may omit the nested schema version; an explicitly supplied
115+
version must match. Historical read compaction remains unchanged.
115116

116117
```json
117118
{
118-
"schema_version": "goal_path_delta_v0",
119-
"outcome": "replan",
120-
"prior_assumption": "The current monitor lane would produce acceptance evidence.",
121-
"observed_reality": "Two bounded polls produced no material transition.",
122-
"retained": ["Keep the verified monitor target and evidence refs."],
123-
"changed": ["Create one runnable advancement successor."],
124-
"stopped": ["Stop treating future polling as completion evidence."],
125-
"unresolved_questions": ["Which successor can falsify the new path?"],
126-
"reentry_condition": "Resume the monitor-only wait after successor evidence lands.",
127-
"evidence_refs": ["evidence:monitor-poll-02", "todo:successor-01"]
119+
"vision_patch": {"vision_summary": "Deliver the verified successor."},
120+
"path_delta": {
121+
"schema_version": "goal_path_delta_v0",
122+
"outcome": "replan",
123+
"prior_assumption": "Polling would produce acceptance evidence.",
124+
"observed_reality": "Repeated polls produced no material transition.",
125+
"retained": ["Keep the verified monitor target."],
126+
"changed": ["Create one runnable advancement successor."],
127+
"stopped": ["Stop treating polling as completion evidence."],
128+
"evidence_refs": ["evidence:monitor-poll", "todo:successor"]
129+
}
128130
}
129131
```
130132

@@ -232,6 +234,14 @@ Valid checkpoint decisions are:
232234
- `not_required`: no material closeout trigger was present, including a valid
233235
typed in-flight continuation.
234236

237+
A material closeout should carry its own vision patch or evidence-backed unchanged
238+
reason. If omitted, `refresh-state` still records the outcome and returns the
239+
checkpoint repair action. Follow that action in the same turn with the original
240+
settlement identity, removing already executed state mutations. The supplement
241+
must satisfy the checkpoint before terminal closeout; it neither re-authors the
242+
outcome nor spends a second time. Never invent an unchanged reason to clear a gap.
243+
Typed in-flight continuations keep their existing exemption.
244+
235245
`missing_required` is not a chat reminder. Status keeps it in compact run
236246
history, quota filters it by current `agent_id`, and goal-frontier projection
237247
turns it into `acceptance_gaps[]`. If the current agent has no runnable

‎examples/control_plane/heartbeat-prompt-smoke.py‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,10 @@ def user_output_policy(task_body: str, *, mode: str) -> dict[str, str]:
8484

8585
def assert_sole_notification_authority(task_body: str, *, mode: str) -> None:
8686
body = normalized(task_body)
87-
assert "no-change=`surface_only`/no spend; unchanged->" in body, mode
88-
assert "`--vision-unchanged-reason`; material->actual outcome." in body, mode
87+
assert "no-change=`surface_only`/no spend" in body, mode
88+
assert "material=实际outcome+vision决定" in body, mode
89+
assert "缺则同turn按返回命令补齐再terminal" in body, mode
90+
assert "unchanged→真实`--vision-unchanged-reason`" in body, mode
8991

9092
if mode == "full":
9193
assert (
@@ -596,7 +598,7 @@ def main() -> int:
596598
"host_action=pause_or_delete_current_heartbeat->automation_update stop(no-spend)",
597599
"else RRULE/projected-fallback_hint/ack/fail",
598600
"no-change=`surface_only`/no spend",
599-
"unchanged->`--vision-unchanged-reason`",
601+
"unchanged→真实`--vision-unchanged-reason`",
600602
"guard; 2 stalls->replan",
601603
"`agent_read_required`",
602604
"drain/read/triage before work; settle/ACK",
@@ -698,7 +700,7 @@ def main() -> int:
698700
"host_action=pause_or_delete_current_heartbeat->automation_update stop(no-spend)",
699701
"else RRULE/projected-fallback_hint/ack/fail",
700702
"no-change=`surface_only`/no spend",
701-
"unchanged->`--vision-unchanged-reason`",
703+
"unchanged→真实`--vision-unchanged-reason`",
702704
"guard; 2 stalls->replan",
703705
"P0 blocked: safe P1/P2",
704706
"monitor quiet/no-spend",
@@ -714,7 +716,7 @@ def main() -> int:
714716
):
715717
assert "no-change=`surface_only`/no spend" in task, label
716718
assert "`--vision-unchanged-reason`" in task, label
717-
assert "material->actual outcome" in task, label
719+
assert "material=实际outcome+vision决定" in task, label
718720
assert "if absent say" not in thin_task, thin_task
719721
assert "If false/0: quiet/no-user-todo" not in thin_task, thin_task
720722

‎examples/project/goal-vision-refresh-state-budget-smoke.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@ def main() -> int:
375375
check=False,
376376
)
377377
assert unexplained_inline_drift.returncode == 1, unexplained_inline_drift
378-
assert "provide goal_path_delta_v0 with outcome=replan" in payload(
378+
assert "provide path_delta with schema_version=goal_path_delta_v0 and outcome=replan" in payload(
379379
unexplained_inline_drift
380380
)["error"], unexplained_inline_drift.stdout
381381

@@ -391,7 +391,7 @@ def main() -> int:
391391
check=False,
392392
)
393393
assert unexplained_drift.returncode == 1, unexplained_drift
394-
assert "provide goal_path_delta_v0 with outcome=replan" in payload(
394+
assert "provide path_delta with schema_version=goal_path_delta_v0 and outcome=replan" in payload(
395395
unexplained_drift
396396
)["error"], unexplained_drift.stdout
397397

‎loopx/control_plane/goals/vision_checkpoint.ts‎

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ const BASIC_CREDENTIAL_VALUE =
115115
const PRIVATE_TEXT_PATTERNS = [
116116
/\/Users\//,
117117
/\/ext_data\//,
118-
/larkoffice/i,
118+
/lark[o]ffice/i, // Equivalent matcher avoids matching its own policy source.
119119
/docs\.internal/i,
120120
/\bt-20\d{12}-[a-z0-9]+\b/,
121121
/\bBearer\b/i,
@@ -340,6 +340,11 @@ function normalizeGoalPathDelta(
340340
): [JsonObject | null, Record<string, number>] {
341341
if (value === null || value === undefined) return [null, {}];
342342
const source = requiredObject(value, "agent_vision.path_delta");
343+
if (source.schema_version !== undefined && source.schema_version !== GOAL_PATH_DELTA_SCHEMA_VERSION) {
344+
throw new EffectRuntimeRequestError(
345+
`agent_vision.path_delta.schema_version must be ${GOAL_PATH_DELTA_SCHEMA_VERSION}`,
346+
);
347+
}
343348
const outcome = compactText(source.outcome).toLowerCase().replaceAll("-", "_");
344349
if (!(GOAL_PATH_DELTA_OUTCOMES as readonly string[]).includes(outcome)) {
345350
throw new EffectRuntimeRequestError(
@@ -484,6 +489,22 @@ function decodePrepareRequest(request: JsonObject): VisionRefreshPrepareRequest
484489

485490
function prepareVisionRefresh(request: VisionRefreshPrepareRequest): JsonObject {
486491
const packet = request.agent_vision_packet;
492+
// Validate authoring before merge/compaction can silently discard a declared
493+
// protocol. Ordinary extension metadata is not classified by overlapping keys.
494+
for (const [container, prefix] of [[packet, "agent_vision"], [packet.vision_patch, "agent_vision.vision_patch"]] as const) {
495+
if (typeof container !== "object" || container === null || Array.isArray(container)) continue;
496+
for (const [field, value] of Object.entries(container)) {
497+
if (prefix === "agent_vision" && field === "path_delta") continue;
498+
if (field === GOAL_PATH_DELTA_SCHEMA_VERSION ||
499+
(prefix === "agent_vision.vision_patch" && field === "path_delta") ||
500+
(typeof value === "object" && value !== null && !Array.isArray(value) &&
501+
(value as JsonObject).schema_version === GOAL_PATH_DELTA_SCHEMA_VERSION)) {
502+
throw new EffectRuntimeRequestError(
503+
`${prefix}.${field} must be supplied as agent_vision.path_delta; ${GOAL_PATH_DELTA_SCHEMA_VERSION} is the schema_version, not the enclosing field`,
504+
);
505+
}
506+
}
507+
}
487508
const existing = request.existing_agent_vision ?? {};
488509
const updatePacket: JsonObject = { ...packet };
489510
if (request.merge_patch && Object.keys(existing).length > 0) {
@@ -619,7 +640,7 @@ function prepareVisionRefresh(request: VisionRefreshPrepareRequest): JsonObject
619640
);
620641
if (changedFields.length > 0 && pathDelta?.outcome !== "replan") {
621642
throw new EffectRuntimeRequestError(
622-
`autonomous agent vision replan changes durable fields ${changedFields.join(", ")}; provide goal_path_delta_v0 with outcome=replan so the mainline change is explicit`,
643+
`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`,
623644
);
624645
}
625646
}

‎loopx/control_plane/heartbeat/rules.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
"具体user todo未投影,需修复LoopX状态投影;静默时内部修复。"
2323
)
2424
HEARTBEAT_VISION_WRITEBACK_RULE_SHORT = (
25-
"writeback: no-change=`surface_only`/no spend; "
26-
"unchanged->`--vision-unchanged-reason`; material->actual outcome."
25+
"writeback: no-change=`surface_only`/no spend;material=实际outcome+vision决定;"
26+
"缺则同turn按返回命令补齐再terminal;unchanged→真实`--vision-unchanged-reason`。"
2727
)
2828
REWARD_MEMORY_OUTCOME_RULE = (
2929
"`reward_memory_recall.experiment.automatic_ingest=true`: reusable Todo outcomes "

‎tests/control_plane/test_vision_budget_cli.py‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,34 @@ def test_full_budget_roundtrips_without_erasing_replan_or_partial_writes(tmp_pat
7070
assert "total_agent_vision uses 1801 chars; limit is 1800" in fixture.payload(rejected)["error"]
7171
assert index.read_bytes() == before_index
7272
assert state.read_bytes() == before_state
73+
74+
75+
def test_misplaced_delta_rejects_before_write_and_corrected_packet_roundtrips(tmp_path):
76+
source = Path(__file__).resolve().parents[2] / "examples/project/goal-vision-refresh-state-budget-smoke.py"
77+
spec = importlib.util.spec_from_file_location("vision_packet_fixture", source)
78+
fixture = importlib.util.module_from_spec(spec)
79+
spec.loader.exec_module(fixture)
80+
registry, runtime, project = fixture.write_fixture(tmp_path)
81+
state = project / ".codex/goals" / fixture.GOAL_ID / "ACTIVE_GOAL_STATE.md"
82+
before = state.read_bytes()
83+
delta = {"schema_version": "goal_path_delta_v0", "outcome": "replan",
84+
"prior_assumption": "Keep the route.", "observed_reality": "A dependency changed.",
85+
"changed": ["Use the successor."]}
86+
packet = {"vision_patch": {"vision_summary": "Deliver the successor."},
87+
"goal_path_delta_v0": delta}
88+
path = tmp_path / "vision.json"
89+
fixture.write_json(path, packet)
90+
rejected = fixture.run_cli(registry, runtime, vision_path=path, check=False,
91+
dry_run=False, autonomous_replan_recorded=False)
92+
assert rejected.returncode == 1
93+
assert "must be supplied as agent_vision.path_delta" in fixture.payload(rejected)["error"]
94+
assert state.read_bytes() == before
95+
index = runtime / "goals" / fixture.GOAL_ID / "runs/index.jsonl"
96+
assert not index.exists()
97+
packet["path_delta"] = packet.pop("goal_path_delta_v0")
98+
packet["telemetry"] = {"outcome": "ok", "evidence_refs": ["evidence:probe"]}
99+
fixture.write_json(path, packet)
100+
result = fixture.payload(fixture.run_cli(registry, runtime, vision_path=path, check=True,
101+
dry_run=False, autonomous_replan_recorded=False))
102+
assert result["agent_vision"]["path_delta"] == delta
103+
assert json.loads(index.read_text().splitlines()[-1])["agent_vision"]["path_delta"] == delta

‎tests/control_plane_ts/vision_checkpoint.test.ts‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,34 @@ test("prepare owns packet normalization, budgets, and path delta", () => {
9191
});
9292
});
9393

94+
test("authoring rejects misplaced declared deltas without classifying telemetry", () => {
95+
const delta = {schema_version: "goal_path_delta_v0", outcome: "replan",
96+
prior_assumption: "Keep the route.", observed_reality: "A dependency changed.",
97+
changed: ["Use the successor."]};
98+
const patch = {vision_summary: "Deliver the successor."};
99+
for (const extra of [
100+
{goal_path_delta_v0: delta}, {comparison: delta},
101+
{path_delta: delta, comparison: delta},
102+
{vision_patch: {...patch, path_delta: delta}},
103+
]) {
104+
assert.throws(() => buildVisionCheckpoint(prepareRequest({
105+
agent_vision_packet: {vision_patch: patch, ...extra},
106+
})), /must be supplied as agent_vision.path_delta/);
107+
}
108+
const telemetry = {outcome: "ok", evidence_refs: ["evidence:probe"]};
109+
const baseline = buildVisionCheckpoint(prepareRequest({agent_vision_packet: {vision_patch: patch}}));
110+
assert.deepEqual(buildVisionCheckpoint(prepareRequest({
111+
agent_vision_packet: {vision_patch: patch, telemetry},
112+
})), baseline);
113+
assert.throws(() => buildVisionCheckpoint(prepareRequest({agent_vision_packet: {
114+
vision_patch: patch, path_delta: {...delta, schema_version: "unsupported"},
115+
}})), /path_delta.schema_version must be goal_path_delta_v0/);
116+
const accepted = buildVisionCheckpoint(prepareRequest({agent_vision_packet: {
117+
vision_patch: patch, path_delta: delta, telemetry,
118+
}}));
119+
assert.deepEqual((accepted.agent_vision as Record<string, unknown>).path_delta, delta);
120+
});
121+
94122
test("structured replans have a bounded 1800-character budget including path evidence", () => {
95123
for (const character of ["x", "界"]) {
96124
// 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", () => {
183211
merge_patch: true,
184212
require_path_delta_for_durable_change: true,
185213
})),
186-
/provide goal_path_delta_v0 with outcome=replan/,
214+
/provide path_delta with schema_version=goal_path_delta_v0 and outcome=replan/,
187215
);
188216
});
189217

0 commit comments

Comments
 (0)