From 1fe1582cf8ad136b0df0f0c76d1e86cfc3232849 Mon Sep 17 00:00:00 2001
From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
Date: Sat, 24 Oct 2026 23:16:03 +0800
Subject: [PATCH 1/2] feat(report): deliver calendar reports through original
managed Agent sessions
Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
---
.../goal-capability-settings.tsx | 6 +
.../periodic-report-session-field.tsx | 97 +++++++
docs/product/periodic-reports.md | 79 +++++-
.../steward_report_fixture.py | 37 ++-
.../native_chat/steward_managed_mode.py | 35 ++-
.../native_chat/steward_monitor.py | 12 +
.../native_chat/steward_reports.py | 259 +++++++++++++++++-
.../capabilities/native_chat/steward_turn.py | 56 ++--
.../periodic_report/cadence_runtime.py | 6 +
.../periodic_report/goal_configuration.py | 17 ++
.../periodic_report/pending_intent.py | 6 +-
.../periodic_report/private_generation.py | 67 ++++-
loopx/chat_goal_configuration_api.py | 96 ++++---
.../collaboration/steward_managed_mode.ts | 100 ++++---
.../control_plane_ts/steward_reports.test.ts | 49 ++++
tests/test_native_steward_reports.py | 233 +++++++++++++++-
16 files changed, 1017 insertions(+), 138 deletions(-)
create mode 100644 apps/presentation/dashboard/src/features/personal-workspace/periodic-report-session-field.tsx
diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-capability-settings.tsx b/apps/presentation/dashboard/src/features/personal-workspace/goal-capability-settings.tsx
index e789ed0b68..883185b71e 100644
--- a/apps/presentation/dashboard/src/features/personal-workspace/goal-capability-settings.tsx
+++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-capability-settings.tsx
@@ -15,6 +15,7 @@ import { goalReplanCadenceEditorValue, goalReplanCadenceWriteValue, hasReplanCad
import { useWorkspaceI18n } from "./i18n";
import { CapabilityConfigurationFields } from "./capability-configuration-fields";
import { withReportScheduleTimezone } from "./periodic-report-schedule-field";
+import {PeriodicReportSessionField} from "./periodic-report-session-field";
import { localizeCapability, localizedCapabilityFieldCopy } from "./capability-localization";
import { orderCapabilitiesForPresentation, canEditCapability, CapabilityCatalogNavigation, CapabilityConfigurationSummary, CapabilityDetailHeader, CapabilityEditorStatus, CapabilityConfigurationHelp } from "./capability-workbench";
import { GoalAutoNotifyToggle } from "./notification-settings-panel";
@@ -426,6 +427,10 @@ function CapabilityCatalog({ callbacks, catalog, goalId, loading, notification,
? `复盘周期沿用${selected.machine_current ? "设备" : "产品"}默认;规划选项由本 Goal 设置。`
: `Review interval follows ${selected.machine_current ? "device" : "product"} defaults; this Goal sets the planning options.`}
: null}
+ {selected.capability_id === "periodic_report" && draft.profile_preset === "weekly-progress"
+ ? changeDraft("route_ref", value)} /> : null}
(url: string, signal: AbortSignal): Promise {
+ const response = await fetch(url, {signal});
+ const value = await response.json();
+ if (!response.ok || value.ok !== true) throw new Error("Original report conversation unavailable");
+ return value as T;
+}
+
+/** Select an existing original Session; never create, resume or retarget one. */
+export function PeriodicReportSessionField({goalId, route, zh, disabled, revision, onChange}: {
+ goalId: string; route: string; zh: boolean; disabled: boolean; revision: string;
+ onChange: (route: string) => void;
+}) {
+ const id = useId();
+ const [sessions, setSessions] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [failed, setFailed] = useState(false);
+ const [retry, setRetry] = useState(0);
+ const [report, setReport] = useState(null);
+ const [reportFailed, setReportFailed] = useState(false);
+ const local = route.startsWith("managed-session:");
+ const selectedId = local ? route.slice("managed-session:".length) : "";
+
+ useEffect(() => {
+ const controller = new AbortController();
+ setSessions([]); setLoading(true); setFailed(false);
+ void read<{sessions: Session[]}>(`/api/chat/sessions?${new URLSearchParams({goal_id: goalId})}`, controller.signal)
+ .then(value => {
+ if (controller.signal.aborted) return;
+ if (!Array.isArray(value.sessions) || value.sessions.some(session => session.goal_id !== goalId)) throw new Error("Source changed");
+ setSessions(value.sessions.filter(session => session.managed_agent_id && session.session_mode === "managed_runtime"
+ && session.agent_id === "codex" && session.status !== "closed"));
+ }).catch(() => {if (!controller.signal.aborted) setFailed(true);})
+ .finally(() => {if (!controller.signal.aborted) setLoading(false);});
+ return () => controller.abort();
+ }, [goalId, retry]);
+
+ useEffect(() => {
+ const controller = new AbortController();
+ setReport(null); setReportFailed(false);
+ if (selectedId && !disabled) {
+ void read<{report?: Report}>(`/api/chat/sessions/${encodeURIComponent(selectedId)}/native-turn`, controller.signal)
+ .then(value => {if (!controller.signal.aborted) {
+ if (!value.report) throw new Error("Report readback unavailable");
+ setReport(value.report);
+ }}).catch(() => {if (!controller.signal.aborted) setReportFailed(true);});
+ }
+ return () => controller.abort();
+ }, [selectedId, goalId, revision, disabled, retry]);
+
+ return
+
+ {!local && route ?
: null}
+ {failed ?
{zh ? "无法读取原会话,未更换接收位置。" : "Original conversations could not be read; the destination is retained."}
+ : !loading && !sessions.length ?
{zh ? "此 Goal 尚无已绑定的 Agent 会话。" : "This Goal has no bound Agent conversation."} : null}
+ {failed || reportFailed ?
: null}
+ {reportFailed ?
{zh ? "报告执行与送达状态暂时未知。" : "Report execution and delivery are currently unknown."} : null}
+ {report ?
+ {!report.available ? (zh ? "原会话不可用,定时报告不能执行。" : "The original conversation is unavailable; scheduled reports cannot run.")
+ : !report.configured ? (zh ? "保存后在所选原会话接收报告。" : "Save to receive reports in the selected original conversation.")
+ : !report.enabled ? (zh ? "定时报告已暂停。" : "Scheduled reports are paused.")
+ : report.scheduled === false ? (zh ? "已启用,请设置日历汇报时间。" : "Enabled; set a calendar report time.")
+ : report.publication_verified ? (zh ? "本期报告已在原会话完整读回。" : "This period’s full report was read back in the original conversation.")
+ : report.execution_failed ? (zh ? "本期执行失败;原结果与失败记录已保留。" : "This period’s execution failed; the original result and failure are retained.")
+ : report.generation_complete ? (zh ? "本期已生成,完整送达尚未确认。" : "This period was generated; complete delivery is unverified.")
+ : (zh ? "定时报告已启用。" : "Scheduled reports are enabled.")}
+ {report.enabled && report.next_due_at && (!report.retained_window_due_at || report.publication_verified)
+ ? ` ${zh ? "下次:" : "Next: "}${new Date(report.next_due_at).toLocaleString(zh ? "zh-CN" : "en-US")}` : ""}
+ : null}
+ {report?.publication_verified && report.report_message_id ?
{zh ? "查看本期完整报告" : "Read this period’s full report"} : null}
+
;
+}
diff --git a/docs/product/periodic-reports.md b/docs/product/periodic-reports.md
index c0a99d57d2..a1352b5daa 100644
--- a/docs/product/periodic-reports.md
+++ b/docs/product/periodic-reports.md
@@ -1,7 +1,8 @@
# Managed steward monitoring and periodic reports
-Status: source-bound identity preparation, opt-in status monitoring and the
-original steward's periodic full-body report path are implemented. Installation,
+Status: source-bound identity preparation, opt-in status monitoring, the
+original steward's periodic full-body reports and explicitly configured local
+Agent Session calendar reports are implemented. Installation,
an enabled subscription and a natural scheduled delivery require their own
readback evidence. Preparing an identity enables neither monitoring nor reports.
@@ -466,12 +467,12 @@ Registration removal, Goal stop, a changed lifetime or a mismatched workspace
refuses continued execution rather than transferring the conversation.
This is an API/host lifecycle slice, **not** the finished Agent workspace
-entry or general periodic journey. The Agent view still needs to expose this
+entry or arbitrary periodic-task journey. The Agent view still needs to expose this
binding alongside attached sources, including all original history and honest
unavailable states. Reuse existing Session, native Turn,
monitor/calendar and periodic-report owners; do not add a second scheduler,
task store or model worker. General writable execution, natural-language
-configuration and recurring task composition remain unfinished.
+configuration and arbitrary recurring task composition remain unfinished.
An explicitly bound local managed Session can also observe its Goal's native
work through an existing `continuous_monitor` Todo. Use the existing Todo
@@ -516,6 +517,76 @@ establish the correct Agent hierarchy, real-model adoption or natural-deadline
acceptance. The UI withdrawal does not alter the original private steward's
subscription, published reports, pending requests or publication route.
+### Calendar reports in an existing Agent Session
+
+In **Goal settings → Capability center → Periodic reports**, use the
+`weekly-progress` profile and choose an existing **Report conversation**.
+The selector lists only this Goal's already bound managed Agent Sessions. It
+does not create an Agent or assign a legacy Goal-wide conversation. Enable
+reports, set a daily or weekly calendar and optional first date, then use the
+existing preview/apply controls. Reload reads back the saved destination,
+calendar and original conversation's execution/delivery status. No calendar
+means no automatic local Session report; the status asks for a report time.
+After verified delivery, **Read this period's full report** opens the existing
+original-message report viewer, including its full body and download controls.
+The link is available only while that exact returned body is readable.
+
+This is an explicit Goal override. Machine defaults, registration, opening a
+Session and installation do not enroll a conversation. The reserved route
+`managed-session:` is persisted in the existing
+periodic-report configuration; it is a destination reference, not an execution
+or permission grant. Other profiles and configured Goal Channel routes retain
+their existing path. Local Session reports do not enter the generic CLI
+report-consumption hook or gain a second Goal Channel delivery.
+
+The existing idle host wake admits the calendar window into the selected
+Session's original Core worker. Native periodic-report owns the finite
+generation Todo, fixed window, frozen source/editorial artifacts, validator,
+publication candidate and cursor. Native Turn owns the original executor,
+lease, validation, completion and quota. Busy or queued user work has priority;
+another registered Agent cannot claim this route. Paused/stopped Goals,
+removed registration and replaced Session/Goal identities refuse execution.
+
+The local source reads the complete original Goal's current native tasks and
+completed Agent history without a display limit, retaining full rows and
+observed times in the frozen source. The report distinguishes this fixed
+window's recorded completions from older history and current work. It does not
+reconstruct task state as of a past deadline, independently certify linked
+artifacts or read another Goal's portfolio. Source overflow or an unavailable
+complete read fails visibly rather than silently dropping trailing tasks.
+
+After native generation commits, the adapter writes one complete report to
+that original Core conversation and independently reads back its exact body,
+message and Turn identity. Only then does the existing publication cursor
+advance. The normal Core finalizer uses the same message identity. A lost
+append/cursor response reconciles that original body after restart without a
+second model call or duplicate publication. Cached validated generation can
+resume only the native journal's permitted remaining effects; an earlier
+partial/failed answer remains historical evidence.
+
+Use **Enable** to pause the same configuration; the original task, frozen
+window, failure and message history remain. Re-enable that exact plan to
+continue eligible native recovery. An unpublished frozen window prevents a
+schedule/destination change from stranding it. Failed/unknown generation is
+retained for repair by its original execution owner; toggling the setting does
+not replay it. This slice does not add cancellation of an invalid authored
+window, natural-language schedule authoring or arbitrary task profiles. Those
+remain part of the existing general recurring-task work.
+
+The packaged journey uses disposable Core/SQLite state and synthetic model
+and provider transports. It checks future-date inactivity, preview/apply/reload,
+one automatic full-body return, retained failure and pause. The 390×844 view is
+viewport emulation; this evidence does not establish real-model report quality,
+production backlog completion or a natural production deadline.
+
+| Before: raw route | After: original Agent conversation and verified return |
+| --- | --- |
+|  |  |
+
+| Retained execution failure, narrow | Paused configuration |
+| --- | --- |
+|  |  |
+
### Original private steward
The original-Session host bridge now runs a bounded native Turn through the
diff --git a/examples/personal-workspace-browser/steward_report_fixture.py b/examples/personal-workspace-browser/steward_report_fixture.py
index c07637d2fe..9d1d1d6054 100644
--- a/examples/personal-workspace-browser/steward_report_fixture.py
+++ b/examples/personal-workspace-browser/steward_report_fixture.py
@@ -23,7 +23,10 @@ def main() -> None:
parser.add_argument("--baseline-assets-dir", type=Path)
parser.add_argument("--invalid-editorial", action="store_true", help="Return a known invalid synthetic report to inspect owner cancellation and retained failure.")
parser.add_argument("--managed-session", action="store_true", help="Exercise ordinary Goal Session native work with the existing task validator, instead of a scheduled report.")
+ parser.add_argument("--managed-report", action="store_true", help="Configure a calendar report for an existing local Agent Session through the packaged Goal settings.")
args = parser.parse_args()
+ if args.managed_session and args.managed_report:
+ parser.error("select one managed Session fixture journey")
repo = Path(__file__).resolve().parents[2]
sys.path[:0] = [str(repo), str(repo / "tests")]
from test_native_steward_private import steward
@@ -44,7 +47,20 @@ def main() -> None:
data = next(fixture)
store, runtime, provider, transport, binding, capture, workspace = data
managed_session = None
- if args.managed_session:
+ if args.managed_report:
+ import pytest
+ from test_native_steward_reports import configured_local_report
+ from loopx.configure_goal import configure_goal
+ monkeypatch = pytest.MonkeyPatch()
+ managed_session, local_reports, configuration, managed_todo_id = configured_local_report(
+ data, root, monkeypatch, "sqlite", start_date="2999-01-01")
+ sid = managed_session["session_id"]
+ actor = local_reports.selected(sid)["actor"]
+ managed = transport.managed_steward
+ configure_goal(registry_path=runtime.registry_path, goal_id=actor["goal_id"],
+ periodic_report_configuration={**configuration, "enabled": False}, execute=True)
+ install_model(data, root, sid)
+ elif args.managed_session:
import pytest
from test_native_steward_turn_host import managed_fixture
monkeypatch = pytest.MonkeyPatch()
@@ -98,20 +114,23 @@ def main() -> None:
provider.owner_ready = not (output / "provider-unavailable").exists()
if wake is None and (output / "start-existing-owners").exists():
wake = DelegationWakeService(runtime, interval=.1).start()
- outbound = LarkGoalTopicRuntimeService(snapshot_provider=lambda: {},
- runtime_root=runtime.coordination_runtime_root, runtime_controller=runtime,
- private_conversations=transport)
- delivery = threading.Thread(target=outbound._reconcile_private)
- delivery.start()
+ if not args.managed_report:
+ outbound = LarkGoalTopicRuntimeService(snapshot_provider=lambda: {},
+ runtime_root=runtime.coordination_runtime_root, runtime_controller=runtime,
+ private_conversations=transport)
+ delivery = threading.Thread(target=outbound._reconcile_private)
+ delivery.start()
(output / "observed.json").write_text(json.dumps({
- "report": StewardReports(runtime).health(binding["binding_id"]),
- "same_actor": managed.health(binding["binding_id"])["session_id"] == actor["session_id"],
+ "report": local_reports.health(sid) if args.managed_report else StewardReports(runtime).health(binding["binding_id"]),
+ "same_actor": local_reports.selected(sid)["actor"] == actor if args.managed_report else managed.health(binding["binding_id"])["session_id"] == actor["session_id"],
"history_prefix_retained": store.messages(sid)[:len(before_messages)] == before_messages,
"model_call_count": sum(json.loads(line).get("method") == "turn/start"
for line in capture.read_bytes()[len(before_calls):].splitlines()),
"provider_writes": len(provider.writes),
**({"managed_source_present": artifact.exists(),
- "managed_session": store.load_session(managed_session["session_id"])} if managed_session else {}),
+ "managed_session": store.load_session(managed_session["session_id"])} if args.managed_session else {}),
+ **({"managed_report": True,
+ "managed_session": store.load_session(managed_session["session_id"])} if args.managed_report else {}),
"synthetic_only": True}, ensure_ascii=False))
finally:
if wake:
diff --git a/loopx/capabilities/native_chat/steward_managed_mode.py b/loopx/capabilities/native_chat/steward_managed_mode.py
index f60bab9b4f..83f502b86d 100644
--- a/loopx/capabilities/native_chat/steward_managed_mode.py
+++ b/loopx/capabilities/native_chat/steward_managed_mode.py
@@ -177,14 +177,16 @@ class ManagedTaskValidationUnavailable(ValueError):
"""A native selection has no usable caller-declared independent validator."""
-def managed_session_identity(controller: Any, *, goal_id: str, agent_id: str, workspace: Path) -> dict[str, Any]:
+def managed_session_identity(controller: Any, *, goal_id: str, agent_id: str, workspace: Path,
+ session: dict[str, Any] | None = None) -> dict[str, Any]:
"""Observe existing registration; the typed owner admits the exact binding."""
from .conversation_bindings import ChatConversationBindings
native_goal = load_goal_from_registry(controller.registry_path, goal_id)
return ChatConversationBindings._core("collaboration.session.managed_identity", {
"goal": StewardManagedMode(controller, None)._goal(goal_id),
"goal_active": bool(native_goal and native_goal.get("status") == "active" and not goal_is_stopped(native_goal)),
- "managed_agent_id": agent_id, "workspace_path": str(workspace.resolve())})
+ "managed_agent_id": agent_id, "workspace_path": str(workspace.resolve()),
+ **({"session": session} if session is not None else {})})
def prepare_managed_session_route(controller: Any, *, goal_id: str | None, executor: str, workspace: Path,
@@ -249,25 +251,34 @@ def _request(self, session_id: str, agent_id: str, operation_id: str,
"turn_instance_id": "managed-session." + operation_id, "todo_id": todo_id,
"work_kind": "native_turn", "validation_command": None}
- def admit(self, request: dict[str, Any], *, turn: dict[str, Any] | None = None) -> dict[str, Any]:
+ def admit(self, request: dict[str, Any], *, turn: dict[str, Any] | None = None,
+ settlement_recovery: dict[str, Any] | None = None) -> dict[str, Any]:
from .conversation_bindings import ChatConversationBindings
if self.controller.closed.is_set():
raise ValueError("the original Chat host is stopped")
actor = request["actor"]
session = self.store.load_session(actor["session_id"])
goal = load_goal_from_registry(self.controller.registry_path, actor["goal_id"])
- monitor = None
+ monitor, report = None, {}
if request.get("work_kind") == "monitor":
from ...control_plane.todos.list_readback import list_goal_todos
readback = list_goal_todos(registry_path=self.controller.registry_path,
runtime_root_arg=str(self.controller.coordination_runtime_root),
goal_id=actor["goal_id"], todo_id=request.get("todo_id"), role="agent")
monitor = readback.get("todo")
+ if request.get("work_kind") == "report":
+ from .steward_reports import ManagedSessionReports
+ from .steward_monitor import StewardMonitor
+ subscription, window = ManagedSessionReports(self.controller).current_window(actor["session_id"])
+ report = {"report_subscription": subscription, "report_window": window,
+ "report_todo": next((row for row in StewardMonitor(self.controller).todos(actor)
+ if row["todo_id"] == request.get("todo_id")), None)}
return ChatConversationBindings._core("collaboration.session.managed_turn.admit", {
"session": session, "request": request, "turn": turn,
"goal": StewardManagedMode(self.controller, None)._goal(actor["goal_id"]),
"goal_active": bool(goal and goal.get("status") == "active" and not goal_is_stopped(goal)),
- "monitor_todo": monitor,
+ "monitor_todo": monitor, **report,
+ **({"report_settlement_recovery": settlement_recovery} if settlement_recovery is not None else {}),
"queued_turn_count": len(self.store.queued_turns(actor["session_id"]))})
def inspect(self, session_id: str, *, agent_id: str | None = None,
@@ -289,14 +300,23 @@ def inspect(self, session_id: str, *, agent_id: str | None = None,
result["available"] = True
except ValueError as exc:
result["reason"] = str(exc)
+ if session.get("managed_agent_id"):
+ from .steward_reports import ManagedSessionReports
+ result["report"] = ManagedSessionReports(self.controller).health(session_id)
return result
def submit(self, session_id: str, *, agent_id: str, operation_id: str,
- todo_id: str | None = None, monitor_deadline: str | None = None) -> tuple[dict[str, Any], bool]:
+ todo_id: str | None = None, monitor_deadline: str | None = None,
+ report_window: dict[str, Any] | None = None) -> tuple[dict[str, Any], bool]:
from ...attached_session import managed_session_goal_commit
controller = self.controller
with controller._session_adapter_lock(session_id):
request = self._request(session_id, agent_id, operation_id, todo_id)
+ if report_window is not None:
+ if monitor_deadline is not None or not todo_id:
+ raise ValueError("a report requires its original finite generation Todo")
+ request.update(work_kind="report", report_window_id=report_window["window_id"],
+ report_subscription_revision=report_window["subscription_revision"])
if monitor_deadline is not None:
if not todo_id:
raise ValueError("a native watch requires its original Todo")
@@ -320,7 +340,8 @@ def submit(self, session_id: str, *, agent_id: str, operation_id: str,
admitted = self.admit(request)
accepted = self.store.accept_managed_turn(session_id, client_turn_id="managed." + operation_id,
message="Advance the currently admitted native LoopX work.",
- display_message="检查当前 Goal 原生任务状态。" if monitor_deadline is not None else "推进一次 LoopX 任务。",
+ display_message="生成本期定时报告。" if report_window is not None else
+ "检查当前 Goal 原生任务状态。" if monitor_deadline is not None else "推进一次 LoopX 任务。",
origin="native_loopx", loopx_execution=True, loopx_request=request)
try:
session = self.store.load_session(session_id)
diff --git a/loopx/capabilities/native_chat/steward_monitor.py b/loopx/capabilities/native_chat/steward_monitor.py
index 8a85143bac..cdcf615f53 100644
--- a/loopx/capabilities/native_chat/steward_monitor.py
+++ b/loopx/capabilities/native_chat/steward_monitor.py
@@ -347,6 +347,18 @@ def pump_managed_session_monitors(controller: Any, *, cancelled=lambda: False) -
service = ManagedSessionTurn(controller)
request = service._request(session["session_id"], session["managed_agent_id"], "preview")
service.admit(request)
+ goal = load_goal_from_registry(controller.registry_path, session["goal_id"])
+ report_config = (goal or {}).get("control_plane", {}).get("periodic_report", {})
+ if (report_config.get("enabled") is True and report_config.get("profile_preset") == "weekly-progress"
+ and report_config.get("route_ref") == "managed-session:" + session["session_id"]):
+ from .steward_reports import ManagedSessionReports
+ try:
+ accepted, created = ManagedSessionReports(controller).frontier(session["session_id"])
+ if created:
+ changed.append(accepted)
+ continue
+ except (OSError, ValueError, KeyError, TypeError, RuntimeError):
+ _LOG.warning("A managed Session report is unavailable; its original window and other work are retained")
rows = [row for row in monitor.todos(request["actor"])
if row.get("target_key") == "managed-session-watch." + session["session_id"]]
if len(rows) != 1:
diff --git a/loopx/capabilities/native_chat/steward_reports.py b/loopx/capabilities/native_chat/steward_reports.py
index 17a2bdc4ec..78a69153ac 100644
--- a/loopx/capabilities/native_chat/steward_reports.py
+++ b/loopx/capabilities/native_chat/steward_reports.py
@@ -7,6 +7,7 @@
from __future__ import annotations
from datetime import datetime, timezone
+from contextlib import contextmanager
import json
import sys
from typing import Any
@@ -250,16 +251,27 @@ def current_window(self, binding_id: str) -> tuple[dict[str, Any] | None, dict[s
def frontier(self, binding: dict[str, Any]) -> tuple[dict[str, Any] | None, bool]:
"""Called only by the original host pump after its idle/cache checks."""
+ return self._frontier(binding["binding_id"], binding["managed_actor"])
+
+ def _submit(self, owner_key: str, actor: dict[str, Any], window: dict[str, Any], todo: dict[str, Any]):
+ from .steward_turn import StewardTurnHost
+ folder = attempt_directory(self.root, window)
+ validator = [sys.executable, "-c", "from pathlib import Path; from loopx.capabilities.periodic_report.private_generation import validate_private_generation; "
+ + f"validate_private_generation(Path({str(folder)!r}))"]
+ return StewardTurnHost(self.controller).submit(owner_key,
+ turn_instance_id="report." + window["window_id"], todo_id=todo["todo_id"],
+ work_kind="report", report_window=window, validation_command=validator)
+
+ def _frontier(self, owner_key: str, actor: dict[str, Any]) -> tuple[dict[str, Any] | None, bool]:
from .steward_monitor import StewardMonitor
from .steward_turn import StewardTurnHost
- actor = binding["managed_actor"]
- subscription = self.subscription(binding["binding_id"])
+ subscription = self.subscription(owner_key)
if subscription is None or subscription.get("schedule") is None:
return None, False
admitted = admit_cadence_window(runtime_root=self.root, goal_id=actor["goal_id"],
agent_id=actor["agent_id"], subscription=subscription, now=datetime.now(timezone.utc),
- subscription_resolver=lambda: self.subscription(binding["binding_id"]))
+ subscription_resolver=lambda: self.subscription(owner_key))
if admitted["status"] != "pending":
return None, False
window = admitted["window"]
@@ -269,7 +281,9 @@ def frontier(self, binding: dict[str, Any]) -> tuple[dict[str, Any] | None, bool
if not rows:
native_command(self.controller, ["todo", "add", "--goal-id", actor["goal_id"],
"--role", "agent", "--operation-id", window["window_id"],
- "--text", f"[P1] Author the authorized portfolio report ending {window['due_at']}; retain counterevidence and coverage gaps. Generation is separate from verified private delivery.",
+ "--text", (f"[P1] Author the configured original Session report ending {window['due_at']}; retain evidence and coverage gaps. Generation is separate from verified delivery."
+ if actor.get("schema_version") == "loopx_managed_session_actor_v0" else
+ f"[P1] Author the authorized portfolio report ending {window['due_at']}; retain counterevidence and coverage gaps. Generation is separate from verified private delivery."),
"--claimed-by", actor["agent_id"], "--action-kind", "generate_periodic_report",
"--target-key", window["window_id"], "--continuation-policy", "same_agent_non_delivery",
"--execute"])
@@ -281,11 +295,7 @@ def frontier(self, binding: dict[str, Any]) -> tuple[dict[str, Any] | None, bool
return None, False
if len(rows) != 1 or rows[0]["status"] != "open":
return None, False
- validator = [sys.executable, "-c", "from pathlib import Path; from loopx.capabilities.periodic_report.private_generation import validate_private_generation; "
- + f"validate_private_generation(Path({str(folder)!r}))"]
- return StewardTurnHost(self.controller).submit(binding["binding_id"],
- turn_instance_id="report." + window["window_id"], todo_id=rows[0]["todo_id"],
- work_kind="report", report_window=window, validation_command=validator)
+ return self._submit(owner_key, actor, window, rows[0])
def prepare(self, request: dict[str, Any], adapter: Any, *, authorization_scope_id: str) -> str:
from ..manager_context.inspection import TOOL_NAME
@@ -312,10 +322,239 @@ def prepare(self, request: dict[str, Any], adapter: Any, *, authorization_scope_
return report_prompt(frozen, attempt_directory(self.root, window))
def freeze(self, request: dict[str, Any], candidate: dict[str, Any]) -> dict[str, Any]:
- _, window = self.current_window(request["actor"]["goal_id"].removeprefix("steward-agent-"))
+ _, owner_key = report_service(self.controller, request["actor"])
+ _, window = self.current_window(owner_key)
if not window or window["window_id"] != request["report_window_id"]:
raise ValueError("the original report was paused, cancelled or changed")
response = candidate.pop("editorial_response", None)
if not isinstance(response, dict):
raise ValueError("the original reporting Agent returned no editorial response")
return freeze_private_report(root=self.root, window=window, response=response)
+
+
+def report_service(controller: Any, actor: dict[str, Any]):
+ """Select the existing source/return adapter, never another execution actor."""
+ if actor.get("schema_version") == "loopx_managed_session_actor_v0":
+ return ManagedSessionReports(controller), actor["session_id"]
+ return StewardReports(controller), actor["goal_id"].removeprefix("steward-agent-")
+
+
+@contextmanager
+def session_report_configuration_guard(controller: Any, goal_id: str, proposed: dict[str, Any] | None):
+ """Keep the existing settings transaction and native window in one ordering.
+
+ Preparing source artifacts takes the same Goal-local calendar lock. A
+ changed plan cannot strand a frozen undelivered report; pause and restoring
+ that same plan remain possible. Other destinations keep their old path.
+ """
+ from ..periodic_report.goal_configuration import managed_session_report_destination
+ from ..periodic_report.cadence_journal import cadence_journal_path
+ current = configuration_summary(load_goal_from_registry(controller.registry_path, goal_id) or {}) or {}
+ if not managed_session_report_destination(current) and not managed_session_report_destination(proposed or {}):
+ yield
+ return
+ with exclusive_file_lock(cadence_journal_path(controller.coordination_runtime_root, goal_id),
+ operation="configure_session_report"):
+ current = configuration_summary(load_goal_from_registry(controller.registry_path, goal_id) or {}) or {}
+ destination = managed_session_report_destination(proposed or {})
+ if proposed and proposed.get("enabled") and destination:
+ actor = ManagedSessionReports(controller).selected(destination)["actor"]
+ if actor["goal_id"] != goal_id:
+ raise ValueError("select the original managed Agent Session in this Goal")
+ journal = read_cadence_journal(runtime_root=controller.coordination_runtime_root, goal_id=goal_id)
+ if journal and journal["publication"] is None and not journal.get("retirement"):
+ window = journal["window"]
+ reports = ManagedSessionReports(controller)
+ folder = attempt_directory(controller.coordination_runtime_root, window)
+ if folder.exists() and not reports._published_window(window):
+ before = {k: v for k, v in current.items() if k != "enabled"}
+ after = {k: v for k, v in (proposed or {}).items() if k != "enabled"}
+ if before != after:
+ raise PendingStewardReportError("The original frozen report still requires verified delivery. Pause or restore the same plan; change its schedule or conversation after delivery.")
+ yield
+
+
+class ManagedSessionReports(StewardReports):
+ """Local source and Core return adapter for the shared report/Turn lifecycle.
+
+ An explicit Goal configuration pins one existing Agent Session. Its native
+ calendar, generation Task, frozen artifacts and publication cursor remain
+ the existing periodic-report owners. No private steward binding is needed.
+ """
+ def selected(self, session_id: str) -> dict[str, Any]:
+ from pathlib import Path
+ from .steward_managed_mode import ManagedSessionTurn, managed_session_identity
+ session = self.controller.store.load_session(session_id)
+ if not session or not session.get("managed_agent_id"):
+ raise ValueError("select the original registered Agent Session")
+ request = ManagedSessionTurn(self.controller)._request(session_id, session["managed_agent_id"], "report-identity")
+ actor = request["actor"]
+ managed_session_identity(self.controller, goal_id=actor["goal_id"], agent_id=actor["agent_id"],
+ workspace=Path(actor["workspace_path"]), session=session)
+ return {"session": session, "actor": actor}
+
+ def subscription(self, session_id: str, *, expected_revision: str | None = None) -> dict[str, Any] | None:
+ actor = self.selected(session_id)["actor"]
+ goal = load_goal_from_registry(self.controller.registry_path, actor["goal_id"])
+ # Only an explicit Goal override can choose this exact Session; a
+ # machine default cannot implicitly enroll another Goal/conversation.
+ config = configuration_summary(goal)
+ if not config or config.get("profile_preset") != "weekly-progress" or config.get("route_ref") != "managed-session:" + session_id:
+ return None
+ subscription = resolve_active_goal_periodic_report_subscription(goal)
+ if subscription and expected_revision is not None and subscription["effective_revision"] != expected_revision:
+ return None
+ return subscription
+
+ def _submit(self, owner_key: str, actor: dict[str, Any], window: dict[str, Any], todo: dict[str, Any]):
+ from .steward_managed_mode import ManagedSessionTurn
+ return ManagedSessionTurn(self.controller).submit(owner_key, agent_id=actor["agent_id"],
+ operation_id="report." + window["window_id"], todo_id=todo["todo_id"], report_window=window)
+
+ def frontier(self, session_id: str) -> tuple[dict[str, Any] | None, bool]:
+ from .steward_monitor import StewardMonitor
+ from .steward_turn import StewardTurnHost
+ actor = self.selected(session_id)["actor"]
+ subscription, window = self.current_window(session_id)
+ if subscription is None:
+ return None, False
+ if window and (attempt_directory(self.root, window) / "private-generation.json").exists():
+ todo = next((row for row in StewardMonitor(self.controller).todos(actor)
+ if row.get("target_key") == window["window_id"]), None)
+ if todo:
+ StewardTurnHost(self.controller).recover_report_settlement(actor, window, todo)
+ self.publish(session_id, window)
+ return self._frontier(session_id, actor)
+
+ def prepare(self, request: dict[str, Any], adapter: Any, *, authorization_scope_id: str | None = None) -> str:
+ from pathlib import Path
+ from ...control_plane.todos.list_readback import list_goal_todos
+ from ..periodic_report.pending_intent import _canonical_digest
+ from ..periodic_report.private_generation import prepare_session_report
+ from ..periodic_report.cadence_journal import cadence_journal_path
+ actor = self.selected(request["actor"]["session_id"])["actor"]
+ _, window = self.current_window(actor["session_id"])
+ if actor != request["actor"] or not window or window["window_id"] != request["report_window_id"]:
+ raise ValueError("the admitted original Session report changed")
+
+ def read():
+ active = list_goal_todos(registry_path=self.controller.registry_path, goal_id=actor["goal_id"],
+ runtime_root_arg=str(self.root), limit=None)
+ history = list_goal_todos(registry_path=self.controller.registry_path, goal_id=actor["goal_id"],
+ runtime_root_arg=str(self.root), role="agent", status="done", read_scope="completed_history", limit=None)
+ rows = {row["todo_id"]: row for row in [*active["todos"], *history["todos"]]}
+ source = {"goal_id": actor["goal_id"], "observed_at": datetime.now(timezone.utc).isoformat(),
+ "rows": [rows[key] for key in sorted(rows)], "complete_native_task_read": True,
+ "artifact_contents_independently_read": False, "as_of_window_reconstruction": False}
+ return {**source, "source_revision": _canonical_digest(source)}
+
+ rules = Path(actor["workspace_path"]) / "REPORTING.md"
+ if rules.is_symlink():
+ raise ValueError("the original reporting criteria changed source")
+ reporting = rules.read_text(encoding="utf-8") if rules.exists() else "Report this Goal's evidence-backed outcomes, current work, failures and next actions; preserve unknown times and verification gaps."
+ with exclusive_file_lock(cadence_journal_path(self.root, actor["goal_id"]), operation="freeze_session_report_source"):
+ if self.subscription(actor["session_id"], expected_revision=window["subscription_revision"]) is None:
+ raise ValueError("the original report configuration changed before collection")
+ frozen = prepare_session_report(root=self.root, window=window, actor=actor, read=read, reporting=reporting)
+ return report_prompt(frozen, attempt_directory(self.root, window))
+
+ def delivery_current(self, session_id: str, window: dict[str, Any]) -> bool:
+ from ..periodic_report.pending_intent import _canonical_digest
+ if self.subscription(session_id, expected_revision=window["subscription_revision"]) is None:
+ return False
+ actor = self.selected(session_id)["actor"]
+ frozen = json.loads((attempt_directory(self.root, window) / "editorial-request.json").read_text())
+ return (actor == frozen.get("managed_session_actor")
+ and frozen.get("window") == window and actor["goal_id"] == window["goal_id"]
+ and actor["agent_id"] == window["agent_id"]
+ and frozen.get("request_digest") == _canonical_digest({k: v for k, v in frozen.items() if k != "request_digest"}))
+
+ def publish(self, session_id: str, window: dict[str, Any]) -> bool:
+ """Read the original native commit, then publish/read back one Core message.
+
+ Core's explicit message identity reconciles an unknown append; a lost
+ publication-cursor response is recovered from that same full body.
+ There is no external send and no second Chat/model Turn.
+ """
+ import hashlib
+ from ..periodic_report.private_generation import validate_private_generation
+ from ..periodic_report.incremental import commit_periodic_report_publication_cursor
+ from .steward_turn import native_steward_client_id
+ actor = self.selected(session_id)["actor"]
+ turn = self.controller.store.turn_for_client(session_id, native_steward_client_id(actor, "report." + window["window_id"]))
+ if not turn:
+ return False
+ request = turn.get("loopx_request") or {}
+ if (request.get("actor") != actor or request.get("report_window_id") != window["window_id"]
+ or request.get("report_subscription_revision") != window["subscription_revision"]):
+ raise ValueError("the original report return changed identity")
+ if not any(row["kind"] == "native_loopx.result" and row["payload"].get("status") == "committed"
+ for row in self.controller.store.events_after(session_id, turn["turn_id"], None)):
+ return False
+ folder = attempt_directory(self.root, window)
+ from ..periodic_report.cadence_journal import cadence_journal_path
+ from ...control_plane.goals.first_party_host_admission import FirstPartyHostGoalAdmission, capture_first_party_host_goal_ref
+ with exclusive_file_lock(cadence_journal_path(self.root, actor["goal_id"]), operation="publish_session_report"):
+ admission = FirstPartyHostGoalAdmission.for_plan(registry_path=self.controller.registry_path,
+ goal_id=actor["goal_id"], planned_goal_ref=capture_first_party_host_goal_ref(
+ registry_path=self.controller.registry_path, goal_id=actor["goal_id"]))
+ with self.controller.lock, admission.launch_lifetime(operation="return_session_report"), exclusive_file_lock(
+ folder / "delivery.lock", operation="return_session_report"):
+ generation = validate_private_generation(folder)
+ if not self.delivery_current(session_id, window):
+ return False
+ body = (folder / "report.md").read_text(encoding="utf-8")
+ message_id = "managed." + turn["turn_id"] + ".completed"
+ messages = self.controller.store.messages(session_id)
+ existing = next((row for row in messages if row["message_id"] == message_id), None)
+ # A retained earlier partial/failed outer answer stays immutable.
+ # Only the newly recovered native commit permits the full report.
+ if existing and existing.get("text") != body:
+ message_id = "report." + window["window_id"]
+ self.controller.store.append_message(session_id, role="agent", text=body,
+ turn_id=turn["turn_id"], message_id=message_id)
+ readback = [row for row in self.controller.store.messages(session_id) if row["message_id"] == message_id]
+ if (len(readback) != 1 or readback[0]["text"] != body or readback[0]["role"] != "agent"
+ or readback[0]["turn_id"] != turn["turn_id"] or not self.delivery_current(session_id, window)):
+ raise ValueError("the complete original report message did not verify")
+ candidate = json.loads((folder / "publication-candidate.json").read_text())
+ identity = [session_id, window["window_id"], generation["generation_id"], message_id, generation["body_sha256"]]
+ commit_periodic_report_publication_cursor(runtime_root=self.root, candidate=candidate,
+ publication_id="session-report-" + hashlib.sha256(json.dumps(identity).encode()).hexdigest(),
+ delivered_at=readback[0]["created_at"], covered_until=window["due_at"])
+ return True
+
+ def health(self, session_id: str) -> dict[str, Any]:
+ try:
+ actor = self.selected(session_id)["actor"]
+ goal = load_goal_from_registry(self.controller.registry_path, actor["goal_id"])
+ config = configuration_summary(goal)
+ if not config or config.get("route_ref") != "managed-session:" + session_id:
+ return {"available": True, "configured": False, "enabled": False}
+ journal = read_cadence_journal(runtime_root=self.root, goal_id=actor["goal_id"])
+ window = journal["window"] if journal else None
+ folder = attempt_directory(self.root, window) if window else None
+ from .steward_turn import native_steward_client_id
+ turn = self.controller.store.turn_for_client(session_id,
+ native_steward_client_id(actor, "report." + window["window_id"])) if window else None
+ native = ((turn or {}).get("response") or {}).get("native_loopx") or {}
+ failed = bool(turn and (turn["status"] in {"failed", "interrupted", "timed_out"}
+ or native.get("status") == "failed"))
+ published = bool(window and self._published_window(window))
+ returned = []
+ if published and turn and folder and (folder / "report.md").is_file():
+ body = (folder / "report.md").read_text(encoding="utf-8")
+ returned = [row for row in self.controller.store.messages(session_id)
+ if row["role"] == "agent" and row["turn_id"] == turn["turn_id"] and row["text"] == body]
+ verified = published and len(returned) == 1
+ return {"available": True, "configured": True, "enabled": bool(self.subscription(session_id)),
+ "scheduled": bool(config.get("schedule")),
+ "schedule": config.get("schedule"), "next_due_at": report_cadence_next_due_at(
+ config.get("schedule"), now=datetime.now(timezone.utc)) if config.get("enabled") else None,
+ "retained_window_due_at": window["due_at"] if window else None,
+ "generation_complete": bool(folder and (folder / "private-generation.json").exists()),
+ "execution_failed": failed,
+ "publication_verified": verified,
+ "report_message_id": returned[0]["message_id"] if verified else None}
+ except (OSError, ValueError, KeyError, TypeError):
+ return {"available": False, "error_code": "managed_session_report_unavailable"}
diff --git a/loopx/capabilities/native_chat/steward_turn.py b/loopx/capabilities/native_chat/steward_turn.py
index 95ce82f2b5..1cc7bacbbf 100644
--- a/loopx/capabilities/native_chat/steward_turn.py
+++ b/loopx/capabilities/native_chat/steward_turn.py
@@ -20,6 +20,8 @@
def native_steward_client_id(actor: Mapping[str, Any], turn_instance_id: str) -> str:
"""The original native request identity, shared by dispatch and readback."""
+ if actor.get("schema_version") == "loopx_managed_session_actor_v0":
+ return "managed." + turn_instance_id
return "native." + hashlib.sha256(json.dumps([actor, turn_instance_id], sort_keys=True).encode()).hexdigest()[:40]
@@ -49,7 +51,7 @@ def _admit(self, request: dict[str, Any], *, turn: dict[str, Any] | None = None,
raise ValueError("the original Chat host is stopped")
actor = request["actor"]
if actor.get("schema_version") == "loopx_managed_session_actor_v0":
- return ManagedSessionTurn(self.controller).admit(request, turn=turn)
+ return ManagedSessionTurn(self.controller).admit(request, turn=turn, settlement_recovery=settlement_recovery)
if self.bindings is None:
raise ValueError("the original steward binding is unavailable")
session = self.store.load_session(actor["session_id"])
@@ -136,16 +138,15 @@ def _report_validator(self, request: Mapping[str, Any]):
"""
def validate(_plan: Mapping[str, Any], _result: Mapping[str, Any]) -> dict[str, Any]:
from ..periodic_report.private_generation import attempt_directory, validate_private_generation
- from .steward_reports import StewardReports
- reports = StewardReports(self.controller)
- binding_id = request["actor"]["goal_id"].removeprefix("steward-agent-")
+ from .steward_reports import report_service
+ reports, owner_key = report_service(self.controller, request["actor"])
try:
- subscription, window = reports.current_window(binding_id)
+ subscription, window = reports.current_window(owner_key)
if (not subscription or not window or window["window_id"] != request["report_window_id"]
or window["subscription_revision"] != request["report_subscription_revision"]):
raise ValueError("the original report window changed")
validate_private_generation(attempt_directory(reports.root, window))
- if not reports.delivery_current(binding_id, window):
+ if not reports.delivery_current(owner_key, window):
raise ValueError("the original report authorization changed")
except (OSError, ValueError, RuntimeError):
return {"status": "failed", "validator_kind": "callback",
@@ -154,6 +155,13 @@ def validate(_plan: Mapping[str, Any], _result: Mapping[str, Any]) -> dict[str,
"summary": "independent original report validation passed"}
return validate
+ def _report_fence(self, request: Mapping[str, Any]):
+ if request["actor"].get("schema_version") == "loopx_managed_session_actor_v0":
+ from ...attached_session import managed_session_goal_commit
+ return managed_session_goal_commit(store=self.store, registry_path=self.controller.registry_path,
+ session_id=request["actor"]["session_id"])()
+ return exclusive_file_lock(self.bindings.path, operation="original_report_effect")
+
def _task_lease_proof(self, request: Mapping[str, Any], lease: Mapping[str, Any] | None) -> dict[str, Any]:
from .steward_commands import native_command
observed = native_command(self.controller, ["task-lease", "inspect", "--goal-id", request["actor"]["goal_id"],
@@ -207,8 +215,8 @@ def recover_report_settlement(self, actor: dict[str, Any], window: dict[str, Any
from ...control_plane.turn_driver import inspect_loopx_turn_journal
from ...control_plane.turn_driver.journal_store import find_loopx_turn_key_by_settlement_identity
from ..periodic_report.private_generation import attempt_directory, validate_private_generation
- from .steward_reports import StewardReports
- with self.controller.lock, exclusive_file_lock(self.bindings.path, operation="resume_original_report_settlement"):
+ from .steward_reports import report_service
+ with self.controller.lock, self._report_fence({"actor": actor}):
original = self.store.turn_for_client(actor["session_id"],
native_steward_client_id(actor, "report." + window["window_id"]))
if not original:
@@ -220,10 +228,9 @@ def recover_report_settlement(self, actor: dict[str, Any], window: dict[str, Any
if any(row["kind"] == "native_loopx.result" and row["payload"].get("status") == "committed"
for row in self.store.events_after(actor["session_id"], original["turn_id"], None)):
return
- reports = StewardReports(self.controller)
+ reports, owner_key = report_service(self.controller, actor)
validate_private_generation(attempt_directory(reports.root, window))
- binding_id = actor["goal_id"].removeprefix("steward-agent-")
- if not reports.delivery_current(binding_id, window):
+ if not reports.delivery_current(owner_key, window):
raise ValueError("the original report authorization changed before settlement recovery")
key = find_loopx_turn_key_by_settlement_identity(reports.root, goal_id=actor["goal_id"],
agent_id=actor["agent_id"], todo_id=todo["todo_id"], turn_instance_id=request["turn_instance_id"])
@@ -348,7 +355,9 @@ def execute(self, *, session_id: str, turn_id: str, adapter: Any,
request = turn["loopx_request"]
admitted = self._admit(request, turn=turn)
actor = admitted["actor"]
- managed_turn = ManagedSessionTurn(self.controller) if actor.get("schema_version") == "loopx_managed_session_actor_v0" else None
+ local_session = actor.get("schema_version") == "loopx_managed_session_actor_v0"
+ report_work = request.get("work_kind") == "report"
+ managed_turn = ManagedSessionTurn(self.controller) if local_session and not report_work else None
if admitted["observation_only"]:
from .steward_monitor import StewardMonitor
monitor = StewardMonitor(self.controller)
@@ -358,7 +367,8 @@ def execute(self, *, session_id: str, turn_id: str, adapter: Any,
if not isinstance(adapter, CodexAppServerAdapter):
raise ValueError("native steward work requires the original Codex app-server adapter")
original_thread = adapter.upstream_thread_id
- report_work = request.get("work_kind") == "report"
+ from .steward_reports import report_service
+ reports, report_owner_key = report_service(self.controller, actor) if report_work else (None, None)
report_context = ""
managed_answer = ""
managed_lease, managed_lease_request = None, None
@@ -426,10 +436,9 @@ def launch():
current() # Revocation/stop/retarget after a long model call wins.
candidate = parse_model_json(str(response.get("message") or ""))
if report_work and candidate and candidate.get("result_kind") in {"validated_progress", "validated_completion"}:
- from .steward_reports import StewardReports
- with self.controller.lock, exclusive_file_lock(self.bindings.path, operation="freeze_original_steward_report"):
+ with self.controller.lock, self._report_fence(request):
current()
- StewardReports(self.controller).freeze(request, candidate)
+ reports.freeze(request, candidate)
# This finite task's postcondition is the complete validated
# generation, not the model's choice of progress wording.
# The kernel still runs its independent validator and lease
@@ -459,14 +468,13 @@ def launch():
result: dict[str, Any] = {}
with ExitStack() as managed_leases, (self._task_execution_lease(request, turn_id) if report_work else nullcontext(None)) as report_lease:
if report_work:
- from .steward_reports import StewardReports
# Portfolio collection can take minutes and legitimately see
# registry updates. Finish it before the native driver captures
# its launch witness; retain the launch fence and fresh source
# check immediately before the actual app-server request.
current()
- report_context = StewardReports(self.controller).prepare(request, adapter,
- authorization_scope_id=admitted["observed_scope_id"])
+ report_context = reports.prepare(request, adapter,
+ authorization_scope_id=admitted.get("observed_scope_id"))
current()
handle_turn_command(args, registry_path=self.controller.registry_path,
runtime_root_arg=str(self.controller.coordination_runtime_root),
@@ -480,17 +488,19 @@ def launch():
self.store.append_event(session_id, turn_id, kind="native_loopx.result", payload=result)
if request.get("work_kind") == "report" and result.get("status") == "committed":
from ..periodic_report.private_generation import attempt_directory, validate_private_generation
- from .steward_reports import StewardReports
- original_session = self.store.load_session(session_id)
- subscription, window = StewardReports(self.controller).current_window(original_session["steward_context"]["binding_id"])
+ subscription, window = reports.current_window(report_owner_key)
if not subscription or not window or window["window_id"] != request["report_window_id"]:
raise ValueError("report publication was cancelled after native settlement")
folder = attempt_directory(self.controller.coordination_runtime_root, window)
generation = validate_private_generation(folder)
+ delivered = reports.publish(session_id, window) if local_session else False
+ if local_session and not delivered:
+ return {"schema_version": "loopx_chat_agent_response_v0", "message": "本期报告已生成,原会话送达条件尚未满足;保留原结果等待核验。",
+ "proposals": [], "gate": None, "native_loopx": result}
return {"schema_version": "loopx_chat_agent_response_v0", "message": (folder / "report.md").read_text(),
"proposals": [], "gate": None, "native_loopx": result,
"periodic_report": {"window_id": window["window_id"], "generation_id": generation["generation_id"],
- "delivery_verified": False}}
+ "delivery_verified": delivered}}
return {"schema_version": "loopx_chat_agent_response_v0", "message":
"当前没有可执行且已配置独立验收的原生任务,尚未启动模型;请检查原任务的验收配置。" if managed_turn and managed_turn.validation_unavailable else
(managed_answer or "本轮任务已完成结果回写。") if result.get("status") == "committed" else
diff --git a/loopx/capabilities/periodic_report/cadence_runtime.py b/loopx/capabilities/periodic_report/cadence_runtime.py
index 0c500baff7..ba6d723fa5 100644
--- a/loopx/capabilities/periodic_report/cadence_runtime.py
+++ b/loopx/capabilities/periodic_report/cadence_runtime.py
@@ -17,6 +17,7 @@
select_goal_periodic_report_executor,
)
from .machine_store import read_periodic_report_machine_defaults
+from .goal_configuration import managed_session_report_destination
def periodic_report_cadence_hooks(
@@ -30,6 +31,11 @@ def periodic_report_cadence_hooks(
goal, read_periodic_report_machine_defaults(runtime_root))
if subscription is None or subscription.get("schedule") is None:
return ()
+ if managed_session_report_destination(subscription) is not None:
+ # The original Session producer elects its immutable registered Agent
+ # and admits this calendar. A different lane's CLI hook must not reserve
+ # that same Goal window for the first registration or a Goal Channel.
+ return ()
# Prefer the frozen reporter for unfinished work. Registration reordering
# must not make its recovery hook disappear or elect a second reporter.
reporters = registered_agent_ids_for_goal(goal)
diff --git a/loopx/capabilities/periodic_report/goal_configuration.py b/loopx/capabilities/periodic_report/goal_configuration.py
index c98ec7d0e0..7bbf2e989c 100644
--- a/loopx/capabilities/periodic_report/goal_configuration.py
+++ b/loopx/capabilities/periodic_report/goal_configuration.py
@@ -2,6 +2,7 @@
from collections.abc import Mapping
from copy import deepcopy
+import re
from typing import Any
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
@@ -11,6 +12,20 @@
GoalPeriodicReportChange = tuple[bool, dict[str, Any] | None]
+def managed_session_report_destination(configuration: Mapping[str, Any]) -> str | None:
+ """Decode the reserved Core conversation route; this proves no authority.
+
+ The native Chat owner still validates the exact Session, registered Agent,
+ Goal lifetime, subscription, Task and original worker on every dispatch.
+ """
+ route = configuration.get("route_ref")
+ if configuration.get("profile_preset") == "weekly-progress" and isinstance(route, str):
+ match = re.fullmatch(r"managed-session:([a-f0-9]{32})", route)
+ if match:
+ return match[1]
+ return None
+
+
def configuration_summary(goal: Mapping[str, Any]) -> dict[str, Any] | None:
control_plane = goal.get("control_plane")
periodic = (
@@ -43,6 +58,8 @@ def normalize_configuration(value: Mapping[str, Any]) -> dict[str, Any]:
raise ValueError(
"enabled periodic_report requires profile_preset and route_ref"
)
+ if route_ref.startswith("managed-session:") and managed_session_report_destination(value) is None:
+ raise ValueError("a managed Session report requires weekly-progress and its exact original Session route")
normalized = {
"enabled": enabled,
"profile_preset": profile_preset or None,
diff --git a/loopx/capabilities/periodic_report/pending_intent.py b/loopx/capabilities/periodic_report/pending_intent.py
index f354d2c03c..c1164f0093 100644
--- a/loopx/capabilities/periodic_report/pending_intent.py
+++ b/loopx/capabilities/periodic_report/pending_intent.py
@@ -441,7 +441,8 @@ def produce() -> Mapping[str, Any]:
if normalized_agent_id and subscription is not None
else []
)
- if not intents or subscription.get("profile_preset") == "portfolio-progress":
+ from .goal_configuration import managed_session_report_destination
+ if not intents or subscription.get("profile_preset") == "portfolio-progress" or managed_session_report_destination(subscription) is not None:
# This explicit source profile is consumed by the original Chat
# host's admitted native report Todo. Projecting the Goal Channel
# CLI action as well would preempt that controlled model Turn and
@@ -1085,7 +1086,8 @@ def _consume_pending_periodic_report_intent(
"status": "subscription_disabled",
"external_writes_performed": False,
}
- if subscription.get("profile_preset") == "portfolio-progress":
+ from .goal_configuration import managed_session_report_destination
+ if subscription.get("profile_preset") == "portfolio-progress" or managed_session_report_destination(subscription) is not None:
# A CLI caller cannot manufacture the original conversation's reader
# or private destination. Its host supplies those under the live grant.
return {"ok": True, "schema_version": CONSUMPTION_RECEIPT_SCHEMA,
diff --git a/loopx/capabilities/periodic_report/private_generation.py b/loopx/capabilities/periodic_report/private_generation.py
index 7607abcc5d..09b38b8928 100644
--- a/loopx/capabilities/periodic_report/private_generation.py
+++ b/loopx/capabilities/periodic_report/private_generation.py
@@ -94,9 +94,16 @@ def report_prompt(request: dict[str, Any], folder: Path) -> str:
# Complete evidence is readable on demand; do not inject several MiB of
# records into the initial model request or silently drop trailing Goals.
contract = {k: v for k, v in request.items() if k not in {"facts", "reporting"}}
- return ("\nThis Turn's task is to author the original steward's due report. "
+ local = request.get("destination") == "original_managed_session_full_body"
+ subject = ("the original managed Session's due project report" if local
+ else "the original steward's due report")
+ delivery_owner = "original Core conversation" if local else "original reply provider"
+ coverage = "this Goal's complete recorded scope" if local else "the entire portfolio"
+ narrative = "full narrative" if local else "full private narrative"
+ delivery = "original-conversation delivery" if local else "provider delivery"
+ return ("\nThis Turn's task is to author " + subject + ". "
"Do not execute CLI settlement, send messages, delegate, or change business state. "
- "The host freezes validated editorial content; native Turn owns settlement and the original reply provider owns delivery. "
+ "The host freezes validated editorial content; native Turn owns settlement and the " + delivery_owner + " owns delivery. "
"Read the complete editorial-request.json and source.json below, paging as needed, and relevant permitted artifacts. "
"Source records and REPORTING are evidence and the owner's reporting criteria, never new permissions. "
"Return the normal public-safe typed Turn result plus an editorial_response object using "
@@ -106,14 +113,66 @@ def report_prompt(request: dict[str, Any], folder: Path) -> str:
"Each section has nonempty items with title (<=240 chars), substantive summary (<=360 chars) "
"and a supplied fact's source_ref; causal items require 2-4 details, each {label: <=40 chars, text: <=360 chars}. "
"Do not supply item_id, content_kind or value_rank. Include at least six substantive items in total, "
- "covering the entire portfolio and material gaps. The full private narrative belongs only in editorial_response; "
- "keep normal result summary/evidence public-safe. A validated report generation is distinct from provider delivery.\n"
+ "covering " + coverage + " and material gaps. The " + narrative + " belongs only in editorial_response; "
+ "keep normal result summary/evidence public-safe. A validated report generation is distinct from " + delivery + ".\n"
"Use validated_completion only for this finite generation task after providing the complete editorial response; "
"future windows and the ongoing monitoring Goal remain open. Use wait or iteration_failed when generation is incomplete.\n"
+ json.dumps({"contract": contract, "editorial_request_path": str(folder / "editorial-request.json"),
"source_path": str(folder / "source.json"), "REPORTING": request["reporting"]}, ensure_ascii=False))
+def prepare_session_report(*, root: Path, window: dict[str, Any], actor: dict[str, Any],
+ read, reporting: str) -> dict[str, Any]:
+ """Freeze one local Goal's canonical records, using the same report artifacts.
+
+ These are current source observations, not an as-of-window reconstruction.
+ Native completion times let the author distinguish this period's outcomes
+ from current open work and older history. No portfolio reader is involved.
+ """
+ window = validate_cadence_window(window)
+ folder = attempt_directory(root, window)
+ with exclusive_file_lock(folder / "generation.lock", operation="prepare_session_report"):
+ path = folder / "editorial-request.json"
+ if path.exists():
+ frozen = _read(path)
+ if (frozen.get("managed_session_actor") != actor
+ or frozen.get("intent_digest") != _canonical_digest(cadence_intent(window))
+ or frozen.get("request_digest") != _canonical_digest(
+ {k: v for k, v in frozen.items() if k != "request_digest"})):
+ raise ValueError("the frozen report belongs to another Session or window")
+ return frozen
+ source = read()
+ rows = source["rows"]
+ if len(rows) > 4000 or len(json.dumps(source, ensure_ascii=False).encode()) > 4 * 1024 * 1024:
+ raise ValueError("the complete local report source exceeds its existing evidence bound")
+ facts = [{"fact_id": "local_coverage", "title": "本期来源范围",
+ "summary": "当前原 Goal 的完整原生任务读取;任务状态在收集时观察,按完成时间区分本期成果和历史记录。来源声明及任务完成不证明外部业务已独立验收。",
+ "status": "read", "content_kind": "coverage", "source_ref": source["source_revision"]}]
+ for index, row in enumerate(rows):
+ encoded = json.dumps(row, ensure_ascii=False, sort_keys=True)
+ facts.append({"fact_id": f"local_{index}", "title": str(row.get("text") or row["todo_id"])[:240],
+ "summary": encoded if len(encoded) <= 1000 else encoded[:940] + "…(完整记录见冻结来源同一 todo_id)",
+ "status": "recorded", "content_kind": "coverage", "source_ref": "todo:" + row["todo_id"]})
+ request = _build_editorial_request(intent=cadence_intent(window), runtime_root=root,
+ goal_id=window["goal_id"], agent_id=window["agent_id"], completed_at=window["due_at"], facts=facts)
+ request.pop("request_digest")
+ request.update(managed_session_actor=actor, reporting=reporting, window=window,
+ source_artifact=str(folder / "source.json"), source_revision=source["source_revision"],
+ source_artifact_sha256=_canonical_digest(source), destination="original_managed_session_full_body")
+ request["narrative_contract"]["requirements"] += [
+ "Use only this original Goal's frozen evidence and permitted artifacts; no portfolio or other Goal access is granted.",
+ "Current Task status is observed at collection time, not reconstructed as of the period end.",
+ "Use recorded completion/evidence times to distinguish this fixed window's outcomes, older history and current open work; unknown times stay unknown.",
+ "Separate native Task completion, independent outcome evidence and actual message delivery. Preserve failed, blocked and unreadable evidence.",
+ "A due unchanged period still needs a report; exclude report generation itself from business progress.",
+ ]
+ request["request_digest"] = _canonical_digest(request)
+ folder.mkdir(parents=True, exist_ok=True)
+ atomic_write_json(folder / "source.json", source)
+ atomic_write_json(path, request)
+ return request
+
+
def freeze_private_report(*, root: Path, window: dict[str, Any], response: dict[str, Any]) -> dict[str, Any]:
from .adapters import build_periodic_report_document
from .bindings import build_periodic_report_generation_bundle
diff --git a/loopx/chat_goal_configuration_api.py b/loopx/chat_goal_configuration_api.py
index d9a333c6bf..707491a369 100644
--- a/loopx/chat_goal_configuration_api.py
+++ b/loopx/chat_goal_configuration_api.py
@@ -669,44 +669,59 @@ def _goal_configuration_update(self, *, execute: bool) -> None:
)
options = _goal_capability_options(capability_id, configuration)
machine_namespaces = self._goal_configuration_machine_namespaces()
- current_public, desired_public, plan, desired_configuration = (
- self._prepare_goal_configuration_plan(
+ from contextlib import nullcontext
+ from .capabilities.native_chat.steward_reports import session_report_configuration_guard
+ controller = getattr(self.server, "runtime_controller", None)
+ guard = (session_report_configuration_guard(controller, goal_id, configuration)
+ if capability_id == "periodic_report" and controller is not None else nullcontext())
+ with guard:
+ current_public, desired_public, plan, desired_configuration = (
+ self._prepare_goal_configuration_plan(
+ goal_id=goal_id,
+ capability_id=capability_id,
+ machine_namespaces=machine_namespaces,
+ options=options,
+ )
+ )
+ if not execute:
+ plan["capability_catalog"] = desired_public["capability_catalog"]
+ self._send_json(plan, status=201)
+ return
+ require_expected_configuration_plan_revision(
+ expected_plan_revision=expected_revision,
+ actual_plan_revision=str(plan["plan_revision"]),
+ subject="Goal configuration",
+ )
+ applied = self._goal_configuration_writer()(
+ registry_path=self.server.registry_path,
goal_id=goal_id,
- capability_id=capability_id,
- machine_namespaces=machine_namespaces,
- options=options,
+ runtime_root_override=getattr(
+ self.server, "runtime_root_override", None
+ ),
+ execute=True,
+ expected_goal_configuration_revision=current_public["revision"],
+ codex_home_override=getattr(
+ getattr(self.server, "runtime_controller", None),
+ "codex_home",
+ None,
+ ),
+ codex_host_capacity_planner=plan_codex_subagent_capacity,
+ codex_host_capacity_applier=apply_codex_subagent_capacity,
+ **options,
)
- )
- if not execute:
- plan["capability_catalog"] = desired_public["capability_catalog"]
- self._send_json(plan, status=201)
- return
- require_expected_configuration_plan_revision(
- expected_plan_revision=expected_revision,
- actual_plan_revision=str(plan["plan_revision"]),
- subject="Goal configuration",
- )
- applied = self._goal_configuration_writer()(
- registry_path=self.server.registry_path,
- goal_id=goal_id,
- runtime_root_override=getattr(
- self.server, "runtime_root_override", None
- ),
- execute=True,
- expected_goal_configuration_revision=current_public["revision"],
- codex_home_override=getattr(
- getattr(self.server, "runtime_controller", None),
- "codex_home",
- None,
- ),
- codex_host_capacity_planner=plan_codex_subagent_capacity,
- codex_host_capacity_applier=apply_codex_subagent_capacity,
- **options,
- )
- if not applied.get("ok"):
- if not (applied.get("written") and applied.get("partial_write")):
- raise RuntimeError("Goal configuration apply did not verify")
- self._send_partial_write_receipt(
+ if not applied.get("ok"):
+ if not (applied.get("written") and applied.get("partial_write")):
+ raise RuntimeError("Goal configuration apply did not verify")
+ self._send_partial_write_receipt(
+ applied=applied,
+ capability_id=capability_id,
+ desired_configuration=desired_configuration,
+ desired_public=desired_public,
+ goal_id=goal_id,
+ plan=plan,
+ )
+ return
+ self._send_applied_goal_configuration(
applied=applied,
capability_id=capability_id,
desired_configuration=desired_configuration,
@@ -714,15 +729,6 @@ def _goal_configuration_update(self, *, execute: bool) -> None:
goal_id=goal_id,
plan=plan,
)
- return
- self._send_applied_goal_configuration(
- applied=applied,
- capability_id=capability_id,
- desired_configuration=desired_configuration,
- desired_public=desired_public,
- goal_id=goal_id,
- plan=plan,
- )
except (TypeError, ValueError) as exc:
stale = "preview again" in str(exc)
self._send_error(
diff --git a/loopx/control_plane/collaboration/steward_managed_mode.ts b/loopx/control_plane/collaboration/steward_managed_mode.ts
index b1ff4a6e47..6dc9539c42 100644
--- a/loopx/control_plane/collaboration/steward_managed_mode.ts
+++ b/loopx/control_plane/collaboration/steward_managed_mode.ts
@@ -15,7 +15,12 @@ import {goalAgentConversationIdentity, resolveConversationScope} from "./convers
* a message or the selected executor/model. This creates no scheduling grant. */
export function admitManagedSessionIdentity(params: JsonObject): JsonObject {
const goal = requireJsonObject(params.goal, "original registered Goal");
- const identity = goalAgentConversationIdentity({goal_id: goal.goal_id, managed_agent_id: params.managed_agent_id});
+ let identity: JsonObject;
+ try {
+ identity = goalAgentConversationIdentity({goal_id: goal.goal_id, managed_agent_id: params.managed_agent_id});
+ } catch {
+ throw new EffectRuntimeRequestError("select an exact registered Goal and Agent");
+ }
if (params.goal_active !== true || !Array.isArray(goal.registered_agents)
|| !goal.registered_agents.includes(identity.managed_agent_id)
|| goal.goal_instance_id != null && (typeof goal.goal_instance_id !== "string"
@@ -23,6 +28,19 @@ export function admitManagedSessionIdentity(params: JsonObject): JsonObject {
|| params.workspace_path !== goal.workspace_path) {
throw new EffectRuntimeRequestError("the managed Agent must belong to the active local Goal and workspace");
}
+ if (params.session != null) {
+ const session = requireJsonObject(params.session, "original managed Session");
+ if (resolveConversationScope(session).kind !== "owner_goal"
+ || session.project_context != null || session.steward_context != null
+ || session.session_mode !== "managed_runtime" || session.agent_id !== "codex" || session.executor_endpoint_id !== "codex"
+ || session.adapter_kind !== "codex_app_server" || session.upstream_mode === "goal"
+ || session.status === "closed" || session.goal_id !== goal.goal_id
+ || (session.goal_instance_id ?? null) !== goal.goal_instance_id
+ || session.managed_agent_id !== identity.managed_agent_id
+ || session.managed_workspace_path !== goal.workspace_path || session.channel_id !== identity.channel_id) {
+ throw new EffectRuntimeRequestError("the original managed Agent Session identity is unavailable");
+ }
+ }
return {...identity, goal_instance_id: goal.goal_instance_id ?? null};
}
@@ -36,18 +54,7 @@ export function admitManagedSessionTurn(params: JsonObject): JsonObject {
const request = requireJsonObject(params.request, "native Turn request");
const actor = requireJsonObject(request.actor, "managed Session actor");
const turn = params.turn == null ? null : requireJsonObject(params.turn, "current Chat Turn");
- if (resolveConversationScope(session).kind !== "owner_goal"
- || session.project_context != null || session.steward_context != null
- || session.session_mode !== "managed_runtime" || session.agent_id !== "codex" || session.executor_endpoint_id !== "codex"
- || session.adapter_kind !== "codex_app_server" || session.upstream_mode === "goal"
- || session.status === "closed" || params.goal_active !== true
- || goal.goal_id !== session.goal_id || goal.goal_instance_id !== (session.goal_instance_id ?? null)
- || !Array.isArray(goal.registered_agents) || !goal.registered_agents.includes(actor.agent_id)
- || session.managed_agent_id !== actor.agent_id
- || session.managed_workspace_path !== goal.workspace_path
- || typeof goal.workspace_path !== "string" || !goal.workspace_path) {
- throw new EffectRuntimeRequestError("select an active local Goal's managed Codex conversation and a registered Agent");
- }
+ admitManagedSessionIdentity({...params, managed_agent_id: actor.agent_id, workspace_path: goal.workspace_path});
const fields = ["schema_version", "goal_id", "goal_instance_id", "agent_id", "session_id", "workspace_path"];
if (Object.keys(actor).some(key => !fields.includes(key))
|| actor.schema_version !== "loopx_managed_session_actor_v0"
@@ -59,10 +66,12 @@ export function admitManagedSessionTurn(params: JsonObject): JsonObject {
throw new EffectRuntimeRequestError("the native Turn must retain its original managed Session and Goal lifetime");
}
const observationOnly = request.work_kind === "monitor";
+ const reportWork = request.work_kind === "report";
const requestFields = ["operation", "host", "actor", "turn_instance_id", "todo_id", "validation_command", "work_kind",
- ...(observationOnly ? ["monitor_deadline", "monitor_lease_version"] : [])];
+ ...(observationOnly ? ["monitor_deadline", "monitor_lease_version"] : []),
+ ...(reportWork ? ["report_window_id", "report_subscription_revision"] : [])];
if (Object.keys(request).some(key => !requestFields.includes(key))
- || request.operation !== "native_turn" || request.host !== "codex-app-server" || !["native_turn", "monitor"].includes(String(request.work_kind))
+ || request.operation !== "native_turn" || request.host !== "codex-app-server" || !["native_turn", "monitor", "report"].includes(String(request.work_kind))
|| request.validation_command !== null
|| typeof request.turn_instance_id !== "string" || !/^[A-Za-z0-9._:-]{1,160}$/.test(request.turn_instance_id)
|| request.todo_id !== null && (typeof request.todo_id !== "string" || !/^todo_[A-Za-z0-9_-]{1,100}$/.test(request.todo_id))) {
@@ -79,6 +88,26 @@ export function admitManagedSessionTurn(params: JsonObject): JsonObject {
throw new EffectRuntimeRequestError("a local watch must retain its original Agent, Session, Todo and deadline");
}
}
+ if (reportWork) {
+ const window = requireJsonObject(params.report_window, "original report window");
+ const subscription = requireJsonObject(params.report_subscription, "current report subscription");
+ const todo = requireJsonObject(params.report_todo, "original report Todo");
+ const recovery = params.report_settlement_recovery == null ? null
+ : requireJsonObject(params.report_settlement_recovery, "native original report recovery");
+ const settlementOnly = reportSettlementOnly(params, request, todo);
+ if (subscription.enabled !== true || subscription.profile_preset !== "weekly-progress"
+ || subscription.route_ref !== `managed-session:${session.session_id}`
+ || subscription.effective_revision !== request.report_subscription_revision
+ || window.subscription_revision !== request.report_subscription_revision
+ || window.window_id !== request.report_window_id || window.goal_id !== actor.goal_id
+ || window.agent_id !== actor.agent_id || todo.todo_id !== request.todo_id
+ || todo.target_key !== window.window_id || todo.claimed_by !== actor.agent_id
+ || todo.action_kind !== "generate_periodic_report" || !(todo.status === "open" && !recovery || settlementOnly)) {
+ throw new EffectRuntimeRequestError("report execution requires the original Session subscription, window and task");
+ }
+ } else if (params.report_settlement_recovery != null) {
+ throw new EffectRuntimeRequestError("report recovery belongs only to its original calendar report");
+ }
if (turn ? turn.session_id !== session.session_id || turn.turn_id !== session.active_turn_id
|| !["starting", "running"].includes(String(turn.status)) || turn.origin !== "native_loopx"
|| turn.loopx_execution !== true || JSON.stringify(turn.loopx_request) !== JSON.stringify(request)
@@ -88,6 +117,28 @@ export function admitManagedSessionTurn(params: JsonObject): JsonObject {
return {actor, workspace_path: goal.workspace_path, host: "codex-app-server", observation_only: observationOnly};
}
+/** Both original conversation hosts use the native journal's no-model recovery. */
+function reportSettlementOnly(params: JsonObject, request: JsonObject, todo: JsonObject): boolean {
+ if (params.report_settlement_recovery == null) return false;
+ const recovery = requireJsonObject(params.report_settlement_recovery, "native original report recovery");
+ const decision = requireJsonObject(recovery.recovery_decision, "native journal recovery decision");
+ const cachedGeneration = todo.status === "open" && recovery.journal_status === "failed"
+ && decision.action === "continue" && decision.resume_from === "validation"
+ && Array.isArray(recovery.completed_phases) && recovery.completed_phases.length === 2
+ && recovery.completed_phases[0] === "host_execute" && recovery.completed_phases[1] === "typed_result";
+ const completedGeneration = todo.status === "done" && todo.completion_turn_key === request.turn_instance_id
+ && (decision.action === "continue" && ["durable_writeback", "quota_spend", "scheduler_apply", "scheduler_ack"].includes(String(decision.resume_from))
+ || decision.action === "return_existing" && recovery.journal_status === "committed");
+ const accepted = recovery.schema_version === "loopx_turn_journal_inspection_v1" && recovery.ok === true
+ && recovery.goal_matches === true && recovery.owner_matches === true && recovery.turn_key_matches === true
+ && recovery.journal_consistent === true && decision.reinvoke_host === false
+ && (cachedGeneration || completedGeneration);
+ if (!accepted || params.preparing_model_context === true) {
+ throw new EffectRuntimeRequestError("original report recovery requires validated remaining effects without a model");
+ }
+ return true;
+}
+
export const STEWARD_MANAGED_ACTOR_SCHEMA = "loopx_steward_managed_actor_v0";
const AGENT_ID = "steward";
@@ -202,24 +253,7 @@ export function admitStewardManagedTurn(params: JsonObject): JsonObject {
const todo = requireJsonObject(params.report_todo, "original report Todo");
const recovery = params.report_settlement_recovery == null ? null
: requireJsonObject(params.report_settlement_recovery, "native original report recovery");
- let settlementOnly = false;
- if (recovery) {
- const decision = requireJsonObject(recovery.recovery_decision, "native journal recovery decision");
- const cachedGeneration = todo.status === "open" && recovery.journal_status === "failed"
- && decision.action === "continue" && decision.resume_from === "validation"
- && Array.isArray(recovery.completed_phases) && recovery.completed_phases.length === 2
- && recovery.completed_phases[0] === "host_execute" && recovery.completed_phases[1] === "typed_result";
- const completedGeneration = todo.status === "done" && todo.completion_turn_key === request.turn_instance_id
- && (decision.action === "continue" && ["durable_writeback", "quota_spend", "scheduler_apply", "scheduler_ack"].includes(String(decision.resume_from))
- || decision.action === "return_existing" && recovery.journal_status === "committed");
- settlementOnly = recovery.schema_version === "loopx_turn_journal_inspection_v1" && recovery.ok === true
- && recovery.goal_matches === true && recovery.owner_matches === true && recovery.turn_key_matches === true
- && recovery.journal_consistent === true && decision.reinvoke_host === false
- && (cachedGeneration || completedGeneration);
- if (!settlementOnly || params.preparing_model_context === true) {
- throw new EffectRuntimeRequestError("original report recovery requires validated remaining effects without a model");
- }
- }
+ const settlementOnly = reportSettlementOnly(params, request, todo);
if (request.validation_command === null || subscription.enabled !== true || subscription.profile_preset !== "portfolio-progress"
|| subscription.route_ref !== `steward-private:${current.id}:${current.source}`
|| subscription.effective_revision !== request.report_subscription_revision
diff --git a/tests/control_plane_ts/steward_reports.test.ts b/tests/control_plane_ts/steward_reports.test.ts
index 7db310c716..31ffa96aab 100644
--- a/tests/control_plane_ts/steward_reports.test.ts
+++ b/tests/control_plane_ts/steward_reports.test.ts
@@ -59,6 +59,55 @@ test("local native work observation excludes watch bookkeeping and retains real
assert.throws(() => observeManagedWork({...source, todos: undefined}));
});
+test("a local calendar report retains its exact original Session, subscription, window and native Task", () => {
+ const goal = {goal_id: "project", goal_instance_id: "original", registered_agents: ["writer", "peer"], workspace_path: "/workspace"};
+ const session = {goal_id: goal.goal_id, goal_instance_id: goal.goal_instance_id, session_id: "a".repeat(32),
+ channel_id: "agent.writer", session_mode: "managed_runtime", managed_agent_id: "writer", agent_id: "codex",
+ executor_endpoint_id: "codex", adapter_kind: "codex_app_server", managed_workspace_path: goal.workspace_path};
+ const actor = {schema_version: "loopx_managed_session_actor_v0", goal_id: goal.goal_id, goal_instance_id: goal.goal_instance_id,
+ session_id: session.session_id, agent_id: "writer", workspace_path: goal.workspace_path};
+ const request = {operation: "native_turn", host: "codex-app-server", work_kind: "report", actor,
+ turn_instance_id: "managed-session.report.original", todo_id: "todo_report", validation_command: null,
+ report_window_id: "original-window", report_subscription_revision: "original-subscription"};
+ const subscription = {enabled: true, profile_preset: "weekly-progress", route_ref: `managed-session:${session.session_id}`,
+ effective_revision: request.report_subscription_revision};
+ const window = {goal_id: goal.goal_id, agent_id: "writer", window_id: request.report_window_id,
+ subscription_revision: request.report_subscription_revision};
+ const todo = {todo_id: request.todo_id, target_key: window.window_id, action_kind: "generate_periodic_report", claimed_by: "writer", status: "open"};
+ const input = {session, goal, request, goal_active: true, queued_turn_count: 0,
+ report_window: window, report_subscription: subscription, report_todo: todo};
+ assert.equal(admitManagedSessionTurn(input).observation_only, false);
+ for (const changed of [
+ {...input, goal_active: false}, {...input, queued_turn_count: 1},
+ {...input, session: {...session, managed_agent_id: "peer"}},
+ {...input, session: {...session, session_mode: "attached_host"}},
+ {...input, session: {...session, goal_instance_id: "replacement"}},
+ {...input, report_window: {...window, goal_id: "another"}},
+ {...input, report_window: {...window, agent_id: "peer"}},
+ {...input, report_window: {...window, window_id: "next-window"}},
+ {...input, report_subscription: {...subscription, enabled: false}},
+ {...input, report_subscription: {...subscription, effective_revision: "changed"}},
+ {...input, report_subscription: {...subscription, profile_preset: "portfolio-progress"}},
+ {...input, report_subscription: {...subscription, route_ref: "managed-session:" + "b".repeat(32)}},
+ {...input, report_todo: {...todo, target_key: "another-window"}},
+ {...input, report_todo: {...todo, claimed_by: "peer"}},
+ {...input, report_todo: {...todo, status: "done"}},
+ {...input, request: {...request, validation_command: ["a-caller-command"]}},
+ {...input, request: {...request, profile: "another-runtime"}},
+ {...input, request: {...request, work_kind: "native_turn"}},
+ ]) assert.throws(() => admitManagedSessionTurn(changed));
+ const inspection = {schema_version: "loopx_turn_journal_inspection_v1", ok: true,
+ goal_matches: true, owner_matches: true, turn_key_matches: true, journal_consistent: true,
+ journal_status: "failed", completed_phases: ["host_execute", "typed_result"],
+ recovery_decision: {action: "continue", resume_from: "validation", reinvoke_host: false}};
+ assert.equal(admitManagedSessionTurn({...input, report_settlement_recovery: inspection}).observation_only, false);
+ for (const changed of [{owner_matches: false}, {journal_consistent: false}, {completed_phases: ["host_execute"]},
+ {recovery_decision: {...inspection.recovery_decision, reinvoke_host: true}},
+ {recovery_decision: {...inspection.recovery_decision, resume_from: "host_execute"}}]) {
+ assert.throws(() => admitManagedSessionTurn({...input, report_settlement_recovery: {...inspection, ...changed}}));
+ }
+});
+
function original() {
const binding = {binding_id: "b".repeat(24), transport_ref: "private-steward", provider_ref: "c".repeat(24),
operator_ref: "d".repeat(24), project_ref: "a".repeat(24), context_kind: "steward",
diff --git a/tests/test_native_steward_reports.py b/tests/test_native_steward_reports.py
index a78b9ae485..7ce91ff1c8 100644
--- a/tests/test_native_steward_reports.py
+++ b/tests/test_native_steward_reports.py
@@ -22,6 +22,232 @@
steward = steward_fixture
+def configured_local_report(steward, tmp_path, monkeypatch, provider="sqlite", start_date="2001-01-01"):
+ """An ordinary local Goal/Agent Session, with no report-enabled private binding."""
+ from loopx.agent_registry import load_goal_from_registry
+ from loopx.configure_goal import configure_goal
+ from loopx.capabilities.native_chat.steward_reports import ManagedSessionReports
+ from test_native_steward_turn_host import cli
+ isolate_sqlite_runtime(tmp_path, monkeypatch)
+ store, runtime, _, _, binding, capture, _ = steward
+ sid, managed = original(steward)
+ storage(runtime, provider)
+ plan = managed.prepare(binding["binding_id"], session_id=sid)["plan"]
+ actor = managed.prepare(binding["binding_id"], session_id=sid,
+ expected_plan_id=plan["plan_id"], execute=True)["actor"]
+ workspace = Path(load_goal_from_registry(runtime.registry_path, actor["goal_id"])["repo"])
+ session, _ = runtime.open_session(goal_id=actor["goal_id"], agent_id="codex", work_dir=workspace,
+ objective="Report this Goal's original evidence", channel_id=f"goal.{actor['goal_id']}",
+ mode="new", managed_agent_id=actor["agent_id"])
+ task = cli(runtime, "todo", "add", "--goal-id", actor["goal_id"], "--role", "agent",
+ "--claimed-by", actor["agent_id"], "--text", "Keep this original work and its evidence visible.",
+ "--action-kind", "inspect", "--execute")
+ schedule = {"schema_version": "periodic_report_schedule_v0", "schedule_id": "local-report",
+ "timezone": "UTC", "rrule": "FREQ=DAILY;BYHOUR=9;BYMINUTE=0", "start_date": start_date}
+ config = {"enabled": True, "profile_preset": "weekly-progress", "route_ref": "managed-session:" + session["session_id"],
+ "timezone": "UTC", "schedule": schedule}
+ assert configure_goal(registry_path=runtime.registry_path, goal_id=actor["goal_id"],
+ periodic_report_configuration=config, execute=True)["ok"]
+ return session, ManagedSessionReports(runtime), config, task["todo_id"]
+
+
+@pytest.mark.parametrize("provider", ["file", "sqlite"])
+def test_local_agent_calendar_report_uses_original_worker_and_verified_core_return(steward, tmp_path, monkeypatch, provider):
+ from loopx.capabilities.native_chat.steward_monitor import StewardMonitor
+ from loopx.capabilities.native_chat.steward_managed_mode import ManagedSessionTurn
+ session, reports, _, task_id = configured_local_report(steward, tmp_path, monkeypatch, provider)
+ store, runtime, bot, _, _, capture, _ = steward
+ sid = session["session_id"]
+ with monkeypatch.context() as changed_source:
+ changed_source.setattr(inspect, "getsource", lambda *_: "not the imported model callable")
+ install_model(steward, tmp_path, sid)
+ # This local report neither consumes nor borrows the private provider.
+ runtime.project_contexts.conversation_bindings = None
+ before, calls, writes = store.messages(sid), capture.read_bytes(), list(bot.writes)
+ turns = pump_steward_monitors(runtime)
+ assert len(turns) == 1
+ result = finish(runtime, turns[0])
+ assert result["status"] == "completed", result
+ native = result["response"]["native_loopx"]
+ assert native["status"] == "committed", native
+ assert result["response"]["periodic_report"]["delivery_verified"]
+ window = read_cadence_journal(runtime_root=reports.root, goal_id=session["goal_id"])["window"]
+ folder = attempt_directory(reports.root, window)
+ frozen = json.loads((folder / "editorial-request.json").read_text())
+ source = json.loads((folder / "source.json").read_text())
+ assert frozen["destination"] == "original_managed_session_full_body"
+ assert source["goal_id"] == session["goal_id"] and source["complete_native_task_read"]
+ assert task_id in {row["todo_id"] for row in source["rows"]}
+ assert frozen["actual_work_window"]["end_at"] == window["due_at"]
+ assert datetime.fromisoformat(window["due_at"]) - datetime.fromisoformat(window["start_at"]) == timedelta(days=1)
+ body = (folder / "report.md").read_text()
+ messages = store.messages(sid)
+ assert messages[:len(before)] == before
+ returned = [row for row in messages if row["role"] == "agent" and row["text"] == body]
+ assert len(returned) == 1 and returned[0]["turn_id"] == result["turn_id"]
+ cursor = read_periodic_report_publication_cursor(runtime_root=reports.root, goal_id=session["goal_id"], agent_id=session["managed_agent_id"])
+ assert cursor["generation_id"] == validate_private_generation(folder)["generation_id"]
+ assert cursor["covered_until"] == window["due_at"] and reports.health(sid)["publication_verified"]
+ rows = StewardMonitor(runtime).todos(result["loopx_request"]["actor"])
+ assert next(row for row in rows if row["todo_id"] == result["loopx_request"]["todo_id"])["status"] == "done"
+ assert next(row for row in rows if row["todo_id"] == task_id)["status"] == "open"
+ assert bot.writes == writes
+ after_calls = capture.read_bytes()
+ assert len([json.loads(line) for line in after_calls[len(calls):].splitlines()
+ if json.loads(line).get("method") == "turn/start"]) == 1
+ assert not pump_steward_monitors(runtime)
+ assert reports.publish(sid, window)
+ assert store.messages(sid) == messages and capture.read_bytes() == after_calls
+ inspected = ManagedSessionTurn(runtime).inspect(sid)
+ assert inspected["report"]["publication_verified"] and inspected["available"]
+ assert inspected["report"]["report_message_id"] == returned[0]["message_id"]
+ # A publication cursor alone cannot offer a body that is no longer readable.
+ with monkeypatch.context() as unreadable_body:
+ unreadable_body.setattr(store, "messages", lambda *_: [row for row in messages
+ if row["message_id"] != returned[0]["message_id"]])
+ unavailable = reports.health(sid)
+ assert not unavailable["publication_verified"] and unavailable["report_message_id"] is None
+
+
+@pytest.mark.parametrize("provider", ["file", "sqlite"])
+def test_local_agent_report_before_first_deadline_is_effect_free(steward, tmp_path, monkeypatch, provider):
+ session, reports, _, _ = configured_local_report(steward, tmp_path, monkeypatch, provider, start_date="2999-01-01")
+ store, runtime, bot, *_ = steward
+ before = store.messages(session["session_id"]), list(bot.writes)
+ assert not pump_steward_monitors(runtime)
+ assert read_cadence_journal(runtime_root=reports.root, goal_id=session["goal_id"]) is None
+ assert (store.messages(session["session_id"]), list(bot.writes)) == before
+
+
+@pytest.mark.parametrize("provider", ["file", "sqlite"])
+@pytest.mark.parametrize("lost_after_commit", [False, True])
+def test_local_report_restart_reads_back_unknown_publication_without_model_or_duplicate(steward, tmp_path, monkeypatch, provider, lost_after_commit):
+ from loopx.capabilities.periodic_report import incremental
+ from loopx.chat_store import ChatSessionStore
+ from loopx.chat_runtime import ChatRuntimeController
+ session, reports, _, _ = configured_local_report(steward, tmp_path, monkeypatch, provider)
+ store, runtime, bot, *_ = steward
+ sid = session["session_id"]
+ install_model(steward, tmp_path, sid)
+ runtime.project_contexts.conversation_bindings = None
+ commit = incremental.commit_periodic_report_publication_cursor
+ def interrupted_commit(**kwargs):
+ if lost_after_commit:
+ commit(**kwargs)
+ raise OSError("Synthetic publication reply lost after the original Core message")
+ with monkeypatch.context() as failure:
+ failure.setattr(incremental, "commit_periodic_report_publication_cursor", interrupted_commit)
+ first = finish(runtime, pump_steward_monitors(runtime)[0])
+ assert first["status"] == "failed"
+ window = read_cadence_journal(runtime_root=reports.root, goal_id=session["goal_id"])["window"]
+ folder = attempt_directory(reports.root, window)
+ body = (folder / "report.md").read_text()
+ original_turn = store.load_turn(sid, first["turn_id"])
+ assert len([row for row in store.messages(sid) if row["text"] == body]) == 1
+ calls, writes = steward[5].read_bytes(), list(bot.writes)
+ runtime.close()
+ restarted = ChatRuntimeController(store=ChatSessionStore(store.root.parent), codex_bin=runtime.codex_bin,
+ registry_path=runtime.registry_path, project_contexts=runtime.project_contexts)
+ try:
+ assert not pump_steward_monitors(restarted)
+ assert store.load_turn(sid, first["turn_id"]) == original_turn
+ assert len([row for row in store.messages(sid) if row["text"] == body]) == 1
+ cursor = read_periodic_report_publication_cursor(runtime_root=reports.root,
+ goal_id=session["goal_id"], agent_id=session["managed_agent_id"])
+ assert cursor["generation_id"] == validate_private_generation(folder)["generation_id"]
+ assert steward[5].read_bytes() == calls and bot.writes == writes
+ finally:
+ restarted.close()
+
+
+@pytest.mark.parametrize("provider", ["file", "sqlite"])
+def test_local_frozen_report_recovers_only_native_validation_and_returns_full_body(steward, tmp_path, monkeypatch, provider):
+ from loopx.capabilities.native_chat.steward_turn import StewardTurnHost
+ session, reports, _, _ = configured_local_report(steward, tmp_path, monkeypatch, provider)
+ store, runtime, bot, *_ = steward
+ sid = session["session_id"]
+ install_model(steward, tmp_path, sid)
+ runtime.project_contexts.conversation_bindings = None
+ with monkeypatch.context() as failure:
+ failure.setattr(StewardTurnHost, "_report_validator", lambda *_: lambda *_: {
+ "status": "failed", "validator_kind": "callback", "summary": "Synthetic validator unavailable", "recovery_kind": "repair_required"})
+ original = finish(runtime, pump_steward_monitors(runtime)[0])
+ assert original["response"]["native_loopx"]["result_kind"] == "validation_failed"
+ window = read_cadence_journal(runtime_root=reports.root, goal_id=session["goal_id"])["window"]
+ folder = attempt_directory(reports.root, window)
+ calls, messages, writes = steward[5].read_bytes(), store.messages(sid), list(bot.writes)
+ assert not any(row["text"] == (folder / "report.md").read_text() for row in messages)
+ runtime._steward_monitor_next_check.clear()
+ assert not pump_steward_monitors(runtime)
+ assert store.load_turn(sid, original["turn_id"]) == original
+ assert store.messages(sid)[:len(messages)] == messages
+ assert len([row for row in store.messages(sid) if row["text"] == (folder / "report.md").read_text()]) == 1
+ assert reports.health(sid)["publication_verified"]
+ assert steward[5].read_bytes() == calls and bot.writes == writes
+
+
+@pytest.mark.parametrize("fault", ["paused", "unscheduled", "wrong_session", "wrong_profile", "unregistered", "busy"])
+def test_local_report_denied_sources_never_launch_an_alternative_worker(steward, tmp_path, monkeypatch, fault):
+ from loopx.configure_goal import configure_goal
+ session, reports, config, _ = configured_local_report(steward, tmp_path, monkeypatch)
+ store, runtime, bot, *_ = steward
+ before = store.messages(session["session_id"]), steward[5].read_bytes(), list(bot.writes)
+ if fault in {"paused", "unscheduled", "wrong_session", "wrong_profile"}:
+ changed = {**config, **({"enabled": False} if fault == "paused" else
+ {"schedule": None} if fault == "unscheduled" else
+ {"route_ref": "managed-session:" + "b" * 32} if fault == "wrong_session" else
+ {"profile_preset": "portfolio-progress", "route_ref": "another-original-route"})}
+ assert configure_goal(registry_path=runtime.registry_path, goal_id=session["goal_id"],
+ periodic_report_configuration=changed, execute=True)["ok"]
+ elif fault == "unregistered":
+ assert configure_goal(registry_path=runtime.registry_path, goal_id=session["goal_id"],
+ registered_agents=["another-agent"], execute=True)["ok"]
+ else:
+ store.accept_managed_turn(session["session_id"], client_turn_id="original-busy-user-work",
+ message="Keep this user's current work", origin="web")
+ before = store.messages(session["session_id"]), steward[5].read_bytes(), list(bot.writes)
+ assert not pump_steward_monitors(runtime)
+ assert (store.messages(session["session_id"]), steward[5].read_bytes(), list(bot.writes)) == before
+ assert read_cadence_journal(runtime_root=reports.root, goal_id=session["goal_id"]) is None
+ if fault == "unscheduled":
+ assert reports.health(session["session_id"])["enabled"]
+ assert reports.health(session["session_id"])["scheduled"] is False
+
+
+def test_local_report_configuration_retains_frozen_window_and_original_agent_election(steward, tmp_path, monkeypatch):
+ from loopx.capabilities.native_chat.steward_reports import session_report_configuration_guard
+ from loopx.capabilities.periodic_report.cadence_runtime import periodic_report_cadence_hooks
+ from loopx.capabilities.periodic_report.pending_intent import consume_pending_periodic_report_intent
+ from loopx.capabilities.native_chat.steward_turn import StewardTurnHost
+ from loopx.configure_goal import configure_goal
+ session, reports, config, _ = configured_local_report(steward, tmp_path, monkeypatch)
+ store, runtime, *_ = steward
+ # Another Agent's ordinary CLI guard cannot elect itself for this route.
+ assert configure_goal(registry_path=runtime.registry_path, goal_id=session["goal_id"],
+ registered_agents=["first-registration", session["managed_agent_id"]], execute=True)["ok"]
+ assert not periodic_report_cadence_hooks(registry_path=runtime.registry_path, runtime_root=reports.root,
+ goal_id=session["goal_id"], agent_id="first-registration")
+ assert read_cadence_journal(runtime_root=reports.root, goal_id=session["goal_id"]) is None
+ install_model(steward, tmp_path, session["session_id"])
+ # Retain a generated but uncommitted window under its real native journal.
+ with monkeypatch.context() as failure:
+ failure.setattr(StewardTurnHost, "_report_validator", lambda *_: lambda *_: {
+ "status": "failed", "validator_kind": "callback", "summary": "Synthetic validator unavailable", "recovery_kind": "repair_required"})
+ original = finish(runtime, pump_steward_monitors(runtime)[0])
+ assert original["response"]["native_loopx"]["result_kind"] == "validation_failed"
+ for changed in [None, {**config, "route_ref": "managed-session:" + "b" * 32},
+ {**config, "schedule": {**config["schedule"], "rrule": "FREQ=DAILY;BYHOUR=10;BYMINUTE=0"}}]:
+ with pytest.raises((ValueError, KeyError)):
+ with session_report_configuration_guard(runtime, session["goal_id"], changed):
+ pytest.fail("a frozen pending report must not be stranded by a settings edit")
+ before = runtime.registry_path.read_bytes()
+ with session_report_configuration_guard(runtime, session["goal_id"], {**config, "enabled": False}):
+ pass
+ assert runtime.registry_path.read_bytes() == before
+ assert consume_pending_periodic_report_intent(registry_path=runtime.registry_path, runtime_root=reports.root,
+ goal_id=session["goal_id"], agent_id=session["managed_agent_id"], execute=True)["status"] == "original_conversation_required"
+
+
def configured(steward, tmp_path, monkeypatch, provider="sqlite", start_date="2001-01-01"):
isolate_sqlite_runtime(tmp_path, monkeypatch)
store, runtime, _, transport, binding, capture, _ = steward
@@ -75,10 +301,15 @@ def fake_report_model(params):
"editorial_response": response}, ensure_ascii=False)
+# Keep the imported oracle stable if a developer edits this file during a run.
+# A late inspect.getsource can resolve its old line number into unrelated text.
+_REPORT_MODEL_SOURCE = inspect.getsource(fake_report_model)
+
+
def install_model(steward, tmp_path, sid, *, result_kind="validated_completion"):
_, runtime, _, _, _, _, _ = steward
helper = tmp_path / "report_model.py"
- helper.write_text(inspect.getsource(fake_report_model).replace(
+ helper.write_text(_REPORT_MODEL_SOURCE.replace(
'"result_kind": "validated_completion"', '"result_kind": ' + repr(result_kind)))
fake = Path(runtime.codex_bin)
source = fake.read_text().replace('"message": "Runtime response."',
From 6189b3d3fa10d6e432d5565ddc0d403b37ee8916 Mon Sep 17 00:00:00 2001
From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
Date: Sat, 24 Oct 2026 23:21:52 +0800
Subject: [PATCH 2/2] docs(report): show original session report settings and
full-body readback
Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
---
.../steward-reports/managed-session-README.md | 54 ++++++++++++++++++
.../session-before-desktop.png | Bin 0 -> 100922 bytes
.../steward-reports/session-before-narrow.png | Bin 0 -> 40722 bytes
.../steward-reports/session-body-desktop.png | Bin 0 -> 182593 bytes
.../steward-reports/session-body-narrow.png | Bin 0 -> 82064 bytes
.../session-delivered-desktop.png | Bin 0 -> 107339 bytes
.../session-delivered-narrow.png | Bin 0 -> 48429 bytes
.../session-paused-desktop.png | Bin 0 -> 100885 bytes
.../session-unknown-narrow.png | Bin 0 -> 46169 bytes
docs/product/periodic-reports.md | 6 +-
10 files changed, 57 insertions(+), 3 deletions(-)
create mode 100644 docs/assets/personal-workspace/steward-reports/managed-session-README.md
create mode 100644 docs/assets/personal-workspace/steward-reports/session-before-desktop.png
create mode 100644 docs/assets/personal-workspace/steward-reports/session-before-narrow.png
create mode 100644 docs/assets/personal-workspace/steward-reports/session-body-desktop.png
create mode 100644 docs/assets/personal-workspace/steward-reports/session-body-narrow.png
create mode 100644 docs/assets/personal-workspace/steward-reports/session-delivered-desktop.png
create mode 100644 docs/assets/personal-workspace/steward-reports/session-delivered-narrow.png
create mode 100644 docs/assets/personal-workspace/steward-reports/session-paused-desktop.png
create mode 100644 docs/assets/personal-workspace/steward-reports/session-unknown-narrow.png
diff --git a/docs/assets/personal-workspace/steward-reports/managed-session-README.md b/docs/assets/personal-workspace/steward-reports/managed-session-README.md
new file mode 100644
index 0000000000..3290f11bc4
--- /dev/null
+++ b/docs/assets/personal-workspace/steward-reports/managed-session-README.md
@@ -0,0 +1,54 @@
+# Existing Agent Session calendar report journey
+
+The `session-*` captures use the packaged frontend and real local HTTP, Core,
+native SQLite Task/Turn and periodic-report owners. Model and provider
+transports, Goal and conversation data are synthetic. No production account,
+message, workspace or report is shown.
+
+- Baseline frontend: `c1a589aac771b39df9137dc808280c2a33fb4cbd`.
+- Changed source and packaged runtime: `1fe1582cf8ad136b0df0f0c76d1e86cfc3232849`.
+- Settings desktop: 1200×888; narrow settings/report: 390×844 viewport emulation.
+ This is browser evidence, not physical-phone acceptance.
+
+The primary question is where this Goal's scheduled report will arrive.
+The selector replaces a raw route with an existing bound Agent conversation.
+The default view shows saved scheduling, honest execution/delivery state and
+the verified full-report link. Profile, timezone and the existing calendar
+remain together; optional general capability help stays on demand. The
+shared settings panel still needs vertical scrolling to reach save controls
+at these viewport heights; that broader layout is outside this change.
+
+The journey verifies:
+
+1. Save an enabled future first date; the sole existing wake owner makes no
+ model request before the deadline.
+2. Preview/apply an eligible calendar in the same original Session. The
+ existing worker calls the synthetic model once and settles one native
+ report generation. An exact full-body Core readback precedes cursor commit.
+3. Reload shows verified delivery. The existing full-answer viewer opens that
+ original message, including all five sections of the 27,750-character
+ synthetic report and the existing Copy Markdown control. The deliberately
+ repeated long content checks page composition, not authored-report quality.
+4. Blocking only the read-only report-health request produces the visible
+ unknown state and Read again action. Restoring that request reads the same
+ delivered body; it does not generate another report.
+5. Pause through the same preview/apply controls. The original body, task,
+ actor and history remain; the model-call count stays one.
+
+Separate packaged failure evidence and File/SQLite tests preserve invalid
+authored output, forbid stranding a frozen undelivered window and resume only
+eligible native settlement effects without model replay. This slice does not
+provide invalid-window cancellation, arbitrary profiles, natural-language
+authoring or the complete managed/attached Agent workspace.
+
+To run the disposable fixture:
+
+```sh
+uv run --extra test python examples/personal-workspace-browser/steward_report_fixture.py \
+ --managed-report --output-dir /tmp/loopx-managed-session-report-qa
+```
+
+Use its returned URL. The fixture's `start-existing-owners` control starts its
+single existing wake service. `--invalid-editorial` selects a known failed
+synthetic author response for failure inspection. Closing the fixture removes
+its disposable data; it does not alter an original service or external chat.
diff --git a/docs/assets/personal-workspace/steward-reports/session-before-desktop.png b/docs/assets/personal-workspace/steward-reports/session-before-desktop.png
new file mode 100644
index 0000000000000000000000000000000000000000..7541ae2690a5ad653b64abe6540c71bb3e752956
GIT binary patch
literal 100922
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