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 @@ -2679,6 +2679,14 @@ File/SQLite reader replay with a missing Markdown display must remain read-only;
the graph never repairs display or changes authority. This retires duplicate
Python relationship/traversal knowledge without changing the D1–D3 gates below.

The T3 lease-inspection reader now binds Todo, lease and handoff mode to one
provider revision and never reads obsolete local lease files after promotion.
Its eligibility policy is shared with current acquire/lifecycle rules, including
claim divergence and exclusion; a read result is not a lease grant or a commit
receipt. An empty canonical lease set stays empty. This read closure and removal
of duplicate eligibility rules do not qualify a provider, alter CAS/replay or
relax D1–D3; permanent Markdown display and the remaining roadmap stay intact.

Capability-gap consumers now share the TS requirement/resolution owner across
legacy and canonical inputs, including quota's Monitor capability partition.
The old Python missing-set and owner/repair decision builders are removed;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2124,6 +2124,12 @@ Task graph 的 T3 topology consumer 现共用 inventory/horizon 关系目录,
必须只读:图不修复展示,也不改变 authority。本批删除 Python 重复关系与
遍历知识,不改变以下 D1–D3 门禁。

T3 lease inspect 已将 Todo、lease 与 handoff mode 绑定到同一 provider revision,
promotion 后不再读取本地旧 lease 文件;canonical 空租约集合保持为空。资格策略与
当前 acquire/lifecycle 共用 TS owner,包含 claim 分歧和 exclusion;读取结果不是
租约授权,也不是 commit receipt。该 reader 闭合和重复规则删除不代表 provider
资格化,不改变 CAS/replay 或 D1–D3;永久 Markdown 展示与后续规划继续保留。

命令清单、update/monitor 事务和 consumer 删除统一按
[TS 执行卡](typescript-control-plane-migration-v0.zh-CN.md#当前-stack-合入后的执行卡)
推进,不在这里复制第二套实现路线,也不把 read-policy PR 合并视为存储就绪。
Expand Down
13 changes: 13 additions & 0 deletions docs/architecture/rfcs/typescript-control-plane-migration-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,19 @@ It does not change lifecycle admission, claim/lease semantics or default provide
The status source can still be incomplete: this closes one T3 interpretation
boundary, not all graph source delivery or the remaining T1–T4 work.

Lease inspection now consumes one canonical Todo/lease/handoff-mode revision
after promotion; an absent canonical lease does not revive a local lease file,
and provider failure cannot fall back to Markdown. The read reports its provider
revision without repairing display or changing the lease. Unpromoted inspection
retains its legacy source contract. The shared `task_lease_eligibility.ts` owner
also replaces the Python authority-core and three TS owner-eligibility copies
used by acquire, lifecycle and terminal fencing. Current-lease effectiveness is
derived inside acquire from the supplied owner/claim/exclusion/registration facts,
not from the old caller-provided `effective` hint. Other-Todo overlap facts still
come from the existing complete execution snapshot; release retains its separate
key/version cleanup fence. This closes one T3 reader and shared rule boundary,
not the remaining Goal-channel lease display, T1/T2 transactions or promotion.

Capability resolution now shares `agents/capability_gate.ts`: missing prerequisites,
repair outputs, owner/agent resolution and blocked-Todo bindings have one typed
owner. Quota planning v1 passes normalized requirements, not Python-computed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,16 @@ evidence/handoff 的脱敏展示。明确的语义修正:successor 谱系不
不改变生命周期准入、claim/lease 或默认 provider。来源仍可能不完整:本批
闭合一个 T3 解释边界,不宣称所有图来源交付或 T1–T4 已完成。

Lease inspect 在 promotion 后从同一 canonical revision 读取 Todo、lease 与
handoff mode;canonical 无租约不复活本地旧文件,provider 失败不回退 Markdown。
结果携带 provider revision,读取不修复展示、不修改租约;未 promotion 的来源契约保留。
`task_lease_eligibility.ts` 同时替代 Python authority core 和三处 TS owner 资格判断,
供 acquire、lifecycle 与终态 fence 复用。当前租约是否有效由 acquire 内部根据同一输入
的 owner/claim/exclusion/注册事实推导,不再由旧 `effective` 派生提示覆盖。
其他 Todo 的 scope 冲突仍消费现有完整执行快照;release 保留独立的 key/version
清理门禁。这是一个 T3 reader 与共享规则边界的闭合,不代表 Goal-channel lease
展示、T1/T2 全部事务或 promotion 已完成。

Quota 的 scope/claim 消费者现通过每个 source 一次 `todo.quota_planning.project`,
组合选择、有限展示与既有 resume planner。`quota_selection.ts` 替代 Python
claim-visibility 模块及 Agent-scope 中独立的 User gate/action 过滤器。Python
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,29 @@ canonical. After promotion, a provider outage or revision mismatch fails
closed; operators may restore a reviewed provider snapshot and regenerate the
Todo sections, but must not promote stale Markdown back to canonical truth.

## Lease inspection / 租约检查

`loopx task-lease inspect --goal-id <goal> --todo-id <todo>` follows the same
promotion boundary as Todo reads. Before promotion it reads the existing local
lease store. After promotion it reads Todo, lease and handoff mode from one
canonical revision, reports `source_authority`, `provider_revision` and
`legacy_fallback_used=false`, and returns `lease_path=null` because no local
lease JSON is authoritative. Canonical absence returns `lease=null, active=false`;
provider errors fail the read, never revive stale local files or repair display.

`active` retains its existing meaning of an effective lease, not just an
unexpired timestamp. The retained `lease.status` can remain `active` while
`executor_constraint` explains a removed/excluded owner or divergent claim.
The shared typed owner predicate does not grant execution, mutate claims or
settle work. Release still requires its own key/version fence and remains usable
for cleanup after eligibility is lost. Acquire derives current effectiveness
from facts; old wire `effective` hints are accepted but cannot override them.

中文:promotion 后检查租约必须读取同一 revision 的 Todo/lease/handoff mode,
不能拼接本地旧文件。canonical 缺失表示无租约;来源故障明确失败。`active` 仍表示
有效租约,未过期但持有人失去资格时返回原因,不自动续租、转移或清理。
读取不提供写授权;release 的 key/version 门禁与幂等、CAS 规则保持不变。

## Migration Path

The projector accepts complete legacy records and native `TodoDomainRecord`
Expand Down
9 changes: 5 additions & 4 deletions loopx/capabilities/periodic_report/goal_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ def normalize_configuration(value: Mapping[str, Any]) -> dict[str, Any]:
if not isinstance(enabled, bool):
raise TypeError("periodic_report.enabled must be a boolean")
timezone = str(value.get("timezone") or "UTC").strip()
try:
ZoneInfo(timezone)
except ZoneInfoNotFoundError as exc:
raise ValueError("periodic_report.timezone is unknown") from exc
if timezone != "UTC":
try:
ZoneInfo(timezone)
except ZoneInfoNotFoundError as exc:
raise ValueError("periodic_report.timezone is unknown") from exc
profile_preset = str(value.get("profile_preset") or "").strip()
route_ref = str(value.get("route_ref") or "").strip()
if enabled and (not profile_preset or not route_ref):
Expand Down
21 changes: 12 additions & 9 deletions loopx/capabilities/periodic_report/machine_defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,11 @@ def normalize_periodic_report_machine_defaults(
periodic.get("timezone", "UTC"),
"periodic_report.timezone",
)
try:
ZoneInfo(timezone)
except ZoneInfoNotFoundError as exc:
raise ValueError("periodic_report.timezone is unknown") from exc
if timezone != "UTC":
try:
ZoneInfo(timezone)
except ZoneInfoNotFoundError as exc:
raise ValueError("periodic_report.timezone is unknown") from exc
normalized_periodic: dict[str, Any] = {
"schema_version": PERIODIC_REPORT_MACHINE_DEFAULTS_SCHEMA,
"enabled": enabled,
Expand Down Expand Up @@ -258,10 +259,11 @@ def _normalized_goal_subscription(
timezone_name = _text(
config.get("timezone", "UTC"), "goal periodic_report.timezone"
)
try:
ZoneInfo(timezone_name)
except ZoneInfoNotFoundError as exc:
raise ValueError("goal periodic_report.timezone is unknown") from exc
if timezone_name != "UTC":
try:
ZoneInfo(timezone_name)
except ZoneInfoNotFoundError as exc:
raise ValueError("goal periodic_report.timezone is unknown") from exc
profile_preset = str(config.get("profile_preset") or "").strip() or None
route_ref = str(config.get("route_ref") or "").strip() or None
if enabled:
Expand Down Expand Up @@ -292,7 +294,8 @@ def _invalid_goal_subscription_fields(config: Mapping[str, Any]) -> tuple[str, .
timezone_name = _text(
config.get("timezone", "UTC"), "goal periodic_report.timezone"
)
ZoneInfo(timezone_name)
if timezone_name != "UTC":
ZoneInfo(timezone_name)
except (TypeError, ValueError, ZoneInfoNotFoundError):
invalid.append("timezone")
if enabled is True:
Expand Down
50 changes: 14 additions & 36 deletions loopx/control_plane/coordination/authority_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,38 +235,6 @@ def _invalid_lease_snapshot(lease: LeaseSnapshot | None) -> bool:
)


def _lease_owner_rejection(
snapshot: CoordinationSnapshot,
owner: str | None,
) -> str | None:
todo = snapshot.todo
if todo is None:
return "todo_not_found"
if todo.status != "open":
return "todo_not_open"
if not owner:
return "invalid_owner"
if owner not in snapshot.registered_agents:
return "owner_not_registered"
if owner in todo.excluded_agents:
return "owner_excluded_from_todo"
if todo.claimed_by and todo.claimed_by != owner:
return "owner_conflicts_with_claim"
return None


def _lease_is_effective(
snapshot: CoordinationSnapshot,
lease: LeaseSnapshot | None,
) -> bool:
return bool(
lease is not None
and lease.present
and lease.active
and _lease_owner_rejection(snapshot, lease.owner) is None
)


def write_scopes_overlap(
left: tuple[str, ...] | list[str],
right: tuple[str, ...] | list[str],
Expand Down Expand Up @@ -502,9 +470,20 @@ def _decide_lease_owner_eligibility(
snapshot: CoordinationSnapshot,
command: LeaseOwnerEligibilityCommand,
) -> TransitionPlan:
rejection = _lease_owner_rejection(snapshot, command.owner)
if rejection is not None:
return _result(DecisionOutcome.REJECTED, rejection)
payload = effect_runtime_result(
"task_lease.owner_eligibility",
{
"todo": _todo_fact_payload(snapshot.todo) if snapshot.todo else None,
"owner": command.owner,
"registered_agents": list(snapshot.registered_agents),
},
)
if not isinstance(payload, dict) or payload.get("schema_version") != "task_lease_owner_eligibility_v0":
raise RuntimeError("TypeScript lease owner eligibility result shape mismatch")
if payload.get("outcome") == "rejected":
return _result(DecisionOutcome.REJECTED, str(payload["code"]))
if payload.get("outcome") != "apply" or payload.get("code") != "lease_owner_allowed":
raise RuntimeError("TypeScript lease owner eligibility verdict mismatch")
return _result(
DecisionOutcome.APPLY,
"lease_owner_allowed",
Expand Down Expand Up @@ -537,7 +516,6 @@ def _decide_acquire(
{
"present": lease.present,
"active": lease.active,
"effective": _lease_is_effective(snapshot, lease),
"status": lease.status,
"owner": lease.owner,
"idempotency_key": lease.idempotency_key,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1162,6 +1162,7 @@ export async function listLocalCoordinationTodos(
todo_read_model: todoReadModel,
...(leaseIndex === null ? {} : {
leases: leaseIndex.lease_todo_ids.map((id) => leaseIndex.leases.get(id)!),
handoff_mode: head.head.handoff_mode ?? "legacy",
}),
provider_revision: head.provider_revision,
cursor: head.cursor,
Expand Down
8 changes: 1 addition & 7 deletions loopx/control_plane/coordination/todo_claim.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {leaseOwnerRejection as ownerRejection} from "../work_items/task_lease_eligibility.ts";
import type { JsonObject } from "../effect_program.ts";
import type { AuthorityStore, AuthorityStoreCommit, AuthorityStoreReceiptResult } from "./authority_store.ts";
import {
Expand All @@ -23,7 +24,6 @@ import {
normalizeIdempotencyKey,
normalizeTtl,
normalizeWriteScopes,
ownerRejection,
TASK_LEASE_SCHEMA_VERSION,
utcIsoformat,
type LeaseRecord,
Expand Down Expand Up @@ -536,11 +536,6 @@ export async function executeCoordinationTodoClaim(
if (handoffMode === "hard_lease" && leaseRequest !== null) {
const todoFact = todoLeaseFact(todo);
const currentActive = currentLease !== undefined && leaseIsActive(currentLease, input.now);
const currentEffective = currentLease !== undefined && currentActive && ownerRejection(
todoFact,
normalizeAgent(currentLease.owner),
input.registered_agents,
) === null;
const otherLeases = projection.lease_todo_ids.flatMap((todoId) => {
if (todoId === input.todo_id) return [];
const candidate = projection.leases.get(todoId)!;
Expand All @@ -565,7 +560,6 @@ export async function executeCoordinationTodoClaim(
lease: currentLease === undefined ? null : {
present: true,
active: currentActive,
effective: currentEffective,
status: typeof currentLease.status === "string" ? currentLease.status : null,
owner: normalizeAgent(currentLease.owner),
idempotency_key: typeof currentLease.idempotency_key === "string"
Expand Down
2 changes: 2 additions & 0 deletions loopx/control_plane/effect_runtime_handlers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {evaluateTaskLeaseOwnerEligibility} from "./work_items/task_lease_eligibility.ts";
import { evaluateSubagentContext, describeSubagentContext } from "./subagent_context.ts";
import {
effectIdsMatch,
Expand Down Expand Up @@ -450,6 +451,7 @@ export function createEffectRuntimeHandlers(
["quota.void.commit", evaluateQuotaVoidCommit],
["quota.settlement.read", readQuotaSettlement],
["quota.turn_envelope.evaluate", evaluateTurnEnvelope],
["task_lease.owner_eligibility", evaluateTaskLeaseOwnerEligibility],
["task_lease.acquire.decide", evaluateTaskLeaseAcquireDecision],
["task_lease.acquire.native", executeTaskLeaseAcquire],
["task_lease.lifecycle.decide", evaluateTaskLeaseLifecycleDecision],
Expand Down
43 changes: 34 additions & 9 deletions loopx/control_plane/work_items/task_lease.py
Original file line number Diff line number Diff line change
Expand Up @@ -906,7 +906,7 @@
)


def inspect_task_lease(

Check failure on line 909 in loopx/control_plane/work_items/task_lease.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 18 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=huangruiteng_loopx&issues=AaCUDow-Qzi-Va3racMM&open=AaCUDow-Qzi-Va3racMM&pullRequest=4269
*,
registry_path: Path,
runtime_root: Path,
Expand All @@ -915,17 +915,42 @@
) -> dict[str, Any]:
goal_id = normalize_goal_id(goal_id)
todo_id = normalize_lease_todo_id(todo_id)
lease_path = task_lease_path(runtime_root=runtime_root, goal_id=goal_id, todo_id=todo_id)
lease = read_lease(lease_path)
from ..coordination.local_authority import read_canonical_todos_if_promoted
from ..todos.handoff_mode import normalize_handoff_mode

# A promoted read cannot combine canonical Todo facts with stale local
# lease files or display frontmatter. Absence is an authoritative result.
canonical = read_canonical_todos_if_promoted(
runtime_root=runtime_root, goal_id=goal_id, include_leases=True,
)
source_fields: dict[str, Any] = {}
if canonical is not None:
if "handoff_mode" not in canonical:
raise TaskLeaseError("canonical lease snapshot omitted handoff mode; update the runtime",
code="local_authority_snapshot_incomplete")
lease_path = None
lease = next((row for row in canonical["leases"] if row.get("todo_id") == todo_id), None)
todo = next((row for row in canonical["todos"] if row.get("todo_id") == todo_id), None)
handoff_mode = normalize_handoff_mode(canonical.get("handoff_mode"))
source_fields = {
"source_authority": canonical["source_authority"],
"provider_revision": canonical["provider_revision"],
"legacy_fallback_used": False,
}
else:
lease_path = task_lease_path(runtime_root=runtime_root, goal_id=goal_id, todo_id=todo_id)
lease = read_lease(lease_path)
handoff_mode = _optional_handoff_mode(registry_path, goal_id)
active = lease_is_active(lease)
executor_constraint: dict[str, Any] | None = None
if active and lease:
try:
todo = task_lease_todo_projection(
registry_path=registry_path,
goal_id=goal_id,
todo_id=todo_id,
)
if canonical is None:
todo = task_lease_todo_projection(
registry_path=registry_path,
goal_id=goal_id,
todo_id=todo_id,
)
except TaskLeaseError as exc:
active = False
executor_constraint = {
Expand All @@ -942,7 +967,6 @@
active = False
else:
executor_constraint = None
handoff_mode = _optional_handoff_mode(registry_path, goal_id)
return {
"ok": True,
"schema_version": TASK_LEASE_SCHEMA_VERSION,
Expand All @@ -951,7 +975,8 @@
"todo_id": todo_id,
"active": active,
"lease": lease,
"lease_path": str(lease_path),
"lease_path": str(lease_path) if lease_path is not None else None,
**source_fields,
**({"handoff_mode": handoff_mode} if handoff_mode else {}),
**({"executor_constraint": executor_constraint} if executor_constraint else {}),
}
Loading