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
3 changes: 2 additions & 1 deletion apps/presentation/dashboard/src/data/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2668,14 +2668,15 @@ const exploreResultPageSchema = z.object({
node_id: z.string(), question: z.string(), scope: z.string(), agent_id: z.string(),
evidence_refs: z.array(z.string()), last_updated_at: z.string(),
measurements: z.unknown().transform(normalizeExploreMeasurements).optional(),
linked_todos_available: z.boolean().optional(),
linked_todos: z.array(z.object({
todo_id: z.string(), text: z.string(), status: z.string(), claimed_by: z.string(),
})),
})),
});
export type ExploreResultPage = z.infer<typeof exploreResultPageSchema>;
export async function fetchExploreResults(goalId: string, cursor?: string, readSourceUrl?: string) {
const query = new URLSearchParams({goal_id: goalId, ...(cursor ? {cursor} : {})});
const query = new URLSearchParams({goal_id: goalId, view: "findings", ...(cursor ? {cursor} : {})});
const page = exploreResultPageSchema.parse(await requestJson<unknown>(sourceReadApiUrl(readSourceUrl, `/api/chat/explore-results?${query}`)));
if (page.goal_id !== goalId) throw new Error("Explore result Goal mismatch");
return page;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ export function GoalExploreResults({goalId, zh, active = true, readSourceUrl}: {
<dt>{zh ? "记录标识" : "Record identifiers"}</dt><dd><code>{selected.finding_id} · {selected.node_id}</code></dd></dl>
{sections ? <details><summary>{zh ? "完整原文" : "Full original text"}</summary><p className="explore-finding-raw">{selected.summary}</p></details> : null}
</details>
<details className="explore-reader-details"><summary>{zh ? "关联任务" : "Linked tasks"} · {selected.linked_todos.length}</summary>
{selected.linked_todos.length ? <ul className="explore-linked-tasks">{selected.linked_todos.map(todo => <li key={todo.todo_id}>
<details className="explore-reader-details"><summary>{zh ? "关联任务" : "Linked tasks"} · {selected.linked_todos_available === false ? "—" : selected.linked_todos.length}</summary>
{selected.linked_todos_available === false ? <p>{zh ? "此来源暂不提供完整的任务关联读取。" : "Complete task associations are not available from this source."}</p> : selected.linked_todos.length ? <ul className="explore-linked-tasks">{selected.linked_todos.map(todo => <li key={todo.todo_id}>
<details><summary><span>{todo.text}</span><small>{statuses[todo.status] ?? todo.status} · {todo.claimed_by || "—"}</small></summary>
<p>{todo.text}</p><code>{todo.todo_id}</code></details>
</li>)}</ul> : <p>{zh ? "当前及已完成任务中未找到关联。" : "No links found in current or completed tasks."}</p>}
Expand Down
6 changes: 3 additions & 3 deletions examples/control_plane/quota-plan-smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
)


def assert_plan_shape(plan: dict, markdown: str | None = None) -> None:
def assert_plan_shape(plan: dict, markdown: str | None = None, *, expected_user_todos: int = 1) -> None:
eligible_ids = [item["goal_id"] for item in plan["groups"]["eligible"]]
operator_gate_ids = [item["goal_id"] for item in plan["groups"]["operator_gate"]]
throttled_ids = [item["goal_id"] for item in plan["groups"]["throttled"]]
Expand All @@ -73,7 +73,7 @@ def assert_plan_shape(plan: dict, markdown: str | None = None) -> None:
assert gated["quota"]["blocked_action_scope"] == "gated_delivery", gated
assert "operator_question" in gated, gated
if "user_todos" in gated:
assert gated["user_todos"]["open_count"] == 1, gated
assert gated["user_todos"]["open_count"] == expected_user_todos, gated
assert "throttled-half" not in eligible_ids, eligible_ids
if markdown is not None:
assert "next_automatic_turn=full-speed" in markdown, markdown
Expand Down Expand Up @@ -1425,7 +1425,7 @@ def main() -> int:
assert_slot_preview(build_quota_slot_preview(status_payload, goal_id="near-limit-half", slots=1))
with tempfile.TemporaryDirectory(prefix="loopx-quota-plan-smoke-") as tmp:
cli_plan, cli_markdown = run_cli_quota_plan(Path(tmp))
assert_plan_shape(cli_plan, cli_markdown)
assert_plan_shape(cli_plan, cli_markdown, expected_user_todos=0)
with tempfile.TemporaryDirectory(prefix="loopx-quota-should-run-smoke-") as tmp:
assert_throttled_cli_should_run(run_cli_throttled_should_run(Path(tmp)))
with tempfile.TemporaryDirectory(prefix="loopx-quota-slot-smoke-") as tmp:
Expand Down
8 changes: 8 additions & 0 deletions examples/control_plane/quota_plan_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,14 @@ def write_cli_fixture(root: Path, *, scoped_agents: bool = False) -> tuple[Path,
+ "\n",
encoding="utf-8",
)
# Production reads require canonical authority; the run-ledger operator
# gate remains independent of a manually authored user Todo.
from tests.control_plane.canonical_authority_fixture import initialize_canonical_authority
from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection
for record in registry_goals:
initialize_canonical_authority(runtime, record["id"],
build_todo_runtime_shadow_projection(goal_id=record["id"], todos=[], handoff_mode="soft_claim"),
state_path=project / record["state_file"], provider="sqlite")
return registry_path, runtime, project


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.
28 changes: 28 additions & 0 deletions examples/personal-workspace-browser/research-history.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -908,6 +908,34 @@ Path(sys.argv[1]).write_text(json.dumps({'goal_id':goal,'display_name':'Syntheti
await page.getByRole('button', {name: 'About deep analysis', exact: true}).click();
await page.getByText('This action reuses existing judgments without a model call; original usage stays with the previous version.', {exact: true}).waitFor();
await page.keyboard.press('Escape');
// A source can expose complete saved findings without a complete Task
// association endpoint. Unknown links must never be presented as zero.
await page.route("**/api/chat/explore-results?*", route => {
const query = new URL(route.request().url()).searchParams;
if (query.get("view") === "history") return route.fallback();
assert.equal(query.get("view"), "findings");
return route.fulfill({json: {ok: true, goal_id: query.get("goal_id"), total: 2, next_cursor: null,
items: [0, 1].map(index => ({finding_id: `saved-${index}`, node_id: "question", finding: `Saved observation ${index + 1}`,
question: "Does this observation support the original objective?", scope: "Fixed synthetic input",
summary: "The measured change did not improve the original objective. Keep the negative result and investigate the next unresolved question. " +
"The observation applies to the fixed input only; comparison with a different input remains unverified.",
status: "refuted", agent_id: "researcher", evidence_refs: ["artifact:fixed-input"],
last_updated_at: "2026-01-01T12:00:00Z", linked_todos: [], linked_todos_available: false}))}});
});
await page.getByText("Read saved findings", {exact: true}).click();
const sourceReader = page.getByRole("region", {name: "Explore evidence", exact: true});
const sourceBody = sourceReader.locator(".goal-team-result-reader");
await sourceBody.waitFor();
await sourceBody.getByText("Linked tasks · —", {exact: true}).click();
assert.match(await sourceBody.innerText(), /Complete task associations are not available/);
assert.doesNotMatch(await sourceBody.innerText(), /No links found/);
await page.setViewportSize({width: 1440, height: 1000});
await sourceReader.evaluate(element => element.scrollIntoView({block: "start"}));
await page.screenshot({path: resolve(outputDir, "source-saved-findings-desktop.png"), animations: "disabled"});
await page.setViewportSize({width: 390, height: 844});
await sourceBody.scrollIntoViewIfNeeded();
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1));
await page.screenshot({path: resolve(outputDir, "source-saved-findings-mobile.png"), animations: "disabled"});
assert.equal(analysisWrites, 0, "Cached, reload, switch and keyboard reading never request execution");
assert.equal(api.turnRequests.length, 0, "Reading the graph cannot start an Agent");
return {note: "Three unchanged real checkpoint histories through production HTTP + packaged UI; per-Goal complete pagination, genuine negative evidence, no invented edges, reload/back/forward, mobile and failed refresh. Separate synthetic coincident-target regression; no benchmark qualification or model dispatch.",
Expand Down
19 changes: 19 additions & 0 deletions loopx/control_plane/status/goal_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,25 @@ def history(self, goal_id, cursor=""):
self._invalidate()
raise

def findings(self, goal_id, cursor="", *, projector):
"""Read saved evidence; this transport has no complete task-list view."""
with self._lock:
if goal_id is None:
raise SourceScopeError("Choose a Goal before reading findings")
if not isinstance(cursor, str) or len(cursor) > 512:
raise ValueError("history_cursor_expired")
self.status(goal_id)
try:
page = self._pages.page(
scope=(self.source.namespace, goal_id, "findings"), cursor=cursor,
load=lambda: projector(self.source.read_events(goal_id),
None, goal_id=goal_id))
return {**page, "source": self.source.describe(), "goal_id": goal_id}
except (ValueError, OSError, subprocess.SubprocessError) as exc:
if str(exc) != "history_cursor_expired":
self._invalidate()
raise


class _NoRedirect(HTTPRedirectHandler):
def redirect_request(self, *_args, **_kwargs):
Expand Down
46 changes: 32 additions & 14 deletions loopx/presentation/explore_results_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,14 @@
explore_history_rows,
explore_result_log_path,
load_explore_result_events_strict,
validate_explore_result_event,
)


def _result_rows(runtime_root, goal_id, registry_path):
events = load_explore_result_events_strict(
explore_result_log_path(runtime_root, goal_id), goal_id=goal_id,
)
projection = build_explore_result_projection(
events, goal_id=goal_id, finding_limit=len(events),
)
nodes = {node["node_id"]: node for node in projection["nodes"]}
# Reuse canonical active/history readback; an association is not adoption.
todos = {}
for view in ({}, {"status": "done", "read_scope": "completed_history"}):
Expand All @@ -29,15 +26,26 @@ def _result_rows(runtime_root, goal_id, registry_path):
goal_id=goal_id, role="agent", **view,
)["todos"]:
todos[todo["todo_id"]] = todo
return _finding_rows(events, todos.values(), goal_id=goal_id)


def _finding_rows(events, todos, *, goal_id):
"""Same Explore projection and task associations for every physical source."""
events = [validate_explore_result_event(event, expected_goal_id=goal_id) for event in events]
projection = build_explore_result_projection(
events, goal_id=goal_id, finding_limit=len(events),
)
nodes = {node["node_id"]: node for node in projection["nodes"]}
links = {}
for todo in todos.values():
for todo in todos or []:
for node_id in todo.get("explore_result_node_refs") or []:
links.setdefault(node_id, []).append({
"todo_id": todo["todo_id"], "text": todo["text"],
"status": todo["status"], "claimed_by": todo.get("claimed_by") or "",
})
return [
{**finding, "linked_todos": links.get(finding["node_id"], []),
**({"linked_todos_available": False} if todos is None else {}),
"question": nodes.get(finding["node_id"], {}).get("title", ""),
"scope": nodes.get(finding["node_id"], {}).get("summary", "")}
for finding in projection["findings"]
Expand All @@ -58,31 +66,41 @@ def _history_rows(runtime_root, goal_id):

class ExploreResultsRequestMixin:
def _explore_results(self):
query = parse_qs(urlparse(self.path).query)
query = parse_qs(urlparse(self.path).query, keep_blank_values=True)
goal_id = query.get("goal_id", [""])[0]
try:
if any(len(values) != 1 for values in query.values()):
self._send_error("Choose one Goal, source and Explore view.", status=400)
return
view = query.get("view", ["findings"])[0]
if view not in {"findings", "history"}:
self._send_error("Unknown Explore view.", status=400)
return
from .goal_source_routing import linked_goal_source
resolved = linked_goal_source(self.server, goal_id)
if resolved is not None:
if not self._require_loopback_origin():
return
if query.get("view", ["history"]) != ["history"]:
self._send_error("Use the original source history.", status=400)
return
row, reader = resolved
if query.get("source_namespace") != [reader.source.namespace]:
self._send_error("Source identity changed; refresh the Goal.", status=409)
return
self._send_json(reader.history(goal_id, query.get("cursor", [""])[0]))
cursor = query.get("cursor", [""])[0]
page = (reader.history(goal_id, cursor) if view == "history" else
reader.findings(goal_id, cursor, projector=_finding_rows))
latest = linked_goal_source(self.server, goal_id)
if latest is None or latest[1].source.namespace != reader.source.namespace:
self._send_error("Source identity changed; refresh the Goal.", status=409)
return
self._send_json(page)
return
if "source_namespace" in query:
self._send_error("Source identity changed; refresh the Goal.", status=409)
return
runtime_root = self._goal_result_scope(goal_id)
if runtime_root is None:
return
# Local HTTP view selection; canonical event vocabularies are unchanged.
view = query.get("view", ["findings"])[0]
if view not in {"findings", "history"}:
self._send_error("Unknown Explore view.", status=400)
return
page = self.server.completed_todo_pages.page(
scope=("explore_history" if view == "history" else "explore_results", goal_id),
cursor=query.get("cursor", [""])[0],
Expand Down
12 changes: 10 additions & 2 deletions loopx/presentation/runtime_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,17 @@ def do_GET(self):
"status_url": self.server.status_url, "read_only_sources": self.server.catalog,
"control_plane_write_enabled": False, "reward_write_enabled": False}
self._json(status)
elif parsed.path == "/api/chat/explore-results" and query.get("view") == ["history"]:
elif parsed.path == "/api/chat/explore-results":
from .explore_results_api import _finding_rows

cursor = query.get("cursor", [""])[0]
self._json(self.server.reader.history(goal_id, cursor))
view = query.get("view", ["findings"])
if view == ["history"]:
self._json(self.server.reader.history(goal_id, cursor))
elif view == ["findings"]:
self._json(self.server.reader.findings(goal_id, cursor, projector=_finding_rows))
else:
self._json({"ok": False, "error": "Unknown Explore view."}, 400)
elif parsed.path in {RESEARCH_ANALYSIS_PATH, RESEARCH_ANALYSIS_PATH + "/citation"}:
if self.server.analysis_execution_enabled:
self._research_analysis_read()
Expand Down
6 changes: 6 additions & 0 deletions tests/presentation/test_explore_history.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ def test_history_keeps_updates_in_snapshot_isolates_goal_and_view_and_never_writ
{"id": name, "repo": str(tmp_path), "state_file": "active.md"}
for name in ("history-fixture", "other-goal")
]}))
from tests.control_plane.canonical_authority_fixture import initialize_canonical_authority
from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection
for goal_id in ("history-fixture", "other-goal"):
initialize_canonical_authority(runtime, goal_id,
build_todo_runtime_shadow_projection(goal_id=goal_id, todos=[], handoff_mode="soft_claim"),
state_path=state, provider="sqlite")
log = explore_result_log_path(runtime, "history-fixture")
for hour in range(19):
append_explore_result_event(log, build_explore_node_event(
Expand Down
6 changes: 6 additions & 0 deletions tests/presentation/test_goal_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@ def test_real_native_host_lists_goals_and_reads_canonical_history(tmp_path):
state_file=f"{goal_id}.md", adapter={"kind": "test_v0", "status": "connected-read-only"},
authority_sources=[]))
registry.write_text(json.dumps(dict(schema_version=1, common_runtime_root=str(runtime), goals=goals)))
from tests.control_plane.canonical_authority_fixture import initialize_canonical_authority
from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection
for goal_id in ("first", "second"):
initialize_canonical_authority(runtime, goal_id,
build_todo_runtime_shadow_projection(goal_id=goal_id, todos=[], handoff_mode="soft_claim"),
state_path=project / f"{goal_id}.md", provider="sqlite")
cli = Path(sys.executable).with_name("loopx")
assert cli.is_file(), "Use the source checkout's prepared Python environment"
source = GoalSource.parse({**manifest(scope={"all_registered": True}), "registry": str(registry),
Expand Down
Loading
Loading