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 @@ -2932,7 +2932,12 @@ 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
receipt. Both source routes now interpret time and eligibility in TS, including
archived-open retained history and explicit malformed-expiry failure; registration
and promotion-fence changes are revalidated with bounded retry. Python no longer
reconstructs the canonical head or diagnostic policy for inspection. See
[the read contract](../../reference/canonical-lease-renew.md#what-inspection-proves).
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.
The ownership-edit slice now uses the same typed authoring and lifecycle boundary
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2330,7 +2330,10 @@ Task graph 的 T3 topology consumer 现共用 inventory/horizon 关系目录,
T3 lease inspect 已将 Todo、lease 与 handoff mode 绑定到同一 provider revision,
promotion 后不再读取本地旧 lease 文件;canonical 空租约集合保持为空。资格策略与
当前 acquire/lifecycle 共用 TS owner,包含 claim 分歧和 exclusion;读取结果不是
租约授权,也不是 commit receipt。该 reader 闭合和重复规则删除不代表 provider
租约授权,也不是 commit receipt。两条来源路径的时间/资格解释现收敛到 TS,
覆盖归档 open 历史与损坏到期时间;注册来源及晋升 fence 变化须重新校验并有界重试。
Python 不再为检查重建 canonical head 或诊断规则,见[读取合同](../../reference/canonical-lease-renew.md#what-inspection-proves)。
该 reader 闭合和重复规则删除不代表 provider
资格化,不改变 CAS/replay 或 D1–D3;永久 Markdown 展示与后续规划继续保留。
ownership 编辑在 promotion 后现在与现有 update transaction 共用 typed authoring
和 lifecycle 边界。claim/exclusion 门禁保留,带 lease 的 ownership 重写继续拒绝;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -825,8 +825,13 @@ 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
revision without repairing display or changing the lease. Both routes now use
`task_lease_inspection.ts` for time and eligibility interpretation; Python only
projects source-bound registration/legacy facts and transports the response.
Diagnostics reuse the TS rejection owner, including archived-Todo eligibility,
strict active expiry and bounded source-change retry. Unpromoted storage stays
unchanged; malformed active expiry intentionally becomes a visible error. See
[inspection semantics](../../reference/canonical-lease-renew.md#what-inspection-proves). 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -627,7 +627,11 @@ evidence/handoff 的脱敏展示。明确的语义修正:successor 谱系不

Lease inspect 在 promotion 后从同一 canonical revision 读取 Todo、lease 与
handoff mode;canonical 无租约不复活本地旧文件,provider 失败不回退 Markdown。
结果携带 provider revision,读取不修复展示、不修改租约;未 promotion 的来源契约保留。
结果携带 provider revision,读取不修复展示、不修改租约。两条路径现由
`task_lease_inspection.ts` 统一时间与资格解释,Python 仅投递绑定来源的注册/legacy
事实和响应;诊断字段复用 TS 拒绝规则。归档 Todo 不产生有效租约,active 到期时间
损坏明确报错,来源变化有界重试。未晋升存储保持原状,错误语义变更见
[检查合同](../../reference/canonical-lease-renew.md#what-inspection-proves)。
`task_lease_eligibility.ts` 同时替代 Python authority core 和三处 TS owner 资格判断,
供 acquire、lifecycle 与终态 fence 复用。当前租约是否有效由 acquire 内部根据同一输入
的 owner/claim/exclusion/注册事实推导,不再由旧 `effective` 派生提示覆盖。
Expand Down
38 changes: 38 additions & 0 deletions docs/reference/canonical-lease-renew.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,44 @@ already assigned to the eligible receiver. Without `--transfer-claim`, transfer
claim. Neither form overrides an exclusion or widens write scopes. Use the actual readback
versions, not these example numbers.

## What inspection proves

`task-lease inspect` uses one TS read owner for legacy files and selected
File/SQLite authority. Service-owned PostgreSQL uses the same reader through its
existing identity-fenced factory; the CLI does not gain a PostgreSQL connection
or enable a service deployment. Promoted inspection reads Todo, lease and mode
from one complete provider revision, ignoring stale display and lease files.

`lease.status: active` describes the retained record. The top-level `active`
means its expiry is strictly after the observation clock **and** its current
owner is eligible for the active, open Todo. An archived record cannot revive
execution even when imported history retains `status: open` and a future lease.
Closed, unregistered, excluded and conflicting-claim owners return `active:
false` with the existing `executor_constraint` diagnostic. Rejection precedence
and diagnostic fields share the mutation-admission owner.

An active lease with an invalid expiry now fails with `corrupt_lease`, including
on the legacy route; it no longer looks like an ordinary inactive lease. Missing,
expired and released leases retain their previous successful inactive response.
For legacy storage, TS first checks the retained lease and only asks Python for
a full Todo projection when it is time-active; the final check re-reads the lease
after that projection. Inactive inspection does not parse the work history.
Registration-source receipts and the promotion fence are rechecked after the
read. Source changes trigger at most three host attempts, then an explicit
`authority_source_changed` failure. Selected-provider failures never fall back.

This is a read-only observation, not an execution grant or a lock across later
work. A subsequent registration change, provider commit or expiry can invalidate
it. Mutations must still prove their current owner/key/version and commit under
their existing fences. Inspection does not repair display, renew a lease, change
provider selection or spend quota.

检查由 TS 统一解释两条来源路径:`lease.status` 是保留记录的状态,顶层 `active`
还要求租约未到期、Todo 活跃且未关闭、owner 仍满足注册、排除与认领约束。
归档但仍标为 open 的历史记录不再产生有效执行资格;损坏的 active 到期时间明确
报错,不再伪装成正常失效。检查前后校验注册来源与晋升 fence,最多重试三次;
结果只代表一次观察,后续写操作仍须校验当前执行证明,不获得新的授权。

## Atomically hand over claimed work

When the current canonical `hard_lease` Todo and lease both belong to the sender,
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 {inspectTaskLease} from "./work_items/task_lease_inspection.ts";
import {evaluateTodoPriority} from "./todos/priority.ts";
import {evaluateUserCompletion} from "./todos/user_completion.ts";
import {projectTodoSuccession, projectTodoClosure} from "./todos/succession.ts";
Expand Down Expand Up @@ -506,6 +507,7 @@ export function createEffectRuntimeHandlers(
["task_lease.owner_eligibility", evaluateTaskLeaseOwnerEligibility],
["task_lease.acquire.decide", evaluateTaskLeaseAcquireDecision],
["task_lease.acquire.native", executeTaskLeaseAcquire],
["task_lease.inspect.native", inspectTaskLease],
["task_lease.lifecycle.decide", evaluateTaskLeaseLifecycleDecision],
["task_lease.lifecycle.native", executeTaskLeaseLifecycle],
["coordination.runtime_shadow.bootstrap", bootstrapCoordinationRuntimeShadow],
Expand Down
126 changes: 25 additions & 101 deletions loopx/control_plane/work_items/task_lease.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,8 @@
from ..runtime.time import now_utc as runtime_now_utc
from ..runtime.time import parse_timestamp, utc_isoformat
from ..coordination.authority_core import (
CoordinationSnapshot,
DecisionOutcome,
LeaseOwnerEligibilityCommand,
decide,
write_scopes_overlap as core_write_scopes_overlap,
)
from ..coordination.local_snapshot import todo_snapshot_from_mapping
from ..todos.contract import (
normalize_required_write_scopes,
normalize_todo_claimed_by,
Expand All @@ -45,7 +40,7 @@
lease_acquire_ttl_seconds as lease_acquire_ttl_seconds,
lease_epoch as lease_epoch,
lease_version as lease_version,
read_lease,
read_lease as read_lease,
require_expected_version as require_expected_version,
write_lease as write_lease,
)
Expand Down Expand Up @@ -605,36 +600,26 @@ def task_lease_owner_constraint(
owner: Any,
registered_agents: list[str] | None = None,
) -> dict[str, Any]:
from ..effect_runtime import effect_runtime_result

normalized_owner = normalize_todo_claimed_by(owner)
effective_registered_agents = (
tuple(registered_agents)
if registered_agents is not None
else ((normalized_owner,) if normalized_owner else ())
)
plan = decide(
CoordinationSnapshot(
registered_agents=effective_registered_agents,
todo=todo_snapshot_from_mapping(todo),
),
LeaseOwnerEligibilityCommand(owner=normalized_owner),
)
if plan.outcome is DecisionOutcome.APPLY:
return {"effective": True}
result: dict[str, Any] = {"effective": False, "reason": plan.code}
if plan.code == "todo_not_open":
todo_status = str((todo or {}).get("status") or "").strip().lower()
result["todo_status"] = todo_status or "unknown"
elif plan.code == "owner_not_registered":
result["registered_agents"] = list(registered_agents or [])
elif plan.code == "owner_excluded_from_todo":
result["excluded_agents"] = normalize_todo_excluded_agents(
(todo or {}).get("excluded_agents")
)
elif plan.code == "owner_conflicts_with_claim":
result["claimed_by"] = normalize_todo_claimed_by(
(todo or {}).get("claimed_by")
)
return result
registered = registered_agents if registered_agents is not None else ([normalized_owner] if normalized_owner else [])
result = effect_runtime_result("task_lease.owner_eligibility", {
"todo": None if todo is None else {
"status": str(todo.get("status") or "").strip().lower(),
"claimed_by": normalize_todo_claimed_by(todo.get("claimed_by")),
"excluded_agents": normalize_todo_excluded_agents(todo.get("excluded_agents")),
},
"owner": normalized_owner, "registered_agents": registered,
})
if not isinstance(result, dict) or result.get("schema_version") != "task_lease_owner_eligibility_v0":
raise RuntimeError("typed lease owner constraint is missing; update the runtime")
constraint = result.get("constraint")
if not isinstance(constraint, dict) or not isinstance(constraint.get("effective"), bool):
raise RuntimeError("typed lease owner constraint shape mismatch")
if constraint["effective"] is False and not isinstance(constraint.get("reason"), str):
raise RuntimeError("typed lease owner rejection omitted its reason")
return constraint


def require_registered_task_lease_owner(
Expand Down Expand Up @@ -918,70 +903,9 @@ def inspect_task_lease(
goal_id: str,
todo_id: str,
) -> dict[str, Any]:
goal_id = normalize_goal_id(goal_id)
todo_id = normalize_lease_todo_id(todo_id)
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,
from .task_lease_acquire_adapter import inspect_native_task_lease

return inspect_native_task_lease(
registry_path=registry_path, runtime_root=runtime_root,
goal_id=normalize_goal_id(goal_id), todo_id=normalize_lease_todo_id(todo_id),
)
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:
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 = {
"effective": False,
"reason": exc.code,
}
else:
executor_constraint = task_lease_owner_constraint(
todo,
owner=lease.get("owner"),
registered_agents=registered_agent_ids_from_registry(registry_path, goal_id),
)
if executor_constraint.get("effective") is not True:
active = False
else:
executor_constraint = None
return {
"ok": True,
"schema_version": TASK_LEASE_SCHEMA_VERSION,
"action": "inspect",
"goal_id": goal_id,
"todo_id": todo_id,
"active": active,
"lease": lease,
"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 {}),
}
2 changes: 1 addition & 1 deletion loopx/control_plane/work_items/task_lease_acquire.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ export function decodeTaskLeaseAuthority(value: unknown): AuthorityFacts {
};
}

function normalizeHandoffMode(value: unknown): string {
export function normalizeHandoffMode(value: unknown): string {
const mode = compact(value) || "legacy";
if (!new Set(["legacy", "soft_claim", "hard_lease"]).has(mode)) {
throw new TaskLeaseAcquireError(
Expand Down
Loading
Loading