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
11 changes: 11 additions & 0 deletions apps/presentation/dashboard/src/data/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,17 @@ export const runGoalSchema = z.object({
goal_objective: z.string().nullable().optional().catch(null),
zcode_goal_eligible_agent_ids: z.array(z.string()).optional().default([]).catch([]),
acceptance_observation: goalAcceptanceObservationSchema.optional().nullable().catch(null),
host_worker_observations: z.object({
goal_id: z.string(),
read_status: z.enum(["read", "stale", "unreadable"]),
rows: z.array(z.object({
agent_id: z.string(),
status: z.enum(["replan_paused", "replan_outcome_unknown"]),
obligation_id: z.string(),
turn_instance_id: z.string().nullable(),
observed_at: z.string(),
})),
}).optional().nullable().catch({goal_id: "", read_status: "unreadable", rows: []}),
id: z.string(),
activation_state: z.enum(["active", "stopped"]).optional().default("active"),
display_name: z.string().optional().nullable(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,12 @@
.goal-outcome .goal-overview-criteria li { grid-template-columns: minmax(0, 1fr); gap: 4px; }
.goal-outcome .goal-overview-criteria small { grid-column: 1; }
}
/* Driver observations remain separate from the Goal's activation and acceptance. */
.goal-overview-worker-state { display: grid; gap: 8px; padding-block: 16px; border-block: 1px solid var(--pw-line); }
.goal-overview-worker-state h3 { color: var(--pw-muted); font-size: var(--text-caption); font-weight: 500; line-height: var(--leading-caption); }
.goal-overview-worker-state p { margin-block: .5rem; overflow-wrap: anywhere; }
.goal-overview-worker-state details { margin-block: .5rem; }
.goal-overview-worker-state dl { color: var(--pw-muted); font-size: var(--text-caption); line-height: var(--leading-caption); }
.goal-overview-worker-state dt { font-weight: 600; }
.goal-overview-worker-state dd { margin: .2rem 0 .6rem; overflow-wrap: anywhere; }
.goal-overview-worker-state > button { justify-self: start; }
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export function GoalOverview({ active, goal, items, userTodos, readOnly, onOpenD
<p className="goal-overview-note">{goal.nextSentence}</p>
{goal.agentSentence && goal.agentSentence !== goal.nextSentence ? <details className="goal-overview-agent-note"><summary>{copy.agentSummary}</summary><p className="goal-overview-note">{goal.agentSentence}</p></details> : null}
</details>
<WorkerReadback goal={goal} zh={locale === "zh-CN"} onViewTasks={() => onView("tasks")}/>
<GoalAcceptanceContractSection overview goalId={goal.goalId} contract={observation?.goal_acceptance_contract} copy={acceptanceCopy} current={review.usable} localSource={!readOnly} canManage={!readOnly && active && review.usable}/>
{run ? <button className="goal-overview-run" type="button" onClick={() => onSelect({ kind: "run", item: run.run })}><small>{copy.execution} · {run.run.agentLabel}</small><ArrowRight aria-hidden size={15} /></button> : null}
</section>
Expand Down Expand Up @@ -113,3 +114,31 @@ export function GoalOverview({ active, goal, items, userTodos, readOnly, onOpenD
: <DeliveryReviewContent review={review} hideWorkMap hideAcceptanceContract active={active} goal={goal} items={items} userTodos={userTodos} onSelect={onSelect} />}
</section>;
}

function WorkerReadback({goal, zh, onViewTasks}: {goal: WorkspaceGoal; zh: boolean; onViewTasks: () => void}) {
const source = goal.workerObservations;
if (!source) return null;
return <section className="goal-overview-worker-state" aria-label={zh ? "外部执行器状态" : "External worker status"}>
<h3>{zh ? "执行器最近的观察" : "Last worker observation"}</h3>
{source.read_status !== "read" ? <p role="status">{zh
? "执行器记录缺失有效来源或与当前目标配置不一致。当前执行结果未知。"
: "The worker record is unreadable or does not match the current Goal configuration. Its current outcome is unknown."}</p> : null}
{source.rows.map(row => <div key={row.agent_id}>
<p><strong>{row.status === "replan_paused" ? (zh ? "需要方向,已暂停复核" : "Direction needed; review paused")
: (zh ? "原请求结果未知,已暂停新派发" : "Original outcome unknown; new dispatch paused")}</strong></p>
<p>{row.status === "replan_paused" ? (zh
? "本次复核没有解决原义务。补充具体任务或相关结果,再重启执行器以检查当前准入。"
: "The review did not resolve its original obligation. Add concrete work or relevant results, then restart the worker to check current admission.") : (zh
? "先核实原操作及其结算。已发送请求仍可能产生费用;新事件或重启不能证明原请求未执行。"
: "Check the original operation and its settlement first. An issued request may still incur costs; a new event or restart cannot prove it did not run.")}</p>
<p className="goal-overview-note">{zh ? "此记录不证明 Goal 达成;完整费用未知。" : "This record does not establish Goal completion. Complete costs are unknown."}</p>
<details><summary>{zh ? "原工作与观察时间" : "Original work and observation time"}</summary>
<dl><div><dt>Agent</dt><dd>{row.agent_id}</dd></div>
<div><dt>{zh ? "义务" : "Obligation"}</dt><dd>{row.obligation_id}</dd></div>
<div><dt>{zh ? "原回合" : "Original Turn"}</dt><dd>{row.turn_instance_id ?? (zh ? "未绑定,需原 Host 核实" : "Unbound; check the original Host")}</dd></div>
<div><dt>{zh ? "记录时间" : "Observed at"}</dt><dd><time dateTime={row.observed_at}>{row.observed_at}</time></dd></div></dl>
</details>
</div>)}
<button type="button" onClick={onViewTasks}>{zh ? "查看任务与依赖" : "View work and dependencies"}</button>
</section>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,14 @@ export type WorkspaceGoal = {
/** Bounded, safe text from this Goal's declared source document. */
objective?: string | null;
acceptanceObservation?: GoalAcceptanceObservation | null;
/** Driver readback only; it never changes Goal activation or grants work. */
workerObservations?: {
read_status: "read" | "stale" | "unreadable";
rows: Array<{
agent_id: string; status: "replan_paused" | "replan_outcome_unknown";
obligation_id: string; turn_instance_id: string | null; observed_at: string;
}>;
} | null;
loadState?: "loading" | "error";
loadError?: WorkspaceLoadError;
activationState: "active" | "stopped";
Expand Down
3 changes: 3 additions & 0 deletions apps/presentation/dashboard/src/views/dashboard-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1098,6 +1098,9 @@ function buildPersonalHomeModel(
})),
} : undefined,
acceptanceObservation: goal.acceptance_observation,
workerObservations: !goal.host_worker_observations ? null
: goal.host_worker_observations.goal_id === goal.id ? goal.host_worker_observations
: {read_status: "unreadable", rows: []},
goalId: goal.id,
parentGoalId: goal.parent_goal_id,
sourceAccess: goal.source_access ? {...goal.source_access, readOnly: true} : undefined,
Expand Down
91 changes: 89 additions & 2 deletions docs/integrations/runtime-connector-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,100 @@ observe-only polls retain their interval, and failed wakes retain error backoff.
Commands that finish without more useful work may opt into
`--wake-stop-exit-code <1..255>`: that explicit child exit ends the worker
normally, without Todo completion or quota spend. Omit the option to preserve
ordinary nonzero-exit failure/backoff behavior. Timeout and output-limit
failures cannot be converted into a normal stop by this option. The caller owns
ordinary nonzero-exit failure/backoff behavior outside automatic replan. Timeout
and output-limit failures cannot be converted into a normal stop by this option. The caller owns
the meaning of its chosen code; the generic worker does not parse provider
receipts or infer work completion from stdout.
Stop signals and subprocess cleanup keep their existing behavior. This adapter
does not override admission or reinterpret task eligibility.

### Persistent replan attempts and truthful readback

The same configured worker state route now has one live consumer and retains
automatic replan dispatch attempts across restarts. A successful child exit
does not resolve the original replan obligation: if fresh Core admission still
names that obligation, the worker reports `replan_paused`, leaves the Goal
incomplete, and does not dispatch another semantic review. Cadence resets,
new observation contexts and intervening independent work do not erase the
attempt. Original Todo lifecycle and receipt-bound settlement remain under
their existing owners; they are not another semantic replan opportunity.

An interrupted, timed-out or otherwise failed replan is retained as
`replan_outcome_unknown`. A new obligation ID cannot make that original work
safe to repeat. When the original admission actually supplied a settlement
identity and the configured Host preserved it, the worker reads that exact
Turn through the existing settlement owner. Only verified complete settlement
permits further current work. Pending recovery stays on its original identity.
An opaque unbound shell command supplies no such proof; use its original Host
recovery and preserve the worker state. The worker does not infer a Turn from
stdout or the latest log row, and deleting the state is not a recovery method.

The existing host-poll receipt also publishes the worker's last paused or
unknown observation in the Goal overview. It identifies the original Agent and
obligation, retains a real Turn only when supplied by admission, and links to
the Goal's tasks and dependencies. This observation is readback, not current
admission or proof that a provider stopped charging. The page explicitly keeps
complete costs unknown. A missing, corrupt or differently scoped association
cannot display the old operation as current. Reading the observation and
opening tasks do not dispatch work; the existing Stop control still uses the
Goal lifecycle owner.

Ordinary `--record-host-poll` distinguishes an absent receipt from an unreadable
one. It may create an absent receipt, but preserves unreadable JSON, UTF-8,
schema or Goal association bytes and reports the observation write as
unsuccessful while returning the current quota decision.
Readback therefore continues to show uncertainty after the next liveness poll.
An explicit worker observation can restore its known original attempt; clearing
an unreadable receipt cannot stand in for that evidence. This does not resolve
the attempt or authorize another dispatch.

New attempts also capture `source_goal_observation_digest`: a readback witness of the full
source Goal and its registry route. The writer and reader share the existing
source-to-global projection transformation, so synchronization metadata does
not invalidate an observation. A changed source, Goal instance, configuration
or runtime still makes it stale. The quota cache keeps its original full-row
digest. Older observations retain their original, narrower raw-registry check;
missing source proof is never reconstructed as current execution authority.

The shared projection owner also preserves `preserve_attention_override`
semantics: with the complete source Goal unchanged, mirror `synced_at` updates
and the merge owner's consumption of `clear_attention_override` do not invalidate
the observation. An older attempt can gain this stronger observation witness
only while its original raw registry proof still matches. Lost proof is not
guessed or rebound, and the durable attempt is never rewritten by this upgrade.

普通存活轮询可以创建缺失的回执,但不能覆盖无法读取的回执。损坏的 JSON、
UTF-8、schema 或 Goal 关联仍保留在原位置,观察写入明确失败,当前 quota 判断
正常返回,页面继续显示未知。原 worker 可从持久 attempt 恢复观测,
但这不证明原操作已经结束,
也不允许再次派发。
新观测绑定完整 source Goal 和原注册表路径,正常同步到 global 后仍可显示原
Agent、义务和观测时间。同步时间变化不改变该来源;来源、实例、配置或运行环境
真正变化时仍显示过期/未知。旧记录沿用原关联检查,不把缺失的来源证明补造成权限。
投影继续复用 `preserve_attention_override` 等原合并规则;完整 source Goal 未变时,
镜像的 `synced_at` 更新及合并过程中消费的 `clear_attention_override` 不使观察过期。
只有旧 attempt 的原 raw registry 证明仍匹配,才可补强它的观察来源;不猜测重绑
丢失的证明,也不因此改写持久派发身份。

Scope includes the registry, runtime, Goal and Agent. Exact GoalRef is retained
when supplied; executable legacy Goals keep their existing alias scope.
Agent spelling follows Core's canonical identity in admission matching, the
default state route, lock and observation. An accepted uppercase Agent alias
and its canonical spelling therefore resume the same attempt. The original
CLI input still goes to Core for its normal validation.
Missing or newly supplied lifecycle metadata cannot reset an old attempt.
Legacy alias reuse has no exact recreation guarantee. State corruption or
write failure refuses a new wake. Separate state routes and distributed Hosts
still require their existing shared execution/lease authority; the local lock
is not a distributed exactly-once guarantee.

This is a dispatch brake on existing replan work, not a new review model or
provider budget. The configured Host still owns provider retries and token
limits; `--wake-timeout-seconds` bounds this worker's process only when supplied,
and the process output byte limit is not a token allowance. Unknown in-flight
provider usage remains unknown. Ordinary admitted long work can use successive
wakes, and a real negative experiment remains evidence for the original owner.

## External Tool Extension Candidates

An MCP server can play two different roles around LoopX:
Expand Down
9 changes: 7 additions & 2 deletions examples/dashboard-goal-acceptance-browser-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ try {
await page.goto(`http://127.0.0.1:${port}/${packaged ? "chat/" : ""}?statusUrl=/status.json`, {waitUntil: "networkidle"});
if (viewport.width < 640) {
const sidebar = page.getByRole("button", {name: language === "en" ? "Open Goal navigation" : "打开 Goal 导航"});
if (await sidebar.isVisible()) await sidebar.click();
await sidebar.waitFor({state: "visible"});
await sidebar.click();
}
await page.locator(".personal-goal-link").first().click({timeout: 10000});
await page.getByRole("button", {name: language === "en" ? "Overview" : "概览", exact: true}).click();
Expand All @@ -58,7 +59,11 @@ try {
goal.acceptance_observation = observation;
await page.reload({waitUntil: "networkidle"});
const navigation = page.getByRole("button", {name: "Open Goal navigation"});
if (await navigation.isVisible()) await navigation.click();
// The progressive workspace can finish its network requests before
// React has restored the narrow navigation. Wait for the actual user
// control instead of treating one early visibility sample as final.
await navigation.waitFor({state: "visible"});
await navigation.click();
await page.locator(".personal-goal-link").first().click();
await page.getByRole("button", {name: "Overview", exact: true}).click();
const refreshedCard = page.locator(".personal-goal-acceptance");
Expand Down
2 changes: 2 additions & 0 deletions examples/personal-workspace-browser-smoke.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env node
import { selectScenarioShard } from "./personal-workspace-browser/shard.mjs";
import {researchWorkerReadbackScenario} from "./personal-workspace-browser/research-worker-readback.mjs";
import { zcodeGoalScenario } from "./personal-workspace-browser/zcode-goal.mjs";
import {goalWorkspaceCreateScenario} from "./personal-workspace-browser/goal-workspace-create.mjs";
import {agentModelSelectionScenario} from "./personal-workspace-browser/agent-model-selection.mjs";
Expand Down Expand Up @@ -102,6 +103,7 @@ scenarioCatalog.push(conversationImageRequestScenario);
scenarioCatalog.push(workspaceViewRecoveryScenario);
scenarioCatalog.push(monitorReadbackScenario);
scenarioCatalog.push(blockedReasonReadbackScenario);
scenarioCatalog.push(researchWorkerReadbackScenario);
scenarioCatalog.push(turnStepsScenario);
scenarioCatalog.push(goalWorkMapScenario);
scenarioCatalog.push(goalMastheadScenario);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Research worker readback: packaged first viewports

These screenshots use a disposable synthetic Goal and Agent. They contain no
live workspace, model session, private trace or user data.

- Before: packaged dashboard built from upstream
`82f9c09bd61e26e458cecfd4cffeab2350359381`.
- After: the candidate source in this change, with the packaged bundle's
source-file fingerprints verified against the checkout.
- Viewports: desktop `1512 × 982`; mobile `390 × 844`.
- Displayed locale: `zh-CN`. The same smoke also exercises `en` at both sizes.
- Browser: headless Chromium 151, with a CJK font installed.
- Preview boundary: locally served packaged dashboard; no hosted preview.

## Changed states

| State | Desktop | Mobile |
| --- | --- | --- |
| Before: no worker observation | [First viewport](research-worker-before-zh-CN-desktop.png) | [First viewport](research-worker-before-zh-CN-mobile.png) |
| Paused: the original replan obligation remains unresolved | [First viewport](research-worker-paused-zh-CN-desktop.png) | [First viewport](research-worker-paused-zh-CN-mobile.png) |
| Unknown: the original request requires reconciliation | [First viewport](research-worker-unknown-zh-CN-desktop.png) | [First viewport](research-worker-unknown-zh-CN-mobile.png) |

The observation sits in the existing Goal overview, before its acceptance and
work sections. It states the next useful action and keeps complete costs
unknown. Original operation details are expandable; the existing tasks link
and Goal Stop control remain available. It does not replace current admission
or claim that the Goal has finished.

## Reproduction and behavioral checks

Run the existing packaged browser entry point with a working Playwright browser:

```bash
LOOPX_PERSONAL_WORKSPACE_SCENARIO=research-worker-readback LOOPX_PERSONAL_WORKSPACE_PACKAGED=1 \
node examples/personal-workspace-browser-smoke.mjs
```

The scenario uses `tests/control_plane/research_worker_readback_fixture.py` to
produce an actual worker observation through the File provider and real CLI,
then reads it through the Chat HTTP API and packaged page. Both states, locales
and viewport sizes are exercised. Checks cover horizontal overflow, unexpected
browser errors, original identity details, and task navigation. Missing,
corrupt and differently scoped associations hide stale operation identity.
Reads and navigation leave canonical Goal state and dispatch counts unchanged;
the existing Stop action actually stops the synthetic Goal without another
worker dispatch.

An absent optional Agent endpoint may return its typed session-preparation
error; this is handled separately from business writes. The fixture does not
install a model provider, invoke a paid model or claim to measure complete
provider costs.
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
Loading