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
7 changes: 7 additions & 0 deletions docs/product/periodic-reports.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,13 @@ in its original managed Session. This backend/configuration slice is described
below; arbitrary new tasks from natural language and the unified Agent UI
remain separate, unfinished acceptance.

Calendar changes use the existing Goal configuration transaction. If a pause,
schedule change or clear was written but its readback is unavailable or differs,
the response retains the partial-write receipt instead of claiming that the old
configuration was preserved. Reload the source configuration before deciding on
another change. This does not rerun the task, confirm an unfinished result or
retire a failed window; those remain with their original lifecycle owners.

The steward becomes a persistent LoopX-managed Agent. Its original private
conversation remains the user's entry point. Native monitor Todos, Turn
admission, scheduling and periodic-report publication own recurring work.
Expand Down
10 changes: 5 additions & 5 deletions loopx/chat_goal_configuration_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -626,20 +626,20 @@ def _send_applied_goal_configuration(
capability_id, readback_configuration, desired_configuration, applied
)
except Exception:
if capability_id != "reward_memory" or not applied.get("written"):
if not applied.get("written"):
raise
self._send_partial_write_receipt(
applied={**applied, "error": "Memory configuration was written, but current readback is unavailable.",
applied={**applied, "error": "Configuration was written, but current readback is unavailable.",
"recommended_action": "Reload the current Goal configuration before previewing another change."},
capability_id=capability_id, desired_configuration=desired_configuration,
desired_public=desired_public, goal_id=goal_id, plan=plan,
)
return
if not readback_verified:
if capability_id == "reward_memory" and applied.get("written"):
if applied.get("written"):
self._send_partial_write_receipt(
applied={**applied, "error": "Configuration was written, but the memory binding readback did not verify.",
"recommended_action": "Reload the current Goal configuration and inspect the binding before previewing another change."},
applied={**applied, "error": "Configuration was written, but current readback did not verify.",
"recommended_action": "Reload the current Goal configuration before previewing another change."},
capability_id=capability_id, desired_configuration=desired_configuration,
desired_public=desired_public, goal_id=goal_id, plan=plan,
)
Expand Down
59 changes: 59 additions & 0 deletions tests/test_managed_session_calendar_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,65 @@ def test_real_settings_only_select_existing_completed_source_and_current_preview
assert not SessionCalendarTasks(runtime).health(session["session_id"])["configured"]


@pytest.mark.parametrize("provider", ["file", "sqlite"])
@pytest.mark.parametrize("change", ["pause", "reschedule", "clear"])
@pytest.mark.parametrize("readback_fault", ["unavailable", "mismatch"])
def test_calendar_settings_report_persisted_changes_when_readback_is_unverified(
steward, tmp_path, monkeypatch, provider, change, readback_fault):
from loopx.chat_server import ChatRequestHandler
from loopx.capabilities.periodic_report.goal_configuration import configuration_summary

session, actor, _, capture, _, config = setup_task(steward, tmp_path, monkeypatch, provider)
store, runtime, bot, *_ = steward
sid, goal_id = session["session_id"], actor["goal_id"]
target = ({**config, "enabled": False} if change == "pause" else
{**config, "schedule": {**config["schedule"], "rrule": "FREQ=DAILY;BYHOUR=10;BYMINUTE=0"}}
if change == "reschedule" else None)
calls, writes, messages = capture.read_bytes(), list(bot.writes), store.messages(sid)
with settings_server(steward) as request:
apply_configuration(request, goal_id, config)
body = {"goal_id": goal_id, "capability_id": "periodic_report", "configuration": target}
status, preview = request("/api/chat/goal-configuration/preview", body)
assert status == 201, preview
original_read = ChatRequestHandler._read_public_goal_configuration

def unavailable_after_source_write(handler, requested_goal, *args, **kwargs):
current = configuration_summary(load_goal_from_registry(runtime.registry_path, goal_id))
if requested_goal == goal_id and current == (normalize_configuration(target) if target else None):
if readback_fault == "unavailable":
raise OSError("synthetic private readback diagnostic must stay server-side")
value = original_read(handler, requested_goal, *args, **kwargs)
entry = next(item for item in value["capability_catalog"]["capabilities"]
if item["capability_id"] == "periodic_report")
entry["current"] = {"enabled": True}
return value
return original_read(handler, requested_goal, *args, **kwargs)

with monkeypatch.context() as fault:
fault.setattr(ChatRequestHandler, "_read_public_goal_configuration", unavailable_after_source_write)
status, receipt = request("/api/chat/goal-configuration/apply",
{**body, "expected_plan_revision": preview["plan_revision"]})
# Read the actual source independently of the failed response path.
assert configuration_summary(load_goal_from_registry(runtime.registry_path, goal_id)) == (
normalize_configuration(target) if target else None)
assert status == 207, receipt
assert receipt["status"] == "partial_write" and receipt["source_written"] is True
assert receipt["readback_verified"] is False and receipt["applied_revision"] is None
assert receipt["goal_configuration"] == (None if readback_fault == "unavailable" else {"enabled": True})
assert receipt["capability_id"] == "periodic_report" and receipt["goal_id"] == goal_id
assert "prior state was preserved" not in receipt["error"]
assert "synthetic private" not in json.dumps(receipt)
assert "Reload" in receipt["recommended_action"]
# A normal read/re-preview settles the uncertainty without replaying a
# write, a model turn, a scheduled period or an external delivery.
status, loaded = request("/api/chat/goal-configuration?goal_id=" + goal_id, None, method="GET")
assert status == 200, loaded
status, refreshed = request("/api/chat/goal-configuration/preview", body)
assert status == 201 and refreshed["writes_required"] == 0, refreshed
assert capture.read_bytes() == calls and bot.writes == writes and store.messages(sid) == messages
assert read_cadence_journal(runtime_root=runtime.coordination_runtime_root, goal_id=goal_id) is None


@pytest.mark.parametrize("fault", ["queued", "busy", "paused", "unregistered", "stopped"])
def test_original_user_work_and_withdrawn_authority_win_without_model(steward, tmp_path, monkeypatch, fault):
session, actor, _, capture, _, config = setup_task(steward, tmp_path, monkeypatch, "sqlite")
Expand Down
Loading