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
21 changes: 21 additions & 0 deletions docs/architecture/rfcs/loopx-overall-roadmap-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,18 @@ admission, fresh task derivation and frontend/Lark qualification remain open.
中文:异常读回区分本次调用未知副作用与原 Turn 的持久观察/待核对效果,只完成
R2 的一个恢复切片;调用方准入、新任务生成与前端/Lark 验收仍未闭环。

[PR #6163](https://github.com/loopx-project/loopx/pull/6163) proposes the
S2/S7/S10 PR dependency observation slice in the existing opt-in heartbeat
prequota path, independent of open review-queue membership. The Todo typed owner
selects exact unfinished waits and durable polling cadence from complete
retained facts. Resume readback shares that source before display compaction,
retaining the original merge proof through history growth and restart without
clearing requirements or changing claims, leases or status. Real File/SQLite
CLI growth/restart and repository/failure negatives qualify this bounded slice;
installed host adoption and TurnEnvelope polling remain distinct. It does not
close legacy retirement or sustained authority qualification. See
[Todo readback](../../reference/todo-continuation-readback.md#pr-dependency-observation--pr-依赖观察).

[PR #5283](https://github.com/loopx-project/loopx/pull/5283) is the proposed
S2/S10 read-preview cost slice. Summary and succession validation share the
existing TS owner request; empty succession skips transport. Read models are
Expand Down Expand Up @@ -670,6 +682,15 @@ admission remain separate open acceptances; this PR does not retire Python
planning or establish a machine default.

中文:
[#6163](https://github.com/loopx-project/loopx/pull/6163) 拟交付 S2/S7/S10
PR 依赖观察切片:既有 opt-in heartbeat prequota 不依赖 open review 队列成员,
Todo 类型化 owner 从完整保留的事实选择精确未完成等待及持久核验节奏。恢复回读
在展示裁剪前复用同一源,历史增长和重启保留原合并证明,不清要求、不改认领、
lease 或状态。真实 File/SQLite CLI 的增长/重启及仓库/失败负例只验收本批;
安装态 host 采用、TurnEnvelope 核验分别跟踪,不关闭 legacy 退役或 authority
持续运行资格。参见
[Todo 回读](../../reference/todo-continuation-readback.md#pr-dependency-observation--pr-依赖观察)。

[#5283](https://github.com/loopx-project/loopx/pull/5283) 是拟交付的 S2/S10
只读预检成本切片:摘要与继承验证共用既有 TS owner 请求,空继承行跳过传输;
读模型与写适配器分离,未选中的报告、Lark、扩展及 Host 路由延迟加载。兼容导出
Expand Down
21 changes: 21 additions & 0 deletions docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,15 @@ executor/profile 检查启动条件。任务准入、当前 pinned 验收绑定
表达;未探测的通用/云端运行时保留未知。CLI、已启用 MCP 和新 Chat 工具共用此
检查,不新增状态账本,也不启动执行。

[#6163](https://github.com/loopx-project/loopx/pull/6163) 拟交付 S2/S7/S10
PR 依赖观察切片:既有 opt-in heartbeat prequota 不依赖 open review 队列成员,
Todo 类型化 owner 从完整保留的事实选择精确未完成等待及持久核验节奏。恢复回读
在展示裁剪前复用同一源,历史增长和重启保留原合并证明,不清要求、不改认领、
lease 或状态。真实 File/SQLite CLI 的增长/重启及仓库/失败负例只验收本批;
安装态 host 采用、TurnEnvelope 核验分别跟踪,不关闭 legacy 退役或 authority
持续运行资格。参见
[Todo 回读](../../reference/todo-continuation-readback.md#pr-dependency-observation--pr-依赖观察)。

[#5283](https://github.com/loopx-project/loopx/pull/5283) 是拟交付的 S2/S10
只读预检成本切片:摘要与继承验证共用既有 TS owner 请求,空继承行跳过传输;
读模型与写适配器分离,未选中的报告、Lark、扩展及 Host 路由延迟加载。兼容导出
Expand All @@ -488,6 +497,18 @@ R2 持续运行、真实调用方采用、冷 CLI/p95 与 provider 准入仍是
此 PR 不代表 Python 规划退役或机器默认切换。

English:
[PR #6163](https://github.com/loopx-project/loopx/pull/6163) proposes the
S2/S7/S10 PR dependency observation slice in the existing opt-in heartbeat
prequota path, independent of open review-queue membership. The Todo typed owner
selects exact unfinished waits and durable polling cadence from complete
retained facts. Resume readback shares that source before display compaction,
retaining the original merge proof through history growth and restart without
clearing requirements or changing claims, leases or status. Real File/SQLite
CLI growth/restart and repository/failure negatives qualify this bounded slice;
installed host adoption and TurnEnvelope polling remain distinct. It does not
close legacy retirement or sustained authority qualification. See
[Todo readback](../../reference/todo-continuation-readback.md#pr-dependency-observation--pr-依赖观察).

[PR #5283](https://github.com/loopx-project/loopx/pull/5283) is the proposed
S2/S10 read-preview cost slice. Summary and succession validation share the
existing TS owner request; empty succession skips transport. Read models are
Expand Down
51 changes: 51 additions & 0 deletions docs/reference/todo-continuation-readback.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,57 @@ or an explicit `no_followup=true`. This read policy lives in
its decisions. It belongs to the existing Todo control plane, uses the selected
AuthorityStore, and adds no capability or extension provider.

## PR dependency observation / PR 依赖观察

`pr_merged:owner/repo#42` identifies a dependency; absence of a merge event
does not prove that GitHub is still pending. Delivery-history supervision no
longer exempts that unobserved wait. Causal blocked closeout continues to require
a registered `monitor_changed` or `todo_done` target with current readback.

The existing `heartbeat-prequota --goal-id GOAL --agent-id AGENT` observes exact
unfinished PR dependencies from the complete Todo head, including deferred work.
It polls at most four distinct repository-qualified dependencies per invocation,
with a durable 30-minute cadence for successful and failed attempts. A missing
repository binding is reported for repair; it never guesses from PR numbers.
An exact merged observation appends the existing native `pr_merge` fact so the
original condition becomes satisfied. Resume readback uses the same complete
retained Goal log as observation, before any recent-event display limit. The
typed resume owner keeps one original merge proof per exact dependency; more
than 500 unrelated events or 256 other dependencies cannot evict that proof.
List, status, quota and archived reads keep their bounded presentation, and
restarts need neither repeated remote reads nor rewritten merge timestamps.
Queue disappearance, closed-without-merge
and provider failures cannot satisfy it. This observation changes no Todo status,
requirements, claims, leases or Goal acceptance. Deferred ready work still needs
its normal lifecycle transition.

Generated heartbeat prompts include prequota only when the host declares
`--available-capability external_evidence_poll`; filesystem/shell declarations
alone do not opt in. Omitting that declaration keeps external observation off.
Explicit prequota invocation authorizes compact metadata reads and local
observation writeback, never merge, review, remote mutation or quota spend.
Read back with `todo list --goal-id GOAL --todo-id TODO --format json`.
These same typed dependency facts are the boundary to reuse in TurnEnvelope;
this heartbeat adoption does not claim that every Turn host already polls them.

`pr_merged:owner/repo#42` 只标识依赖;缺少合并事件不证明 GitHub 仍在等待。
历史交付监督不再豁免这种未观察等待,因果阻塞结算仍要求已注册的
`monitor_changed` 或 `todo_done` 目标及当前回读。
原生 `heartbeat-prequota --goal-id GOAL --agent-id AGENT` 从完整 Todo 源选择
精确的未完成 PR 依赖(含 deferred),每次最多核验四个不同仓库限定的依赖。
成功和失败均保留 30 分钟核验节奏,重启后仍有效。仓库缺失需要修复,不能按编号猜测。
真实合并观察写入既有 `pr_merge`,原条件自然满足。回读与观察共用完整保留的 Goal
日志,在最近事件展示裁剪前由 TS resume owner 保留每项精确依赖的原始合并证明;
超过 500 条无关事件或 256 项其他依赖不能挤走它。list、status、quota 与归档读取
仍保留原展示边界,重启不需要重复远端读取或改写合并时间。离开 open 列表、关闭未合并或
provider 失败不能解除等待。观察不修改 Todo 状态、要求、认领、lease 或 Goal 验收;
已满足条件的 deferred Todo 仍走正常生命周期转换。
生成的 heartbeat 仅在声明 `--available-capability external_evidence_poll` 时加入
prequota;只声明 filesystem/shell 不会启用。省略该声明即关闭外部观察。
显式调用仅授权紧凑元数据读取和本地观察记账,不授权合并、review、远端写入或额度支出。
通过 `todo list --goal-id GOAL --todo-id TODO --format json` 回读。
TurnEnvelope 应复用同一类型化依赖事实;本批未证明所有 Turn host 已接入自动核验。

## Relationship evidence

The policy evaluates the complete available Todo graph before role, status,
Expand Down
3 changes: 2 additions & 1 deletion loopx/capabilities/issue_fix/pr_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -860,6 +860,7 @@ def fetch_github_pr_lifecycle_payload(
reference: Mapping[str, Any],
*,
timeout_seconds: int = 10,
compact: bool = False,
) -> dict[str, Any]:
repo = str(reference.get("repo") or "")
number = reference.get("number")
Expand All @@ -877,7 +878,7 @@ def fetch_github_pr_lifecycle_payload(
repo_slug,
"--json",
",".join(
[
["state", "url", "mergedAt", "closedAt"] if compact else [
"baseRefName",
"closingIssuesReferences",
"closedAt",
Expand Down
92 changes: 92 additions & 0 deletions loopx/capabilities/issue_fix/pr_wait_reconcile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"""Transport the typed dependency poll plan into existing public lifecycle facts.

This internal issue-fix adapter changes neither Todo requirements nor claims,
leases, status, settlement or review authority. It does not consult an open PR
queue: dependencies are selected from the complete current canonical Todo head.
"""
from __future__ import annotations

from pathlib import Path
from subprocess import TimeoutExpired
from typing import Any

from ...agent_registry import registered_agent_ids_from_registry
from ...control_plane.effect_runtime import effect_runtime_result
from ...control_plane.runtime.time import now_utc_iso
from ...control_plane.todos.resume_condition import compact_todo_resume_items
from ...history import load_registry
from ...paths import resolve_runtime_root
from ...rollout_event_log import (
append_rollout_event, build_rollout_event, rollout_event_log_path,
)
from ...todos import list_goal_todos
from . import pr_lifecycle
from .pr_lifecycle_rollout import append_pr_merge_rollout_event


def reconcile_pr_wait_dependencies(
*, registry_path: Path, runtime_root_arg: str | None, goal_id: str,
agent_id: str, fetch_timeout_seconds: int = 10,
) -> dict[str, Any]:
if agent_id not in registered_agent_ids_from_registry(registry_path, goal_id):
raise ValueError("PR dependency observation requires a registered agent")
runtime = (Path(runtime_root_arg).expanduser() if runtime_root_arg
else resolve_runtime_root(load_registry(registry_path), None))
Comment thread
loopx-agent marked this conversation as resolved.
# Deliberately omit list --agent-id and display limits. The typed owner must
# see archived/deferred/claimed facts before choosing this actor's subjects.
todos = list_goal_todos(registry_path=registry_path, runtime_root_arg=str(runtime),
goal_id=goal_id, role="agent")["todos"]
compact = compact_todo_resume_items(todos)
for row, original in zip(compact, todos, strict=True):
if "excluded_agents" in original:
row["excluded_agents"] = original["excluded_agents"]
log = rollout_event_log_path(runtime, goal_id)
plan = effect_runtime_result("todo.pr_wait_observation.plan", {
"schema_version": "todo_pr_wait_observation_request_v0", "agent_id": agent_id,
"generated_at": now_utc_iso(), "items": compact,
"rollout_events": [],
"rollout_event_source": {"runtime_root": str(runtime), "goal_id": goal_id},
})
results: list[dict[str, Any]] = []
for target in plan["targets"]:
reference = {"repo": target["repo"], "number": target["number"]}
result = {"pr_ref": target["pr_ref"], "todo_ids": target["todo_ids"],
"state": "FAILED", "merge_event_recorded": False}
try:
metadata = pr_lifecycle.fetch_github_pr_lifecycle_payload(reference,
timeout_seconds=fetch_timeout_seconds, compact=True)
# A provider redirect or malformed success must never attest another
# repository's identically numbered dependency. No default OPEN.
if str(metadata.get("url") or "").lower().rstrip("/") != target["url"].lower():
raise ValueError("PR dependency provider identity mismatch")
state = str(metadata.get("state") or "").upper()
if state not in {"OPEN", "CLOSED", "MERGED"}:
raise ValueError("PR dependency provider state unavailable")
if state == "MERGED" and not metadata.get("mergedAt"):
raise ValueError("Merged PR observation lacks its terminal timestamp")
if state == "MERGED":
lifecycle = pr_lifecycle.build_issue_fix_pr_lifecycle_monitor_packet(
repo=target["repo"], pr_ref=target["pr_ref"], url=target["url"],
provider_payload=metadata)
result["merge_event_recorded"] = append_pr_merge_rollout_event(payload=lifecycle,
goal_id=goal_id, registry_path=registry_path, runtime_root_arg=str(runtime))["recorded"]
result["state"] = state
except (ValueError, RuntimeError, OSError, TimeoutExpired) as error:
result["error_category"] = type(error).__name__
# Persist failed attempts too: restarts and repeated heartbeats respect
# the same cadence. This validation is not delivery/settlement evidence.
append_rollout_event(log, build_rollout_event(goal_id=goal_id,
event_kind="validation", agent_id=agent_id, pr_ref=target["pr_ref"],
classification=plan["observation_classification"], status=result["state"],
recorded_at=now_utc_iso(), summary="Observed exact PR wait dependency; no Todo transition."))
results.append(result)
failures = sum(row["state"] == "FAILED" for row in results)
observed = {row["pr_ref"]: row["state"] for row in results}
retained_failures = sum(observed.get(row["pr_ref"], row["last_observed_state"]) == "FAILED"
for row in plan["waiting_targets"])
return {"ok": True, "degraded": retained_failures > 0 or bool(plan["unresolved"]),
"failure_count": failures, "external_read_count": len(results),
"unavailable_dependency_count": retained_failures,
"merged_count": sum(row["merge_event_recorded"] for row in results),
"closed_without_merge_count": sum(row["state"] == "CLOSED" for row in results),
"results": results, "plan": plan, "todo_write_performed": False}
3 changes: 2 additions & 1 deletion loopx/control_plane/effect_runtime_handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,8 @@ export function createEffectRuntimeHandlers(
["todo.next_action.transition", lazyHandler(() => import("./todos/next_action.ts"), ({transitionTodoNextAction}) => transitionTodoNextAction)],
["todo.next_action.binding", lazyHandler(() => import("./todos/next_action.ts"), ({projectNextActionBinding}) => projectNextActionBinding)],
["todo.resume_condition.normalize", lazyHandler(() => import("./todos/resume_condition.ts"), ({normalizeTodoResumeWhen}) => normalizeTodoResumeWhen)],
["todo.resume_condition.evaluate", lazyHandler(() => import("./todos/resume_condition.ts"), ({evaluateTodoResumeConditions}) => evaluateTodoResumeConditions)],
["todo.resume_condition.evaluate", lazyHandler(() => import("./todos/resume_condition.ts"), ({evaluateTodoResumeConditionsFromSource}) => evaluateTodoResumeConditionsFromSource)],
["todo.pr_wait_observation.plan", lazyHandler(() => import("./todos/pr_wait_observation.ts"), ({planPrWaitObservationsFromSource}) => planPrWaitObservationsFromSource)],
["todo.resume_planning.project", lazyHandler(() => import("./todos/resume_planning.ts"), ({projectTodoResumePlanning}) => projectTodoResumePlanning)],
["todo.quota_planning.project", lazyHandler(() => import("./todos/quota_selection.ts"), ({projectTodoQuotaPlanning}) => projectTodoQuotaPlanning)],
["todo.frontier_revision.project", lazyHandler(() => import("./todos/frontier_revision.ts"), ({projectAdvancementFrontier}) => projectAdvancementFrontier)],
Expand Down
3 changes: 3 additions & 0 deletions loopx/control_plane/todos/active_state_todos.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
)

from .succession_warning import public_todo_summary
from .resume_condition import TodoResumeRolloutEvents
from ...agent_registry import registered_agent_ids_for_goal
from ..work_items.recommendation_source_io import (
load_recommendation_source_goal, recommendation_source_context,
Expand Down Expand Up @@ -122,6 +123,8 @@ def active_state_todo_fields(
rollout_event_log_path(runtime_root, goal_id),
limit=max_todo_index_rollout_events_per_goal,
)
if runtime_root is not None and goal_id:
events = TodoResumeRolloutEvents(events, runtime_root=runtime_root, goal_id=goal_id)
if canonical is not None:
fields = canonical_todo_summary_fields(
canonical["todos"],
Expand Down
3 changes: 3 additions & 0 deletions loopx/control_plane/todos/list_readback.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
todo_list_projection_contract,
)
from .todo_index import MAX_TODO_INDEX_ROLLOUT_EVENTS_PER_GOAL
from .resume_condition import TodoResumeRolloutEvents


def _restore_full_source_body(
Expand Down Expand Up @@ -98,6 +99,8 @@ def list_goal_todos(
limit=MAX_TODO_INDEX_ROLLOUT_EVENTS_PER_GOAL,
)

rollout_events = TodoResumeRolloutEvents(rollout_events, runtime_root=runtime_root, goal_id=goal_id)

roles = [role] if role else ["user", "agent"]
canonical_read = read_canonical_todos_if_promoted(
runtime_root=runtime_root,
Expand Down
Loading
Loading