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
3 changes: 2 additions & 1 deletion loopx/capabilities/agent_turn_recall/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,8 @@ def handle_agent_turn_recall_command(
payload: dict[str, Any] = {
"ok": True,
"schema_version": AGENT_TURN_RECALL_SCHEMA_VERSION,
"status": "disabled",
"status": str(experiment_status.get("status") or "not_available"),
"reason_code": experiment_status.get("reason_code"),
"goal_id": args.goal_id,
"agent_id": args.agent_id,
"experiment": experiment_status,
Expand Down
26 changes: 26 additions & 0 deletions loopx/capabilities/reward_memory/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,32 @@ OpenViking is the first provider used by the Issue Fix pilot, but it is not a
global LoopX feature flag or mandatory dependency; another provider can
satisfy the same binding contract.

### Recovering a changed binding

Editing the ignored config invalidates its old enablement receipt. `enablement_stale`
and `enablement_unverified` now return a shared `repair` plan: reuse the invoked
registry and existing Agent allowlist with `configure-goal`, inspect the config
change, preview, apply within existing authorization, then verify `available`.
Commands are explicitly marked templates with a required `<invoked-registry>`
binding. Bind it to the exact invocation registry before execution; an omitted
registry must never silently select a default. The private registry path, config
pointer and provider scopes are not published. The config pointer is retained. Apply re-runs provider write/exact-readback and
synchronizes the source/global binding. Never repair drift by copying a digest
into an old receipt. Disabled capabilities offer no re-enable plan.

The configuration catalog checks the live config digest and reuses runtime
admission validation rather than presenting a cached receipt as current verification. It separates `desired_automation` and
`recorded_verified_agents` from `binding_status`, `effective_available` and
effective automation. The existing settings summary renders these same fields.

The same plan is carried through Turn recall, quota and agent status/Markdown;
the explicit recall CLI preserves the actual failure instead of labelling every
unavailable configuration `disabled`. The existing capability editor can preview
and reapply the retained pointer/Agent list through the same owner. No automatic
configuration acceptance or new provider permissions are introduced. Recovery of
enablement must still be followed by a real qualified experience write, exact
readback and destination recall before claiming useful memory is available.

### OpenViking v0.4.19 identity boundary

LoopX currently assumes one Agent belongs to exactly one Goal, while a Goal may
Expand Down
20 changes: 20 additions & 0 deletions loopx/capabilities/reward_memory/README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -612,3 +612,23 @@ publish、production 或跨项目 authority。
可选的[外发指导召回](OUTBOUND.zh-CN.md)会在真正绑定 Goal/Agent 的 Lark
inbox send/reply 边界召回已经审阅过的偏好。它把指导交给 Agent 审视,但不会
授予发送权限。

## 配置变更后的恢复

直接修改忽略路径中的配置会使旧启用回执失效。`enablement_stale` 和
`enablement_unverified` 现在返回共享 `repair` 计划:沿用本次 registry 和完整
Agent 名单,通过 `configure-goal` 检查变更、预览、在既有授权内应用,再读回
`available`。命令明确标为模板,执行前必须将 `<invoked-registry>` 绑定为本次调用的
确切 registry,不能省略后悄悄采用默认值。模板不公开 registry 路径、配置指针或
provider scope,配置指针沿用原值;应用时重新
执行 provider 写入及精确读回,并同步源/全局绑定。不能把新摘要填进旧回执;
明确停用时不生成重新启用建议。

Turn recall、quota、Agent status/Markdown 传递同一计划;显式召回 CLI 保留
真实故障类型,不再把所有不可用情况标成 disabled。现有 capability editor
可保留原指针/Agent 名单执行同一预览和应用流程,无须新增配置权威。恢复计划
不自动接受配置变化、不增加 provider 权限。恢复后仍须用合格经验验证真实写入、
精确读回和业务召回,才能宣称该经验可用。

配置目录现在核对当前文件摘要并复用运行时准入校验,将期望自动化和历史验证回执与当前绑定状态、
可用性和有效自动化分开;现有设置页直接显示这些共享字段,避免缓存回执误报正常。
77 changes: 75 additions & 2 deletions loopx/capabilities/reward_memory/configuration.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,91 @@
from __future__ import annotations

import hashlib
from collections.abc import Mapping, Sequence
from copy import deepcopy
from pathlib import Path
from typing import Any

from ...control_plane.operator_inbox_binding import local_private_config_digest
from ...control_plane.reward_memory import reward_memory_goal_policy
from ...control_plane.operator_inbox_binding import (
local_private_config_digest,
operator_inbox_binding,
)
from ...control_plane.reward_memory import (
reward_memory_goal_policy,
reward_memory_host_coverage,
)
from .experiment import (
load_reward_memory_experiment_config,
preflight_reward_memory_experiment_config,
resolve_goal_reward_memory_experiment,
)


def reward_memory_goal_configuration_summary(
goal: Mapping[str, Any],
) -> dict[str, Any]:
"""Reconcile the declared policy with live, read-only runtime admission.

The configuration catalog and the settings summary need one projection that
separates the desired automation and historical receipts from the effective
binding. Effective availability reuses runtime admission (same digest,
isolation and receipt checks) so a drifted or unverified config cannot be
reported as currently verified. No provider is contacted and nothing is
written.
"""

policy = reward_memory_goal_policy(goal)
binding_revision = ""
if policy["config_path"] and policy["config_digest"]:
binding_revision = "sha256:" + hashlib.sha256(
(
f"{policy['config_path']}\0{policy['config_digest']}\0"
+ "\0".join(policy["enabled_agents"])
).encode("utf-8")
).hexdigest()
binding = operator_inbox_binding(
project=str(goal.get("repo") or ""),
config_path=policy["config_path"],
expected_digest=policy["config_digest"],
)
recorded_verified_agents = sorted(
agent_id
for agent_id, receipt in policy["enablement_receipts"].items()
if receipt.get("status") == "verified"
and receipt.get("writability_verified") is True
and receipt.get("exact_readback_verified") is True
)
effective_verified_agents: list[str] = []
for agent_id in recorded_verified_agents:
try:
status, _ = resolve_goal_reward_memory_experiment(
goal=goal, agent_id=agent_id
)
except ValueError:
continue
if status.get("available") is True:
effective_verified_agents.append(agent_id)
effective_available = bool(effective_verified_agents)
return {
"enabled": policy["enabled"],
"binding_status": binding["status"],
"effective_available": effective_available,
"desired_automation": dict(policy["automation"]),
"recorded_verified_agents": recorded_verified_agents,
"experimental": policy["experimental"],
"config_pointer_registered": bool(policy["config_path"]),
"binding_revision": binding_revision,
"automatic_ingest": effective_available
and policy["automation"].get("automatic_ingest") is True,
"automatic_recall": effective_available
and policy["automation"].get("automatic_recall") is True,
"automation_intent": dict(policy["automation_intent"]),
"host_coverage": reward_memory_host_coverage(),
"enabled_agents": list(policy["enabled_agents"]),
"enablement_verified_agents": effective_verified_agents,
}


def plan_reward_memory_goal_configuration(
*,
goal: Mapping[str, Any],
Expand Down
80 changes: 79 additions & 1 deletion loopx/capabilities/reward_memory/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import hashlib
import json
import re
import shlex
import uuid
from collections.abc import Mapping, Sequence
from pathlib import Path
Expand Down Expand Up @@ -787,6 +788,55 @@ def resolve_reward_memory_surface_config(
}


def _enablement_repair(
*, goal_id: str, agent_ids: Sequence[str], agent_id: str
) -> dict[str, Any]:
"""Reuse the configuration owner without publishing its private pointer.

This is a recovery plan, never authorization to trust a changed binding.
Retain every enabled Agent when requalifying a shared corpus.
"""
argv = [
"loopx", "--registry", "<invoked-registry>",
"configure-goal", "--goal-id", goal_id,
]
for enabled_agent in agent_ids:
argv.extend(["--reward-memory-agent", enabled_agent])
return {
"schema_version": "reward_memory_enablement_repair_v0",
"kind": "requalify_existing_binding",
"owner": "configure-goal",
"registry_context": "reuse_invoked_registry",
"commands_are_templates": True,
"required_bindings": {"<invoked-registry>": "invoked_registry_path"},
"automatic_apply": False,
"instruction": (
"Bind <invoked-registry> to the exact registry used for this invocation "
"before executing any command; never substitute the default registry. "
"Inspect the local configuration change and its existing authorization; "
"preview, then apply through configure-goal within that authorization. "
"Apply performs provider write/readback and synchronizes the binding. "
"Do not copy the new digest into an old receipt. Keep ordinary work running."
),
"preview_command": shlex.join(argv),
"apply_command": shlex.join([*argv, "--execute"]),
"verify_command": shlex.join(
[
"loopx",
"--registry",
"<invoked-registry>",
"reward-memory",
"experiment-status",
"--goal-id",
goal_id,
"--agent-id",
agent_id,
]
),
"success_status": "available",
}


def resolve_reward_memory_experiment(
*, registry_path: Path, goal_id: str, agent_id: str
) -> tuple[dict[str, Any], dict[str, Any] | None]:
Expand All @@ -807,6 +857,24 @@ def resolve_reward_memory_experiment(
)
if goal is None:
raise ValueError(f"goal_id not found in registry: {goal_id}")
return resolve_goal_reward_memory_experiment(
goal=goal,
agent_id=normalized_agent,
registry_role=str(registry.get("registry_role") or "project-local"),
)


def resolve_goal_reward_memory_experiment(
*, goal: Mapping[str, Any], agent_id: str, registry_role: str = "project-local"
) -> tuple[dict[str, Any], dict[str, Any] | None]:
"""Resolve an already loaded Goal with the same checks as runtime admission.

Configuration summaries reuse this read-only owner; no provider is contacted.
"""
goal_id = str(goal.get("id") or "")
normalized_agent = normalize_todo_claimed_by(agent_id)
if not normalized_agent:
raise ValueError("agent_id must be a public-safe registered agent id")
registered_agents = normalize_registered_agents(
(goal.get("coordination") or {}).get("registered_agents")
if isinstance(goal.get("coordination"), Mapping)
Expand All @@ -815,7 +883,7 @@ def resolve_reward_memory_experiment(
if normalized_agent not in registered_agents:
raise ValueError(f"agent_id is not registered for goal {goal_id}")
policy = reward_memory_goal_policy(goal)
registry_role = str(registry.get("registry_role") or "project-local").strip()
registry_role = str(registry_role or "project-local").strip()
config_runtime_route = {
"schema_version": "reward_memory_config_runtime_route_v0",
"registry_source": "invoked_registry",
Expand Down Expand Up @@ -892,6 +960,11 @@ def resolve_reward_memory_experiment(
"status": "enablement_stale",
"available": False,
"reason_code": "config_digest_missing_or_drifted",
"repair": _enablement_repair(
goal_id=goal_id,
agent_ids=policy["enabled_agents"],
agent_id=normalized_agent,
),
"isolation_mode": isolation["isolation_mode"],
"actor_binding_verified": isolation["actor_binding_verified"],
"writability_verified": False,
Expand All @@ -918,6 +991,11 @@ def resolve_reward_memory_experiment(
"status": "enablement_unverified",
"available": False,
"reason_code": "provider_write_preflight_missing_or_unverified",
"repair": _enablement_repair(
goal_id=goal_id,
agent_ids=policy["enabled_agents"],
agent_id=normalized_agent,
),
"isolation_mode": isolation["isolation_mode"],
"actor_binding_verified": isolation["actor_binding_verified"],
"writability_verified": False,
Expand Down
2 changes: 2 additions & 0 deletions loopx/cli_commands/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,8 @@ def _agent_reward_memory_projection(
"configured_for_agent",
"experiment_status",
"experiment_available",
"reason_code",
"repair",
"config_schema_version",
"automatic_ingest",
"automatic_recall",
Expand Down
9 changes: 9 additions & 0 deletions loopx/configuration_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,15 @@ def build_goal_configuration_catalog(
"availability": "experimental_opt_in",
"default": {"enabled": False},
"current": {
"binding_status": reward_memory.get("binding_status"),
"effective_available": reward_memory.get("effective_available")
is True,
"desired_automation": dict(
reward_memory.get("desired_automation") or {}
),
"recorded_verified_agents": list(
reward_memory.get("recorded_verified_agents") or []
),
"enabled": reward_memory.get("enabled") is True,
"experimental": reward_memory.get("experimental") is True,
"config_pointer_registered": reward_memory.get(
Expand Down
8 changes: 3 additions & 5 deletions loopx/configure_goal.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from .capabilities.reward_memory.configuration import (
apply_reward_memory_goal_configuration,
plan_reward_memory_goal_configuration,
reward_memory_goal_configuration_summary,
reward_memory_preflight_markdown_lines,
)
from .configuration_catalog import (
Expand All @@ -49,9 +50,6 @@
from .control_plane.coordination import runtime_shadow as shadow
from .control_plane.coordination.configuration import normalize_goal_write_scope
from .control_plane.operator_inbox_binding import local_private_config_digest
from .control_plane.reward_memory import (
reward_memory_goal_policy_summary,
)
from .control_plane.todos.contract import normalize_todo_claimed_by
from .control_plane.todos.mutation_authority import (
normalize_todo_lifecycle_authority,
Expand Down Expand Up @@ -254,7 +252,7 @@ def _settings_summary(goal: dict[str, Any]) -> dict[str, Any]:
"issue_fix_reviewer_notification": _reviewer_notification_config_summary(goal),
"lark_event_inbox": _lark_event_inbox_config_summary(goal),
"lark_kanban_heartbeat_sync": _lark_kanban_heartbeat_config_summary(goal),
"reward_memory": reward_memory_goal_policy_summary(goal),
"reward_memory": reward_memory_goal_configuration_summary(goal),
"pull_request_review": pr_review_config.configuration_summary(goal),
"change_quality_qualification": change_quality_goal_policy_summary(goal),
"explore_graph": compact_explore_graph_policy(goal.get("explore_graph")),
Expand Down Expand Up @@ -1257,7 +1255,7 @@ def configure_goal(
"coordination_runtime_shadow": deepcopy(after["coordination_runtime_shadow"]),
"lark_event_inbox": _lark_event_inbox_config_summary(goal),
"lark_kanban_heartbeat_sync": _lark_kanban_heartbeat_config_summary(goal),
"reward_memory": reward_memory_goal_policy_summary(goal),
"reward_memory": reward_memory_goal_configuration_summary(goal),
"pull_request_review": pr_review_config.configuration_summary(goal),
"change_quality_qualification": change_quality_goal_policy_summary(goal),
"default": "off",
Expand Down
2 changes: 2 additions & 0 deletions loopx/control_plane/quota/goal_boundary.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,8 @@ def _reward_memory_enablement_projection(
status: Mapping[str, Any],
) -> dict[str, Any]:
fields = (
"reason_code",
"repair",
"isolation_mode",
"enablement_receipt_status",
"actor_binding_verified",
Expand Down
32 changes: 0 additions & 32 deletions loopx/control_plane/reward_memory.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from __future__ import annotations

import hashlib
from collections.abc import Mapping
from typing import Any

Expand Down Expand Up @@ -107,38 +106,7 @@ def reward_memory_goal_policy(goal: Mapping[str, Any]) -> dict[str, Any]:
}


def reward_memory_goal_policy_summary(goal: Mapping[str, Any]) -> dict[str, Any]:
policy = reward_memory_goal_policy(goal)
binding_revision = ""
if policy["config_path"] and policy["config_digest"]:
binding_revision = "sha256:" + hashlib.sha256(
(
f"{policy['config_path']}\0{policy['config_digest']}\0"
+ "\0".join(policy["enabled_agents"])
).encode("utf-8")
).hexdigest()
return {
"enabled": policy["enabled"],
"experimental": policy["experimental"],
"config_pointer_registered": bool(policy["config_path"]),
"binding_revision": binding_revision,
"automatic_ingest": policy["automation"].get("automatic_ingest"),
"automatic_recall": policy["automation"].get("automatic_recall"),
"automation_intent": dict(policy["automation_intent"]),
"host_coverage": reward_memory_host_coverage(),
"enabled_agents": list(policy["enabled_agents"]),
"enablement_verified_agents": sorted(
agent_id
for agent_id, receipt in policy["enablement_receipts"].items()
if receipt.get("status") == "verified"
and receipt.get("writability_verified") is True
and receipt.get("exact_readback_verified") is True
),
}


__all__ = [
"reward_memory_goal_policy",
"reward_memory_goal_policy_summary",
"reward_memory_host_coverage",
]
Loading
Loading