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
4 changes: 3 additions & 1 deletion apps/presentation/dashboard/src/data/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2772,6 +2772,7 @@ const privateConversationSchema = z.object({
]).optional(),
managed_steward: z.discriminatedUnion("available", [
z.object({available: z.literal(true), prepared: z.boolean(), scheduling_enabled: z.boolean(),
execution_available: z.boolean().optional(), execution_error_code: z.string().optional(),
goal_active: z.boolean().optional(), monitor: z.object({todo_id: z.string(), status: z.string(),
cadence_minutes: z.number().int().nullable(), next_due_at: z.string().nullable(),
last_checked_at: z.string().nullable()}).nullable().optional(),
Expand All @@ -2784,7 +2785,8 @@ const privateConversationSchema = z.object({
z.object({available: z.literal(false), error_code: z.string()}),
]).optional(),
goal_id: z.string().optional(), agent_id: z.string().optional(), session_id: z.string().optional()}),
z.object({available: z.literal(false), error_code: z.string()}),
z.object({available: z.literal(false), error_code: z.string(),
execution_available: z.boolean().optional(), execution_error_code: z.string().optional()}),
]).optional(),
agent_candidates: z.array(privateAgentSessionSchema).default([]), agent_targets: z.array(privateAgentTargetSchema).default([]),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ export function PrivateConversationPanel({apps, onOpenOriginal}: {apps: LarkApp[
{row.recovery_count > 0 ? <p role="status">{zh ? "存在尚未确认的发送回执。服务会读取原回执恢复;不要重新发送同一任务。检查 App 登录、权限和原会话后刷新状态。" : "A send receipt is unconfirmed. The service reads the original receipt to recover; avoid resending the same task. Check this App login, permissions and original Session, then refresh status."}</p> : null}
{!row.context_available ? <p role="alert">{zh ? "工作区授权已失效;请恢复原工作区或重新选择。旧会话不会移到其它工作区。" : "The workspace grant is unavailable. Restore the original workspace or select a new one; the old Session will not move."}</p> : null}
{row.context_kind === "project" && row.audience !== "group" ? <PrivateAgentTargets row={row} revision={revision} zh={zh} busy={busy} act={act}/> : null}
{row.context_kind === "steward" && row.audience === "owner" && row.executor_endpoint_id === "codex"
{row.context_kind === "steward" && row.audience === "owner" && row.managed_steward !== undefined
? <StewardManagedMode state={row.managed_steward} zh={zh} busy={busy} available={row.context_available}
onPrepare={() => void act(() => prepareManagedSteward(row.binding_id))}
onMonitor={(operation, cadence) => void act(() => configureStewardMonitor(row.binding_id, operation, cadence))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,31 +15,37 @@ export function StewardManagedMode({state, zh, busy, available, onPrepare, onMon
const [cadence, setCadence] = useState(initial ?? 30);
useEffect(() => setCadence(initial ?? 30), [initial, monitorId]);
if (state === undefined) return null;
const executionAvailable = state.execution_available !== false;
const canConfigure = available && executionAvailable;
return <details className="personal-cadence-details">
<summary>{zh ? "原生周期监控" : "Native periodic monitoring"}</summary>
{!executionAvailable ? <p role="status">{zh
? "当前执行器不支持此原生周期任务;原计划、窗口和回执均保留,执行与修改暂不可用。"
: "The current executor does not support this native scheduled work. Original schedules, windows and receipts are retained; execution and changes are unavailable."}</p> : null}
{!state.available ? <p role="status">{zh ? "管家工作身份暂不可用;请检查原会话和连接后重试。" : "The steward identity is unavailable. Check the original conversation and connection, then retry."}</p>
: state.prepared ? <>
<p role="status">{state.scheduling_enabled
<p role="status">{!executionAvailable ? (zh ? "原监控配置已保留。" : "Original monitoring configuration retained.") : state.scheduling_enabled
? (zh ? "授权 Goal 状态监控已启用。" : "Authorized Goal monitoring is enabled.")
: state.monitor ? (zh ? "状态监控已暂停。" : "Goal monitoring is paused.")
: (zh ? "管家身份已准备,状态监控尚未启用。" : "The steward identity is prepared; monitoring is not enabled.")}</p>
<ContextHelp label={zh ? "状态检查范围" : "Status inspection scope"}><p>{zh ? "状态检查复用原生监控,不调用模型。" : "State checks use the native monitor without model calls."}</p></ContextHelp>
{state.goal_active === false ? <p role="status">{zh ? "管家的工作 Goal 已停止;恢复原 Goal 后才能继续监控。" : "The steward Goal is stopped. Resume that original Goal before monitoring."}</p> : null}
{state.monitor ? <p>{state.scheduling_enabled ? (zh ? "下次检查:" : "Next check: ") : (zh ? "保留的到期时间:" : "Retained deadline: ")}{state.monitor.next_due_at ? new Date(state.monitor.next_due_at).toLocaleString(zh ? "zh-CN" : "en-US") : (zh ? "未排期" : "Unscheduled")}
{state.monitor ? <p>{executionAvailable && state.scheduling_enabled ? (zh ? "下次检查:" : "Next check: ") : (zh ? "保留的到期时间:" : "Retained deadline: ")}{state.monitor.next_due_at ? new Date(state.monitor.next_due_at).toLocaleString(zh ? "zh-CN" : "en-US") : (zh ? "未排期" : "Unscheduled")}
<br/>{zh ? "上次检查:" : "Last check: "}{state.monitor.last_checked_at ? new Date(state.monitor.last_checked_at).toLocaleString() : (zh ? "尚未完成首次检查" : "No completed check yet")}</p> : null}
<div className="personal-cadence-fields personal-cadence-target"><label>{zh ? "检查周期(分钟)" : "Check interval (minutes)"}
<input type="number" min={30} max={1440} step={1} value={cadence} disabled={busy || !available || state.goal_active === false}
<input type="number" min={30} max={1440} step={1} value={cadence} disabled={busy || !canConfigure || state.goal_active === false}
aria-invalid={!Number.isInteger(cadence) || cadence < 30 || cadence > 1440}
onChange={event => setCadence(Number(event.target.value))}/>
</label></div>
<div className="personal-cadence-actions"><button className="is-primary" style={{minHeight: 44}} type="button" disabled={busy || !available || state.goal_active === false || !Number.isInteger(cadence) || cadence < 30 || cadence > 1440}
<div className="personal-cadence-actions"><button className="is-primary" style={{minHeight: 44}} type="button" disabled={busy || !canConfigure || state.goal_active === false || !Number.isInteger(cadence) || cadence < 30 || cadence > 1440}
onClick={() => onMonitor("enable", cadence)}>{state.scheduling_enabled ? (zh ? "更新周期" : "Update interval") : (zh ? "启用状态监控" : "Enable monitoring")}</button>
{state.monitor?.status === "open" ? <button style={{minHeight: 44}} type="button" disabled={busy || !available} onClick={() => onMonitor("pause", initial ?? 30)}>{zh ? "暂停状态监控" : "Pause monitoring"}</button> : null}</div>
<StewardReport state={state.report} zh={zh} busy={busy} available={available} goalActive={state.goal_active !== false} onChange={onReport}/>
{state.monitor?.status === "open" ? <button style={{minHeight: 44}} type="button" disabled={busy || !canConfigure} onClick={() => onMonitor("pause", initial ?? 30)}>{zh ? "暂停状态监控" : "Pause monitoring"}</button> : null}</div>
<StewardReport state={state.report} zh={zh} busy={busy} available={canConfigure}
executionAvailable={executionAvailable} goalActive={state.goal_active !== false} onChange={onReport}/>
</>
: <>
<p>{zh ? "为当前管家准备一个可见的 LoopX 工作身份,保留原对话和权限。此步骤不会启动定时任务或模型。" : "Prepare one visible LoopX identity for this steward, retaining its original conversation and permissions. This step starts neither scheduled tasks nor a model."}</p>
<button type="button" disabled={busy || !available} onClick={onPrepare}>
<button type="button" disabled={busy || !canConfigure} onClick={onPrepare}>
{zh ? "准备管家身份" : "Prepare steward identity"}
</button>
</>}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import "./steward-report.css";

type Managed = Extract<NonNullable<PrivateConversation["managed_steward"]>, {available: true}>;

export function StewardReport({state, zh, busy, available, goalActive, onChange}: {
export function StewardReport({state, zh, busy, available, executionAvailable = true, goalActive, onChange}: {
state: Managed["report"]; zh: boolean; busy: boolean; available: boolean; goalActive: boolean;
executionAvailable?: boolean;
onChange: (operation: "enable" | "pause" | "cancel", schedule?: Record<string, unknown>) => void;
}) {
const original = state?.available ? state.schedule : null;
Expand All @@ -21,17 +22,18 @@ export function StewardReport({state, zh, busy, available, goalActive, onChange}
const signature = JSON.stringify(original);
useEffect(() => {if (original) {setSchedule(original); setZone(String(original.timezone));}}, [signature]);
if (!state) return null;
const disabled = busy || !available;
const disabled = busy || !available || !executionAvailable;
let validZone = true;
try {new Intl.DateTimeFormat("en", {timeZone: zone}).format();} catch {validZone = false;}
const stamp = (value?: string | null) => value ? new Date(value).toLocaleString(zh ? "zh-CN" : "en-US") : (zh ? "暂无" : "None");
return <section className="personal-steward-report">
<h4>{zh ? "定期详细汇报" : "Detailed periodic reports"}</h4>
{!state.available ? <p role="status">{zh ? "汇报状态暂不可读;原计划和回执已保留。刷新连接后重试。" : "Report status is unavailable. The original schedule and receipts are retained; refresh the connection."}</p> : <>
<p role="status">{state.enabled ? (zh ? "已启用 · 完整中文正文回此私聊" : "Enabled · Full Chinese report returns to this private Chat")
<p role="status">{!executionAvailable ? (zh ? "原汇报计划已保留,当前无法运行。" : "Original report configuration retained; execution unavailable.")
: state.enabled ? (zh ? "已启用 · 完整中文正文回此私聊" : "Enabled · Full Chinese report returns to this private Chat")
: state.configured ? (zh ? "已暂停 · 原窗口和回执保留" : "Paused · Original windows and receipts retained")
: (zh ? "尚未启用" : "Not enabled")}</p>
<p>{zh ? "汇总全部获准本机 Goal,包括停止项和读取缺口。每天汇报前24小时,生成与实际送达分别核验;状态监控照常运行。" : "Covers every authorized local Goal, including stopped Goals and read gaps. Daily reports cover the previous 24 hours; generation and delivery are verified separately. Monitoring continues."}</p>
<p>{zh ? "汇总全部获准本机 Goal,包括停止项和读取缺口。每天汇报前24小时,生成与实际送达分别核验;监控与汇报分别配置。" : "Covers every authorized local Goal, including stopped Goals and read gaps. Daily reports cover the previous 24 hours; generation and delivery are verified separately. Monitoring and reports are configured separately."}</p>
{state.configured ? <p>{zh ? "下次日历计划:" : "Next calendar deadline: "}{stamp(state.next_due_at)}<br/>
{zh ? "最近窗口:" : "Latest window: "}{stamp(state.retained_window_due_at)} · {state.delivery_status === "delivered"
? (zh ? "原私聊全文已核验送达" : "Full private reply verified")
Expand Down
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.
23 changes: 23 additions & 0 deletions docs/product/periodic-reports.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,29 @@ 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.

Changing a previously prepared private steward to an executor without native
scheduled-work support keeps its original configuration readable in connection
settings. The monitor deadline, report schedule, retained window and delivery
state remain visible with an explicit execution-unavailable notice. Configuration
controls are disabled; inspecting retained state neither re-enables the old host
nor retries a failed window. Native admission still owns every execution and
configuration change. Restoring a supported executor restores the existing
configuration view rather than creating a replacement identity or subscription.

The disposable `steward_report_fixture.py --retained-executor` journey prepares
one original synthetic steward and calendar window. With baseline assets the
section is absent; current packaged assets expose the retained state at 1200
and 390 pixels. Its `restore-supported-executor` control restores the same
configuration. No wake owner is started, no model Turn is requested and no
provider write is added during inspection or restoration. This qualifies UI
readback and executor compatibility, not scheduled generation or live delivery.

| Baseline: retained section hidden | Current: original schedule remains readable |
| --- | --- |
| ![Baseline packaged settings hide the retained schedule](../assets/personal-workspace/steward-reports/retained-executor-before.png) | ![Current packaged settings retain the schedule with unavailable execution](../assets/personal-workspace/steward-reports/retained-executor-desktop.png) |

![The same retained schedule at 390 pixels](../assets/personal-workspace/steward-reports/retained-executor-narrow.png)

Markdown, the HTML report and its copied full text use the same source-link
rule. An unchanged, well-formed HTTP(S) address is a link. Opaque evidence
digests, native Task/artifact references and private relative paths stay source
Expand Down
28 changes: 28 additions & 0 deletions examples/personal-workspace-browser/steward_report_fixture.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,14 @@ 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("--host-failure", action="store_true", help="Fail the synthetic native model before an editorial result; retain the original window without retry authority.")
parser.add_argument("--retained-executor", action="store_true", help="Keep an original configured window after selecting an executor without native scheduled-work support; readback only.")
journey = parser.add_mutually_exclusive_group()
journey.add_argument("--managed-session", action="store_true", help="Exercise ordinary Goal Session native work with the existing task validator, instead of a scheduled report.")
journey.add_argument("--managed-report", action="store_true", help="Configure a calendar report for an existing local Agent Session through the packaged Goal settings.")
journey.add_argument("--managed-calendar-task", choices=("file", "sqlite"), help="Repeat a completed, independently validated Task through the real calendar and packaged Goal settings.")
args = parser.parse_args()
if args.retained_executor and (args.host_failure or args.invalid_editorial or args.managed_session or args.managed_report or args.managed_calendar_task):
parser.error("retained executor readback uses the original steward journey alone")
if args.host_failure and (args.invalid_editorial or args.managed_session or args.managed_calendar_task):
parser.error("select the pre-editorial report failure journey alone")
if args.invalid_editorial and (args.managed_session or args.managed_calendar_task):
Expand Down Expand Up @@ -117,6 +120,23 @@ def fail_report(_adapter, _prompt, event_sink):
if args.invalid_editorial:
helper = root / "report_model.py"
helper.write_text(helper.read_text().replace('"request_digest": request["request_digest"]', '"request_digest": "known-invalid"'))
if args.retained_executor:
from datetime import datetime, timezone
from loopx.capabilities.periodic_report.cadence_journal import admit_cadence_window
reports = StewardReports(runtime)
schedule = {"schema_version": "periodic_report_schedule_v0", "schedule_id": "retained-report",
"timezone": "UTC", "rrule": "FREQ=DAILY;BYHOUR=9;BYMINUTE=0", "start_date": "2001-01-01"}
plan = reports.configure(binding["binding_id"], operation="enable", schedule=schedule)["plan"]
reports.configure(binding["binding_id"], operation="enable", schedule=schedule,
execute=True, expected_plan_id=plan["plan_id"])
subscription = reports.subscription(binding["binding_id"])
admit_cadence_window(runtime_root=runtime.coordination_runtime_root, goal_id=actor["goal_id"],
agent_id=actor["agent_id"], subscription=subscription, now=datetime.now(timezone.utc),
subscription_resolver=lambda: subscription)
current = transport.bindings.read()
selected = next(row for row in current["bindings"] if row["binding_id"] == binding["binding_id"])
selected["executor_endpoint_id"] = "antigravity"
transport.bindings.path.write_text(json.dumps(current))
before_calls, before_messages = capture.read_bytes(), store.messages(sid)
server = ChatHTTPServer(("127.0.0.1", 0), ChatRequestHandler)
server.chat_store, server.runtime_controller = store, runtime
Expand Down Expand Up @@ -153,9 +173,16 @@ def fail_report(_adapter, _prompt, event_sink):
owner_start_count = 0
restart_tokens = set()
restart_refusals = set()
executor_restored = False
try:
while not stop.wait(.2):
server.assets_dir = current_assets if (output / "use-current-assets").exists() else baseline_assets
if args.retained_executor and not executor_restored and (output / "restore-supported-executor").exists():
current = transport.bindings.read()
selected = next(row for row in current["bindings"] if row["binding_id"] == binding["binding_id"])
selected["executor_endpoint_id"] = "codex"
transport.bindings.path.write_text(json.dumps(current))
executor_restored = True
provider.owner_ready = not (output / "provider-unavailable").exists()
owners_stopped = (output / "stop-existing-wake").exists()
if owners_stopped and wake:
Expand Down Expand Up @@ -235,6 +262,7 @@ def fail_report(_adapter, _prompt, event_sink):
"provider_history_pages": sum("+chat-messages-list" in call for call in provider.calls),
"existing_wake_owner_running": wake is not None,
"existing_wake_owner_starts": owner_start_count,
"retained_executor_restored": executor_restored,
"runtime_restarts": len(restart_tokens),
"runtime_restart_rejections": len(restart_refusals),
**({"managed_source_present": artifact.exists(),
Expand Down
Loading
Loading