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 @@ -3210,7 +3210,7 @@ or moving a helper is not by itself a package exit.
| --- | --- | --- |
| A / L1: Monitor configuration (this slice) | Existing `todo update` config enters the TS planner/CAS/receipt; delete Python's duplicate intent field catalog. Separate authoring from observed hashes, times and generations. | Ordinary CLI/API, clear/omission, active lease proof, no-op/replay, failed display delivery, complete fixture and real providers. This does not complete delegated Chat or leased polling. |
| A / L2: Complete public mutation admission | User completion updates share the TS edit/terminal transaction and reviewed Chat recovery; linked decision consumption/reject/cancel/resume now commit with the source, replacing Python followthrough rules. Continue the actual CLI/Turn/Chat inventory for remaining effect-owned decisions, delegated owner actions and Monitor lifecycle transitions; [caller contract](../../reference/canonical-todo-completion-update.md). | Build on merged T1 owners, not a generic raw patch. Prove permission rejection and exact caller response; remove replaced Python admission and name every remaining unsupported command. |
| A / L3: Canonical lease lifecycle | Standalone acquire/takeover, atomic claim lease admission and maintenance reuse TS facts/decision/materialization and one provider opening fence. Explicit claimed-work transfer now commits source-authorized Todo ownership and the new lease generation together; canonical request types exclude legacy held-fence fields. Acquire success verifies current execution proof; canonical completion can recover missing display. | Full-head scope conflict, archived/ineffective holders, exact create-CAS retry, stale execution, process loss and real CLI/four-arm rehearsal are covered. [Operation and remaining callers](../../reference/canonical-lease-renew.md). Executor-held external-effect fences remain explicit work; D1–D3/default holds remain. |
| A / L3: Canonical lease lifecycle | Standalone acquire/takeover, atomic claim lease admission and maintenance reuse TS facts/decision/materialization and one provider opening fence. Explicit claimed-work transfer now commits source-authorized Todo ownership and the new lease generation together; canonical request types exclude legacy held-fence fields. Standalone acquire and atomic claim/acquire share current execution proof, including renewal, retirement and uncertain readback. Canonical commands recheck the operation receipt after loading the head, before new admission; canonical completion can recover missing display. | Full-head scope conflict, archived/ineffective holders, exact create-CAS retry, stale execution, process loss and real CLI/four-arm rehearsal are covered. [Operation and remaining callers](../../reference/canonical-lease-renew.md). Executor-held external-effect fences remain explicit work; D1–D3/default holds remain. |
| B / L4: Leased Monitor poll and settlement | Current execution proof now binds CLI intent, observation/generation/independent-successor CAS and historical business receipt. Quota pending admission is frozen before the business write; recovery preserves that decision after lease retirement. | Existing L3 lease lifecycle, real File/SQLite/PostgreSQL, mixed fixtures, process death between business/quota commits, competing renewal and unchanged polling. [Operation and snapshot rehearsal](../../reference/protocols/quota-monitor-observation-receipt-v0.md). Ordinary polls leave leases unchanged and spend no quota; separate authorities stay separate. The retained grouped-Monitor observation/reactivation caller now uses Todo update v4 and the shared Monitor planner, with unchanged-group display recovery. Canonical reactivation now atomically retires retained execution and reopens the observation cycle, sharing typed admission with polling; a fresh execution still needs explicit acquisition. Grouped reconciliation now acquires/revalidates/releases its own bounded execution, recovers interrupted cleanup, and plans the complete bucket set in TS; missing evidence and ambiguous/stale targets reject. This closes that retained caller across legacy/File/SQLite; native/imported mixed fixtures exercise the same effects on real PostgreSQL. Wider L2 admission, external-effect fences and D1–D3/default remain open. |
| B / L5: Consumer and display closure | Reconcile #4316, audit Turn/quota/Dashboard/Chat source reads, and finish D1 freshness/recovery through the existing projection outbox. | CLI, Lark/Chat and packaged frontend read back their affected interactions; absent/stale display, empty canonical state, pending projection and data beyond UI limits. Delete post-promotion legacy fallbacks with each consumer. |
| A–C / L6: Local durability qualification | Continue contributor-owned #4224/#4328 on the selected SQLite profile; reuse File/NoKV references and complete 7.2's ledger. | Capacity, real process/crash/restore/upgrade, retained receipts/scans, consumer lag, supported runtimes/OS and the separately authorized >=10-day synthetic soak. Missing measurements remain holds. |
Expand All @@ -3233,6 +3233,7 @@ PRs**, conditional on the caller audit finding no additional missing effects:
| L7 capture plus L8 integrated migration | 1–2 | Mixed-writer continuity, fenced whole-Goal rehearsal, export/rollback and cohort evidence. |
| L9 default and bounded retirement | 1 | New-Goal onboarding/settings/install choose the qualified profile; remove final obsolete callers. |

The command-observation/current-proof closure removes a concrete L2/L3 concurrency hold.
The retained-Monitor cycle and grouped executor closure remove concrete L4 holds, not an entire
remaining package: the **5–8 PR planning range remains conditional**, rather than
subtracting one for a lifecycle fix. Actual remaining executor/caller coverage,
Expand Down
12 changes: 12 additions & 0 deletions docs/architecture/rfcs/typescript-control-plane-migration-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@ Retain T0 caller/parity inventory, T1/T2 transaction/effect convergence, T3 comp

## Current implementation checkpoint

Canonical command observation now has one typed receipt/head boundary. Team,
Todo creation/edit/claim/terminal/archive, Monitor, lease maintenance and Goal
acceptance recheck receipts after the head read before interpreting new state.
This repairs same-operation races without provider API changes, write retries
or a Python copy of the decision. Standalone and atomic claim acquisition share
current lease proof; renewed proof is returned without rewriting history, while
retired execution and unavailable current authority cannot return stale success.
This closes a concurrency/current-proof slice of L2/L3, not whole-Goal migration,
default onboarding, contributor-owned SQLite D2 or T4 Python retirement. The
[operator contract](../../reference/canonical-lease-renew.md#commit-retry-and-readback)
distinguishes historical results from present execution.

Terminal review and validation now converge in the existing TS terminal owner.
Agent completion and Monitor stop reuse Chat's canonical receipt-first recovery
and display acknowledgement; v2 binds validation continuation to its source
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@

## 当前实现检查点

Canonical command 的 receipt/head 观察顺序统一归属 TS:团队规划、Todo 创建/
修改/领取/终态/归档、Monitor、lease 维护和 Goal acceptance 在读 head 后复查原
receipt,再执行新准入。这修复同 operation 并发竞争,不扩展 provider API、不
自动重试写入,也不在 Python 复制决定。独立 acquire 与原子 claim/acquire 共用
当前 lease 证明:续租返回新 proof 而保留原 receipt,退役执行或当前 authority
不可读均不能返回旧成功。它只关闭 L2/L3 的并发和当前证明缺口,不代表全 Goal
迁移、默认启用、contributor 的 SQLite D2 或 T4 Python 退役完成。见
[操作和恢复合同](../../reference/canonical-lease-renew.md#commit-retry-and-readback)。

终结审核与验证已收敛到既有 TS terminal owner:Agent 完成、Monitor 停止复用 Chat
先恢复 canonical 回执再确认显示的路径;v2 把验证 continuation 绑定来源 revision,
准入/回放之后才请求私有声明。删除 Python 的终结操作审核分流和提前解析声明编排。
Expand Down
56 changes: 52 additions & 4 deletions docs/reference/canonical-lease-renew.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,49 @@ with the same owner/key/epoch. Replay after renewal returns the current
version/expiry in `lease`, with the unchanged original decision in
`original_receipt`; `current_provider_revision/current_cursor` identify that
readback. A transferred, expired or released execution cannot be revived by its
old receipt. Claim receipts and maintenance receipts retain their historical
semantics; they are not acquire responses.
old receipt. Atomic `todo claim` with `--task-lease-idempotency-key` now uses the
same current-proof owner after commit/recovery, including receipt-only no-ops.
A plain claim without an acquisition request and maintenance receipts retain
historical semantics; they do not grant new execution.

For atomic adoption, freeze both identities across an uncertain response:

```bash
loopx --registry registry.json todo claim \
--goal-id example-goal --todo-id todo_work \
--claimed-by agent-a --agent-id agent-a \
--claim-operation-id adopt-work-a \
--task-lease-idempotency-key execution-a --task-lease-expected-version 0
```

The operation id identifies the claim transaction; the execution key identifies
the lease generation. Repeating this exact command after renewal returns the
renewed lease and the original receipt. After release or expiry it fails with
`idempotency_key_reuse`; after a claim transfer it fails current-owner admission.
Inspect the current state before choosing a new execution key and operation id.
If the receipt exists but the current head cannot be read, the command returns
`ambiguous` with same-operation recovery, not the historical active lease.
Switching away from `hard_lease` also invalidates atomic claim/acquire success.

| Readback | Meaning | Next action |
| --- | --- | --- |
| `replayed`, same epoch, newer lease version | Same execution was renewed | Use the returned current lease version. |
| `idempotency_key_reuse` | The receipt belongs to a retired execution | Inspect, then request a new execution with a new key. |
| `owner_conflicts_with_claim` | Current Todo ownership changed | Let the current owner continue or use an authorized handover. |
| `canonical_acquire_readback_required` | History is known, current proof is unavailable | Restore the provider and retry the same operation. |
| Acceptance/source rejection | Current control-plane authority changed | Resolve that boundary before attempting work. |

A successful readback is still a point-in-time proof, not a lock over subsequent
external effects. Execution must retain its existing mutation fences; this
change does not close the remaining external-effect fencing work.

Canonical commands recheck their receipt after reading the decision head.
This handles a peer committing the same operation between the first absent
receipt and the head read: recovery precedes duplicate-ID, stale-revision,
Monitor-generation and other new-admission checks. The second read does not
lock the head; commits after it still resolve through CAS and receipt recovery.
Archive preview remains a current-state preview with no receipt lookup.
No provider becomes the default and no legacy writer is re-enabled by this change.

The canonical-only acquire and lifecycle requests are closed and versioned. Joint claim
transfer uses `loopx_canonical_task_lease_claim_transfer_request_v0`, so an older
Expand Down Expand Up @@ -299,8 +340,15 @@ expected-version 0 的原样重试可恢复回执,改变参数会拒绝。
Acquire 的成功还必须核对当前有效 owner/key/epoch 和资格。同一执行续约后,
重试返回 `lease` 中的当前版本/到期时间,以及 `original_receipt` 中不可变的原始
决定;`current_provider_revision/current_cursor` 标识当前读回。已转交、到期或释放
的旧执行不能凭 receipt 复活。Todo claim 和维护 receipt 仍是历史语义,不能将其
当成新的 acquire 响应。
的旧执行不能凭 receipt 复活。携带 lease 请求的原子 Todo claim 也共享这项检查,
包括首次提交、丢响应恢复、历史重放和只保存 receipt 的 no-op。普通 claim 和维护
receipt 仍是历史语义,不授予新执行权。当前 head 不可读时返回 ambiguous,要求
沿用原 operation id 恢复;不能把原 receipt 中的 active lease 当作当前证明。

各 canonical 命令在读 head 后再次查原 receipt,解决另一调用恰在第一次查无回执后
提交成功的竞争。回执优先于新一轮的重复 ID、陈旧 revision 和 Monitor generation
校验;之后仍由 CAS 防止覆盖并发更新。归档 dry-run 继续只看当前状态,不重放历史。
公开参数、provider 默认值和 legacy 路径保持不变。

canonical acquire 与 lifecycle 各有封闭 wire,旧 renew wire 只接受 renew;旧
runtime 不识别新 acquire schema。fence、provider、注册源变化和 CAS 错误不回退
Expand Down
18 changes: 17 additions & 1 deletion loopx/control_plane/coordination/command_receipt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Business planners and request hashes remain with their command owners. */
import type {JsonObject} from "../effect_program.ts";
import type {AuthorityStore, AuthorityStoreCommit, AuthorityStoreCommitResult,
AuthorityStoreReceiptResult} from "./authority_store.ts";
AuthorityStoreReceiptResult, AuthorityStoreLoadResult} from "./authority_store.ts";
import {AuthorityStoreProtocolError, canonicalAuthorityObject} from "./authority_store_codec.ts";
import {projectionDelivery} from "../todos/projection_delivery.ts";

Expand All @@ -21,6 +21,12 @@ interface CommandReceiptContract<S extends string> {
}
type Result<S extends string> = JsonObject & {schema_version: S};

/** A head and a receipt are separate reads. The receipt observed after the head
* wins: that head may already include this operation's effects. */
export type CommandObservation<S extends string> =
| {kind: "receipt"; result: Result<S>}
| {kind: "authority"; authority: AuthorityStoreLoadResult};

/** One envelope identity, result projection and post-commit state machine for
* canonical Todo commands. Existing wire schemas and request digests are retained. */
export class CoordinationCommandReceipt<S extends string> {
Expand Down Expand Up @@ -56,6 +62,16 @@ export class CoordinationCommandReceipt<S extends string> {
return this.project(await store.readReceipt(this.contract.identity.operation_id), "replayed");
}

/** Call after the initial historical lookup and command-specific source gates.
* Recheck before interpreting the head (including unavailable/stale heads).
* This is not a transaction or a write retry: a later competing commit remains
* subject to the provider CAS and normal post-commit receipt recovery. */
async observe(store: AuthorityStore): Promise<CommandObservation<S>> {
const authority = await store.loadAuthority();
const replay = await this.read(store);
return replay === null ? {kind: "authority", authority} : {kind: "receipt", result: replay};
}

async commit(store: AuthorityStore, commit: AuthorityStoreCommit): Promise<Result<S>> {
const {identity, result_schema, failure} = this.contract;
if (commit.operation_id !== identity.operation_id) {
Expand Down
65 changes: 65 additions & 0 deletions loopx/control_plane/coordination/lease_acquisition_proof.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/** Historical acquisition and current permission are different facts. Both
* standalone acquire and atomic claim/acquire return this current proof. */
import type {JsonObject} from "../effect_program.ts";
import type {AuthorityStore, AuthorityStoreLoadResult} from "./authority_store.ts";
import {canonicalAuthorityObject} from "./authority_store_codec.ts";
import {indexCoordinationProjection, validateCoordinationTodoReadModel} from "./coordination_projection.ts";
import {canonicalTaskLease, canonicalTaskLeaseAcquireFacts} from "./task_lease_state.ts";
import {HANDOFF_MODES} from "./handoff_mode_policy.ts";
import {requireStringLiteral} from "../runtime_decode.ts";
import {leaseOwnerRejection} from "../work_items/task_lease_eligibility.ts";
import {leaseEpoch, leaseVersion, leaseIsActive} from "../work_items/task_lease_acquire.ts";
import {acceptanceWorkGuard} from "../goals/acceptance_contract.ts";

interface AcquisitionIdentity {
goal_id: string; todo_id: string; owner: string; idempotency_key: string;
registered_agents: readonly string[]; now: Date;
}

/** Decode only a loaded decision snapshot. Receipt precedence must be settled
* before calling this: an already committed operation needs no new admission. */
export function acquisitionFacts(head: AuthorityStoreLoadResult, input: AcquisitionIdentity) {
if (head.status !== "loaded") return {head, facts: null, mode: null};
validateCoordinationTodoReadModel(head.head, input.goal_id);
const index = indexCoordinationProjection(head.head, input.goal_id);
return {head, facts: canonicalTaskLeaseAcquireFacts(index, input.goal_id, input.todo_id, input.registered_agents, input.now),
mode: requireStringLiteral(head.head.handoff_mode ?? "legacy", HANDOFF_MODES, "canonical handoff_mode")};
}

export async function currentLeaseAcquisitionProof<S extends string>(store: AuthorityStore,
input: AcquisitionIdentity & {operation_id: string; required_handoff_mode?: "hard_lease"},
result: JsonObject & {schema_version: S},
failed: (code: string, reason: string, detail?: JsonObject) => JsonObject & {schema_version: S},
): Promise<JsonObject & {schema_version: S}> {
if (!["applied", "no_change", "replayed", "recovered"].includes(String(result.status))) return result;
const recovery = {operation_id: input.operation_id, retry_with_same_operation_id: true};
const unavailable = () => ({...failed("canonical_acquire_readback_required",
"acquisition receipt is durable but current authority is unavailable; retry the same request"),
status: "ambiguous", original_receipt: result.original_receipt, recovery});
let loaded: AuthorityStoreLoadResult;
try { loaded = await store.loadAuthority(); }
catch { return unavailable(); }
const {head, facts, mode} = acquisitionFacts(loaded, input);
if (head.status !== "loaded" || facts === null) return unavailable();
const original = canonicalTaskLease(canonicalAuthorityObject(result.lease, "original acquire lease"), input.goal_id, input.todo_id);
const current = facts.current;
const details = {handoff_mode: mode, original_receipt: result.original_receipt,
current_provider_revision: head.provider_revision, current_cursor: head.cursor};
const rejection = mode === "soft_claim" ? "handoff_mode_forbids_lease"
: input.required_handoff_mode !== undefined && mode !== input.required_handoff_mode ? "claim_lease_requires_hard_lease"
: leaseOwnerRejection(facts.todo, input.owner, input.registered_agents);
if (rejection) return failed(rejection, `current authority rejects lease acquire replay: ${rejection}`, details);
if (!current || !leaseIsActive(current, input.now) || current.owner !== input.owner ||
current.idempotency_key !== input.idempotency_key || leaseEpoch(current) !== leaseEpoch(original) ||
leaseVersion(current) < leaseVersion(original)) {
return failed("idempotency_key_reuse", "acquire receipt belongs to a retired execution; use a new execution key", details);
}
const acceptance = acceptanceWorkGuard(head.head, input.goal_id, input.todo_id);
if (acceptance !== null && !acceptance.allowed) {
return failed(String(acceptance.reason_code), `${String(acceptance.reason)} Inspect Goal acceptance and ask the owner to configure or rebind this Todo.`,
{...details, goal_acceptance_guard: acceptance});
}
// A renewal advances version/expiry within this execution. Never rewrite the
// immutable acquisition receipt to make it look like the renewed decision.
return {...result, ...details, lease: current};
}
Loading
Loading