Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -426,13 +427,18 @@ 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.`}
</p> : null}
{selected.capability_id === "periodic_report" && draft.profile_preset === "weekly-progress"
? <PeriodicReportSessionField goalId={goalId} route={String(draft.route_ref ?? "")}
zh={locale === "zh-CN"} disabled={editorBusy} revision={JSON.stringify(selected.current)}
onChange={value => changeDraft("route_ref", value)} /> : null}
<CapabilityConfigurationFields
disabled={editorBusy}
copy={localizedCapabilityFieldCopy(locale, localizedSelected.capability_id)}
editor={localizedSelected.configuration_editor}
advancedKeys={selected.capability_id === "todo_replan_cadence" ? ["planning_horizon_goal"] : []}
omitKeys={selected.capability_id === "reward_memory"
? ["agent_id", ...(draft.agent_id ? ["enabled_agents", ...(draft.enabled === false ? ["config_path"] : [])] : ["enabled"])]
: selected.capability_id === "periodic_report" && draft.profile_preset === "weekly-progress" ? ["route_ref"]
: []}
onChange={changeDraft}
value={draft}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import {useEffect, useId, useState} from "react";

type Session = {session_id: string; goal_id: string; managed_agent_id?: string; session_mode: string;
agent_id: string; status: string; title?: string; updated_at?: string};
type Report = {available: boolean; configured?: boolean; enabled?: boolean; scheduled?: boolean; next_due_at?: string | null;
retained_window_due_at?: string | null; generation_complete?: boolean; execution_failed?: boolean;
publication_verified?: boolean; report_message_id?: string | null};

async function read<T>(url: string, signal: AbortSignal): Promise<T> {
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<Session[]>([]);
const [loading, setLoading] = useState(true);
const [failed, setFailed] = useState(false);
const [retry, setRetry] = useState(0);
const [report, setReport] = useState<Report | null>(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 <div className="personal-capability-fields" style={{gridTemplateColumns: "minmax(0, 1fr)"}}>
<label htmlFor={id}><span>{zh ? "报告接收会话" : "Report conversation"}</span>
<select id={id} disabled={disabled || loading || failed} value={local ? route : route ? "other" : ""}
onChange={event => onChange(event.target.value === "other" ? (local ? "weekly-report" : route || "weekly-report") : event.target.value)}>
<option value="" disabled>{loading ? (zh ? "正在读取原会话…" : "Reading original conversations…")
: zh ? "选择现有 Agent 会话" : "Choose an existing Agent conversation"}</option>
{sessions.map(session => <option key={session.session_id} value={`managed-session:${session.session_id}`}>
{session.managed_agent_id} · {session.title || (zh ? "原会话" : "Original conversation")}
{session.updated_at ? ` · ${new Date(session.updated_at).toLocaleString(zh ? "zh-CN" : "en-US")}` : ""}
</option>)}
{local && !sessions.some(session => session.session_id === selectedId)
? <option value={route} disabled>{zh ? "原接收会话不可用" : "Original destination unavailable"}</option> : null}
<option value="other">{zh ? "其它已配置位置" : "Another configured destination"}</option>
</select>
</label>
{!local && route ? <label><span>{zh ? "现有报告位置" : "Existing report destination"}</span>
<input value={route} disabled={disabled} onChange={event => onChange(event.target.value)} />
</label> : null}
{failed ? <span role="alert">{zh ? "无法读取原会话,未更换接收位置。" : "Original conversations could not be read; the destination is retained."}</span>
: !loading && !sessions.length ? <span role="status">{zh ? "此 Goal 尚无已绑定的 Agent 会话。" : "This Goal has no bound Agent conversation."}</span> : null}
{failed || reportFailed ? <button type="button" disabled={disabled} onClick={() => setRetry(value => value + 1)}>
{zh ? "重新读取" : "Read again"}</button> : null}
{reportFailed ? <span role="status">{zh ? "报告执行与送达状态暂时未知。" : "Report execution and delivery are currently unknown."}</span> : null}
{report ? <span role={report.available ? "status" : "alert"}>
{!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")}` : ""}
</span> : null}
{report?.publication_verified && report.report_message_id ? <a href={`/chat/?${new URLSearchParams({
reportSessionId: selectedId, reportMessageId: report.report_message_id,
})}`}>{zh ? "查看本期完整报告" : "Read this period’s full report"}</a> : null}
</div>;
}
Original file line number Diff line number Diff line change
@@ -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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
79 changes: 75 additions & 4 deletions docs/product/periodic-reports.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 **Copy Markdown**.
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:<original-session-id>` 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 |
| --- | --- |
| ![Previous report settings](../assets/personal-workspace/steward-reports/session-before-desktop.png) | ![Report returned to the selected original conversation](../assets/personal-workspace/steward-reports/session-delivered-desktop.png) |

| Temporarily unknown delivery state, narrow | Paused configuration |
| --- | --- |
| ![Unknown report delivery state on a narrow viewport](../assets/personal-workspace/steward-reports/session-unknown-narrow.png) | ![Paused original report configuration](../assets/personal-workspace/steward-reports/session-paused-desktop.png) |

### Original private steward

The original-Session host bridge now runs a bounded native Turn through the
Expand Down
Loading
Loading