Skip to content
Closed
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
20 changes: 19 additions & 1 deletion docs/quota-allocation.md
Original file line number Diff line number Diff line change
Expand Up @@ -1283,7 +1283,25 @@ Post-turn accounting protocol:
`refresh-state` must run from a separate registry checkout, pass
`--delivery-workspace-path <delivery-worktree>`; the path is validated locally
and omitted from persisted history. Do not point this option at the canonical
checkout for peer work.
checkout for peer work. In a guarded peer lane the two halves of one
writeback therefore run from different checkouts, because the delivery
worktree carries no project registry state and the spend guard reads the
current workspace:

```sh
# 1. from the canonical checkout, attributing the work to the delivery worktree
loopx refresh-state --goal-id <goal-id> --agent-id <agent-id> \
--delivery-workspace-path <delivery-worktree> ...

# 2. from the delivery worktree itself
cd <delivery-worktree>
loopx quota spend-slot --goal-id <goal-id> --agent-id <agent-id> ... --execute
```

`refresh-state` run inside the worktree has no active state to refresh, and a
spend run from the canonical checkout is refused with
`return_to_delivery_worktree`. Moving between the two checkouts is the
documented flow, not a workaround.
- delivery attribution is not synonymous with Git. A registered single-agent
goal whose project has no Git origin records a path-free `local_goal`
workspace identity (`loopx:<goal-id>`) when refresh runs inside that
Expand Down
39 changes: 24 additions & 15 deletions docs/reference/protocols/goal-vision-replan-contract-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,24 +107,33 @@ 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 machine-generated vision packet may include one optional path delta. 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.

The delta is a nested object of the vision packet: `goal_path_delta_v0` is its
`schema_version`, not the key that carries it. The enclosing field is
`path_delta`, as in the packet below. Nesting the delta under its own schema
name instead of `path_delta` is rejected by the write path rather than
silently dropped.

```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"]
"agent_id": "<agent that made the comparison>",
"path_delta": {
"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"]
}
}
```

Expand Down
29 changes: 29 additions & 0 deletions examples/project/goal-vision-path-delta-smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
)
from loopx.control_plane.goals.goal_vision import ( # noqa: E402
compact_goal_vision_packet,
misplaced_goal_path_delta_field,
normalize_goal_vision_packet,
)
from loopx.control_plane.runtime.shared_runtime_refresh_projection import ( # noqa: E402
Expand Down Expand Up @@ -115,6 +116,34 @@ def main() -> int:
else:
raise AssertionError("over-item path delta should fail")

# The delta is nested under `path_delta`; `goal_path_delta_v0` is only its
# schema_version. A packet that nests it under the schema name would drop
# the delta silently, so the write path has to name the accepted key.
assert misplaced_goal_path_delta_field(packet()) is None
assert misplaced_goal_path_delta_field({"path_delta": {"outcome": "wait"}}) is None
assert misplaced_goal_path_delta_field("not-a-packet") is None
misfiled = dict(packet())
misfiled["goal_path_delta_v0"] = misfiled.pop("path_delta")
assert misplaced_goal_path_delta_field(misfiled) == (
"goal_path_delta_v0",
"goal_path_delta_v0",
), misfiled
# A read-path compaction of the misfiled packet loses the delta, which is
# exactly the silent drop the write-path guard exists to prevent.
misfiled_compact = compact_goal_vision_packet(misfiled)
assert "path_delta" not in (misfiled_compact or {}), misfiled_compact
# Placing both keys is not a misfiling: the read path still finds the delta.
both_keys = dict(packet())
both_keys["goal_path_delta_v0"] = {"schema_version": "goal_path_delta_v0"}
assert misplaced_goal_path_delta_field(both_keys) is None
# One shared key is not enough to call an unrelated object a path delta.
assert (
misplaced_goal_path_delta_field({"telemetry": {"outcome": "ok"}}) is None
)
assert misplaced_goal_path_delta_field(
{"telemetry": {"outcome": "ok", "evidence_refs": ["evidence:x"]}}
) == ("telemetry", "goal_path_delta_v0")

print("goal-vision-path-delta-smoke ok")
return 0

Expand Down
15 changes: 15 additions & 0 deletions loopx/cli_commands/project_lifecycle_refresh_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
runtime_capabilities_for_cli_projection,
)
from ..control_plane.capability_hooks import PostWritebackHookRegistration
from ..control_plane.goals.goal_vision import (
GOAL_PATH_DELTA_SCHEMA_VERSION,
GOAL_VISION_PATH_DELTA_FIELD,
misplaced_goal_path_delta_field,
)
from ..control_plane.goals.goal_vision_policy import (
GOAL_VISION_ADVANCEMENT_POLICY_CHOICES,
)
Expand Down Expand Up @@ -384,6 +389,16 @@ def handle_refresh_state_command(
agent_vision_packet = json.loads(
Path(args.agent_vision_json).expanduser().read_text(encoding="utf-8")
)
misplaced_path_delta = misplaced_goal_path_delta_field(agent_vision_packet)
if misplaced_path_delta:
foreign_field, _schema_version = misplaced_path_delta
raise ValueError(
f"agent vision packet nests {GOAL_PATH_DELTA_SCHEMA_VERSION} under "
f"{foreign_field!r}, so the write path would drop it. Nest the path "
f"delta under {GOAL_VISION_PATH_DELTA_FIELD!r} in the same packet, "
f'e.g. {{"{GOAL_VISION_PATH_DELTA_FIELD}": '
f'{{"schema_version": "{GOAL_PATH_DELTA_SCHEMA_VERSION}", ...}}}}.'
)
elif inline_vision_packet:
agent_vision_packet = inline_vision_packet
merge_agent_vision_patch = True
Expand Down
44 changes: 44 additions & 0 deletions loopx/control_plane/goals/goal_vision.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

GOAL_VISION_REPLAN_SCHEMA_VERSION = "goal_vision_replan_contract_v0"
GOAL_PATH_DELTA_SCHEMA_VERSION = "goal_path_delta_v0"
# The goal-vision packet field that carries the `goal_path_delta_v0` object.
GOAL_VISION_PATH_DELTA_FIELD = "path_delta"


GOAL_VISION_FIELD_LIMITS: dict[str, int] = {
Expand Down Expand Up @@ -114,6 +116,48 @@ def _compact_fallback_declarations(value: Any) -> list[dict[str, str]]:
return declarations


def misplaced_goal_path_delta_field(value: Any) -> tuple[str, str] | None:
"""Return the foreign key holding a `goal_path_delta_v0` object, if any.

The path delta is an optional *nested* object of the goal-vision packet, and
the read path only looks under ``path_delta``. A caller that nests it under
the delta's own schema name loses it silently, and the autonomous replan
writeback then rejects the turn with a message that never names the field.
The write path calls this before accepting a packet so that mismatch fails
with the accepted key instead of a generic "no accepted surface" refusal.

A foreign key counts as a misfiled delta when its value is either labelled
with the delta's schema version or carries at least two of the delta's own
field names. Requiring two fields keeps an unrelated object that happens to
have one shared key from being reported as a path delta.
"""

if not isinstance(value, dict):
return None
if isinstance(value.get(GOAL_VISION_PATH_DELTA_FIELD), dict):
return None
for field, candidate in value.items():
if field == GOAL_VISION_PATH_DELTA_FIELD:
continue
if not isinstance(candidate, dict):
continue
if _looks_like_goal_path_delta(candidate):
return str(field), GOAL_PATH_DELTA_SCHEMA_VERSION
return None


def _looks_like_goal_path_delta(candidate: dict[str, Any]) -> bool:
if str(candidate.get("schema_version") or "") == GOAL_PATH_DELTA_SCHEMA_VERSION:
return True
# `outcome` lives beside the two limit maps rather than inside them.
delta_fields = (
{"outcome"}
| set(GOAL_PATH_DELTA_SCALAR_LIMITS)
| set(GOAL_PATH_DELTA_LIST_LIMITS)
)
return len(delta_fields.intersection(candidate)) >= 2


def compact_goal_vision_packet(value: Any) -> dict[str, Any] | None:
"""Return the public read-path shape of an agent goal-vision packet."""

Expand Down