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
@@ -0,0 +1,28 @@
# Exact acceptance restoration without a lease cycle

Source: #4971. An Agent could clear an owner-confirmed Todo's existing wait while
holding a hard lease, release it, and then be unable to restore it: stale
acceptance prohibited acquire, while update demanded an active lease.

The existing TS reviewed-update boundary now admits a narrow restoration after
ordinary registered actor / claim / exclusion checks. With no active lease or
supplied execution proof, the same claimed Agent may restore prior text/wait
only when the candidate's complete work digest equals the current owner binding.
The provider revision CAS and existing operation receipt guard the transaction;
criteria, binding, lifecycle and lease generation are unchanged. Historical
schema-less lease records are normalized once at the shared TS read boundary
so activity checks cannot mistake a live lease for an inactive one; deferred
reopen reuses that normalization instead of injecting the tag itself. Execution still
requires a new ordinary acquire. Unknown prior values or other scope changes
project an explicit owner-review/rebind requirement.

This advances native long-horizon recovery, not provider default selection or
D3 promotion. Real File/SQLite CLI regressions exercise acquire, stale edit,
release, rejected reacquire, restoration and fresh acquire; provider tests also
exercise a disposable PostgreSQL server. Managed frontier projection drops the
stale hold after restoration. The original managed Turn then completes its Todo,
writes back and settles; replaying restoration and spend consumes quota once.
#5000 remains separately owned by Turn settlement
and its retry policy; restoring acceptance does not settle a Turn.

See the [caller contract](../../../../reference/goal-acceptance-observations.md#restore-an-unintended-textwait-edit-after-lease-release).
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# 避免租约循环依赖的精确验收声明恢复

来源:#4971。Agent 持有硬租约时误清 owner 已确认 Todo 的原有等待条件,释放租约后
会陷入死锁:stale 验收阻止获取租约,而 update 又要求活动租约。

修复放在现有 TS reviewed-update 边界,保留注册 actor、claim 和排除条件检查。
没有活动租约、也不提交旧执行证明时,同一 claimed Agent 可以带 provider revision
恢复原文本/等待条件,但候选完整工作摘要必须严格等于当前 owner 绑定的摘要。
现有 CAS 与操作回执保证并发和重试;验收标准、绑定、生命周期和租约代次均不改变。
旧的无 schema 租约在共享 TS 读取边界统一补全格式,避免活动性检查误把有效租约当作
非活动租约;deferred reopen 也复用这条规则,不再自行补字段。
执行工作仍须正常获取新租约。不知道原值或涉及其他范围变更时,明确要求 owner
审核并重新绑定,不能猜测恢复。

此项推进原生长程恢复,不改变默认 provider 或 D3 晋升结论。真实 File/SQLite CLI
覆盖获取租约、误改、释放、拒绝重取、恢复、再次获取;provider 用例还覆盖隔离的
真实 PostgreSQL。恢复后 managed frontier 不再投影 stale hold;原 managed Turn 随后完成 Todo、写回与结算,重复恢复和结算只消费一次
配额。#5000 的 Turn
结算及重试策略仍属独立边界;恢复验收声明不等于结算 Turn。

参见[调用方合同](../../../../reference/goal-acceptance-observations.md#restore-an-unintended-textwait-edit-after-lease-release)。
48 changes: 46 additions & 2 deletions docs/reference/goal-acceptance-observations.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,9 @@ existing `resume_when` is not reconstructible from the latest Todo and remains
and stale associations enter the existing agent-scoped recovery lane. Recovery
preserves the original Turn/Todo identity and does not authorize execution of
held work. Inspect a missing association and prepare it for owner confirmation;
for a stale association, inspect the work delta and restore an unintended edit
or propose the changed association. An already eligible successor remains a
for a stale association, inspect the work delta and use the
[exact text/wait restoration](#restore-an-unintended-textwait-edit-after-lease-release)
when possible, or propose the changed association for owner review. An already eligible successor remains a
separate execution identity. Do not create another unbound repair Todo and
mistake its existence for a runnable successor.

Expand Down Expand Up @@ -396,3 +397,46 @@ Lark 呈现、远端合同编辑、语义意图保持证明与通用共享 amend
不宣称任一 RFC 已完成。合同浏览器检查用 `npm run smoke:goal-acceptance-contract-browser`;
前端集成打包后,`npm run smoke:goal-acceptance-contract-packaged` 对已发布资源跑同一项检查。
Python renderer 测试和 API/export smoke 覆盖缺失、停用、过期、失败及通过的区别。


## Restore an unintended text/wait edit after lease release

A hard-lease Todo may become stale after its claimed Agent accidentally changes
its text or clears an existing `resume_when`, then releases the execution lease.
The same claimed Agent can use a reviewed update to restore the **exact original
work declaration**, without first acquiring a lease over stale work:

```sh
loopx goal-acceptance inspect --goal-id example
loopx todo update --goal-id example --todo-id todo_artifact --agent-id agent-a \
--resume-when 'resume_at:2026-01-01T00:00:00Z' \
--update-operation-id restore-original-wait \
--update-expected-provider-revision '<revision from inspect>'
```

Supply the actual original wait/text, not the example value. TS compares the
entire candidate work digest with the existing owner-confirmed binding. It
rejects a different scope, wrong revision, foreign/excluded actor, active lease,
stale execution proof, or a bundled lifecycle/validator/ownership edit. The
restoration writes neither an owner rebind nor an execution grant. Acquire a
fresh lease through the usual command before executing work; an exact update
retry reads the old operation receipt and cannot alter the new lease generation.
It does not complete a Todo or settle/spend a Turn.

This is not a general history rollback. If the previous declaration is unknown,
other work fields changed, or compatible subsequent revisions prevent an exact
match, prepare the current intent for owner review and explicit rebind. The CLI
and managed replan guidance name that route instead of prescribing a lease /
restore loop. Existing ready, unbound and acceptance-disabled work keeps its
ordinary admission rules. Frontend and Lark consume the resulting canonical
state; this introduces no separate editor or authority owner.

硬租约 Todo 因误改文本或原有等待条件而 stale、且租约已释放时,同一 claimed Agent
可带当前 provider revision 和稳定 operation id,通过原来的 `todo update` 精确还原。
TS 校验整个候选工作声明的摘要必须等于 owner 当初确认的摘要;不会把任意修改当成
无害变化,也不修改验收标准、owner 绑定或租约。恢复后仍须正常获取新租约才能执行。
重复请求仅恢复旧回执,不会重复结算 Turn 或改变新一代租约。

必须提供真实的原文本/等待条件。若不知道原声明、改动涉及其他字段,或后续兼容修订
使完整摘要无法精确匹配,应请 owner 审核并显式重新绑定。不能猜测旧值、伪造完成,
也不能先取得 stale 工作的租约来绕过这条边界。
8 changes: 5 additions & 3 deletions loopx/control_plane/coordination/task_lease_state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import {indexCoordinationProjection} from "./coordination_projection.ts";
import {normalizeTodoAgent} from "./todo_agents.ts";
import {leaseOwnerRejection} from "../work_items/task_lease_eligibility.ts";
import {leaseVersion, leaseEpoch, leaseInteger, leaseIsActive, normalizeOwner,
normalizeIdempotencyKey, type LeaseRecord, type TodoFact} from "../work_items/task_lease_acquire.ts";
normalizeIdempotencyKey, TASK_LEASE_SCHEMA_VERSION, type LeaseRecord, type TodoFact} from "../work_items/task_lease_acquire.ts";
import type {AcquireDecisionInput} from "../work_items/task_lease_acquire_decision.ts";

export function canonicalTaskLease(value: JsonObject, goalId: string, todoId: string): LeaseRecord {
if ((value.schema_version !== undefined && value.schema_version !== "task_lease_v0") ||
if ((value.schema_version !== undefined && value.schema_version !== TASK_LEASE_SCHEMA_VERSION) ||
(value.goal_id !== undefined && value.goal_id !== goalId) || value.todo_id !== todoId ||
(value.status !== "active" && value.status !== "released")) {
throw new AuthorityStoreProtocolError("canonical lease identity or schema is invalid");
Expand All @@ -23,7 +23,9 @@ export function canonicalTaskLease(value: JsonObject, goalId: string, todoId: st
value.write_scopes.some(scope => typeof scope !== "string"))) {
throw new AuthorityStoreProtocolError("canonical lease write_scopes must be strings");
}
return value;
// Older canonical records may omit the wire tag. Normalize once before
// shared lease rules inspect it, without rewriting the persisted record.
return {...value, schema_version: TASK_LEASE_SCHEMA_VERSION};
}

export function canonicalLeaseTodoFact(todo: JsonObject | undefined): TodoFact | null {
Expand Down
50 changes: 50 additions & 0 deletions loopx/control_plane/coordination/todo_acceptance_restoration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/** Exact rollback of an owner-confirmed declaration grants no execution lease.
* A stale binding can prevent lease acquisition; restoration must consequently
* prove the old declaration without requiring a lease over the changed work. */
import type {JsonObject} from "../effect_program.ts";
import {acceptanceTask, goalAcceptanceTodoDigest, readGoalAcceptance} from "../goals/acceptance_contract.ts";
import {canonicalTaskLease} from "./task_lease_state.ts";
import {leaseIsActive} from "../work_items/task_lease_acquire.ts";
import {prepareUpdatedTodo, type CoordinationTodoUpdateInput} from "./todo_update_intent.ts";

export type AcceptanceRestoration =
| {kind: "exact_restoration"}
| {kind: "unavailable"; reason: string};

/** Runs only after actor/claim/exclusion admission and failed execution proof.
* The caller's existing reviewed-update revision is checked by the transaction;
* its operation receipt handles retries before current-state admission. */
export function acceptanceRestoration(
head: JsonObject, todo: JsonObject, lease: JsonObject | undefined,
input: CoordinationTodoUpdateInput,
): AcceptanceRestoration | null {
const state = readGoalAcceptance(head, input.goal_id);
if (!state?.enabled || acceptanceTask(input.todo_id, todo, state).state !== "stale") return null;
const unavailable = (reason: string): AcceptanceRestoration => ({kind: "unavailable", reason});
if (todo.role !== "agent" || !input.actor_agent_id || todo.claimed_by !== input.actor_agent_id) {
return unavailable("Only the same claimed Agent may restore this declaration; ask the owner to review and rebind the Todo");
}
if (input.lease_idempotency_key != null || input.lease_expected_version != null) {
return unavailable("Restoration cannot consume stale execution proof; release any active lease and retry without lease proof");
}
if (lease !== undefined && leaseIsActive(canonicalTaskLease(lease, input.goal_id, input.todo_id), input.now)) {
return unavailable("Release the active execution lease before restoring the owner-confirmed declaration");
}
if (input.expected_provider_revision === undefined) {
return unavailable("Inspect the current provider revision, then restore the exact prior text/wait with --update-operation-id and --update-expected-provider-revision; if the prior declaration is unknown, ask the owner to review and rebind this Todo");
}
// Restoration is deliberately narrower than ordinary planning. It cannot
// change lifecycle, ownership, validator, effects, or acceptance criteria.
if (input.clear_fields.length || Object.keys(input.patch).some(key => key !== "text") ||
Object.keys(input.planning_intent ?? {}).some(key => !["resume_when", "clear_resume_when"].includes(key)) ||
input.completion !== undefined || input.monitor_observation !== undefined ||
input.completion_validation_revision !== undefined) {
return unavailable("Exact restoration supports only the prior text/wait; ask the owner to review and rebind other work changes");
}
const candidate = prepareUpdatedTodo(todo, input, head).next;
const binding = state.bindings.find(item => item.todo_id === input.todo_id)!;
if (goalAcceptanceTodoDigest(candidate) !== binding.todo_semantic_digest) {
return unavailable("The supplied text/wait does not reconstruct the exact owner-confirmed declaration; ask the owner to review and rebind this Todo");
}
return {kind: "exact_restoration"};
}
4 changes: 2 additions & 2 deletions loopx/control_plane/coordination/todo_deferred_reopen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type {JsonObject} from "../effect_program.ts";
import type {CoordinationProjectionMutation} from "./coordination_projection.ts";
import type {CoordinationTodoUpdateInput} from "./todo_update_intent.ts";
import {canonicalTaskLease} from "./task_lease_state.ts";
import {leaseIsActive, leaseVersion, leaseEpoch, TASK_LEASE_SCHEMA_VERSION} from "../work_items/task_lease_acquire.ts";
import {leaseIsActive, leaseVersion, leaseEpoch} from "../work_items/task_lease_acquire.ts";
import {releasedTaskLeaseRecord} from "../work_items/task_lease_lifecycle_decision.ts";

const REOPEN_FIELDS = new Set(["status", "clear_resume_when", "reason"]);
Expand Down Expand Up @@ -36,7 +36,7 @@ export function deferredReopenRejection(input: {
}
if (input.lease === undefined) return null;
const lease = canonicalTaskLease(input.lease, input.goal_id, input.todo_id);
if (leaseIsActive({...lease, schema_version: TASK_LEASE_SCHEMA_VERSION}, input.now)) {
if (leaseIsActive(lease, input.now)) {
return {code: "deferred_resume_active_lease",
reason: "Release the active execution lease before resuming deferred work"};
}
Expand Down
6 changes: 6 additions & 0 deletions loopx/control_plane/coordination/todo_update_admission.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/** Admission for Todo edits; terminal completion retains its own lease proof.
* Grants may cross a claim owner;
* exclusions, bindings and execution lineage remain independent restrictions. */
import {acceptanceRestoration} from "./todo_acceptance_restoration.ts";
import {monitorMutationRejection} from "./todo_monitor_cycle.ts";
import type {JsonObject} from "../effect_program.ts";
import type {CoordinationTodoUpdateInput} from "./todo_update_intent.ts";
Expand Down Expand Up @@ -131,6 +132,11 @@ export function todoUpdateAdmissionRejection(
lease_idempotency_key: input.lease_idempotency_key ?? null,
lease_expected_version: input.lease_expected_version ?? null, now: input.now});
if (fence.outcome !== "apply") {
const restoration = acceptanceRestoration(head, todo, lease, input);
if (restoration?.kind === "exact_restoration") return null;
if (restoration?.kind === "unavailable") {
return reject("goal_acceptance_restoration_unavailable", restoration.reason);
}
return reject(String(fence.code), "Todo update requires the current active lease execution proof");
}
if (lease !== undefined && todo.claimed_by !== input.actor_agent_id) {
Expand Down
4 changes: 3 additions & 1 deletion loopx/control_plane/goals/goal_frontier/acceptance.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ def acceptance_gaps_from_held_goal_binding(
f"Inspect {todo_id}, its acceptance binding and the owner's contract scope; "
"a local validation contract must not unintentionally hold independent work. "
"Prepare a scope/binding correction for the authorized owner when needed; "
+ ("restore an unintended edit, " if state == "stale" else
+ ("restore the exact prior text/wait using todo update with --update-operation-id "
"and --update-expected-provider-revision after releasing any active lease; "
"if the prior declaration is unknown or cannot match, request owner rebind, " if state == "stale" else
"prepare the missing association for owner review, ") +
"or record an evidence-linked path delta and continue via an eligible "
"successor, or record a concrete blocker for the required owner confirmation. "
Expand Down
Loading
Loading