Skip to content

Commit c33706f

Browse files
authored
feat(authority): promoted todo text/note edit through unified provider CAS (#3963)
* feat(authority): compat edit for promoted todo text/note via provider CAS A narrow compatibility editor is admitted through the TS authority owner for promoted goals. It accepts only text/note patches, requires the exact provider revision, current non-excluded claim owner, agent open active Todo, and no lease; it commits through prepareCoordinationProjectionCommit with a request-sha receipt for exact replay. Markdown is used only as an in-memory codec and is never a commit target or input; projection delivery is pending until the capture outbox is wired. Legacy writers for every other command combination remain on the existing path and stay fail-closed under the promotion fence. This is the first vertical slice of the unified TS commit authority, not a goal promotion. Signed-off-by: huangruiteng <huangrt01@163.com> * test(authority): provider compat edit conformance and CLI end-to-end TS conformance covers concurrent-claim stale-snapshot rejection, exact request-sha replay, lost-response recovery, racing CAS, and invalid-patch rejection. Python CLI e2e runs a real promoted goal with no Markdown file: the in-memory editor feeds only requested fields back through TS CAS, keeps complex sibling fields intact, reports pending projection delivery, and advances updated_at without touching disk. Signed-off-by: huangruiteng <huangrt01@163.com> * docs(rfc): record single-command compat slice as validation milestone Clarify the TS control-plane next-delivery sequence: a single-command compatibility slice (todo update --text/--note) qualifies a synthetic goal and is never a real-goal promotion, because every other legacy writer stays fail-closed under the fence once a goal is promoted. Real-goal promotion waits until the write-command family its agents use routes through the same unified TS commit authority (per-command patch types on one entry point, not parallel adapters) and until capture/projection outbox delivery is flushed. The deletion payoff arrives when the in-place Markdown editor is replaced by a pure projection renderer behind that entry point. Signed-off-by: huangruiteng <huangrt01@163.com> --------- Signed-off-by: huangruiteng <huangrt01@163.com>
1 parent 920ac10 commit c33706f

9 files changed

Lines changed: 411 additions & 0 deletions

‎docs/architecture/rfcs/typescript-control-plane-migration-v0.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,17 @@ slice; they must not be reported as migrated or globally free of prose rules.
127127
the existing TS authority owner. Deliver coherent vertical slices with the
128128
real CLI caller, replay/CAS/error tests, and removal of the replaced Python
129129
decisions. A schema or constants-only PR does not satisfy this exit.
130+
A single-command slice (for example `todo update --text/--note` through a
131+
shared compatibility editor) is a validation milestone for synthetic or
132+
qualification goals, never a real-goal promotion: once a goal is promoted,
133+
every other legacy writer is still fenced fail-closed. Promoting a live goal
134+
therefore waits until the write-command family its agents actually use routes
135+
through the same unified TS commit authority (per-command patch types on one
136+
entry point, not parallel adapters) and until capture/projection outbox
137+
delivery is flushed. The deletion payoff lands only when the in-place
138+
Markdown editor is replaced by a pure projection renderer behind that one
139+
entry point.
140+
130141
2. **Qualification before activation.** Join that path with the shared-authority
131142
RFC's explicit v0 import, consumer parity, writer fencing, capture/projection
132143
outbox recovery and fenced export. Integrate the local-persistence slice

‎docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,13 @@ lifecycle classification code 或其他 cadence policy,不能宣称全局已
100100
complete-with-successor、archive 及相关 lease effect 经过现有 TS authority owner。
101101
每个内聚的纵向切片包含真实 CLI caller、replay/CAS/error 测试,并删除被替代的
102102
Python decision。仅统一 schema 或常量不满足退出条件。
103+
单命令切片(例如通过同一兼容 editor 的 `todo update --text/--note`)只是
104+
合成/资格化 goal 的验证里程碑,不是真实 goal 的 promote:一旦 promote,其余
105+
legacy writer 仍被 fence 以 fail-closed 拦截。因此活跃 goal 的 promote 要等
106+
到其 agent 实际使用的写命令族都经由同一个统一 TS 提交权威(同一入口的分命令
107+
patch 类型,而不是平行 adapter),并且 capture/projection outbox 落盘接通之后。
108+
删除的收益只在该入口背后的 in-place Markdown editor 被纯投影 renderer 替代时兑现。
109+
103110
2. **先资格化,再启用。** 与 shared-authority RFC 的显式 v0 import、consumer
104111
parity、writer fencing、capture/projection outbox recovery 和 fenced export 汇合。
105112
集成上述本地持久化切片,包含历史 receipt 保留、容量与 >=10 天 soak 证据。

‎loopx/control_plane/coordination/local_authority_runtime.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
COORDINATION_TODO_CLAIM_RESULT_SCHEMA,
4242
executeCoordinationTodoClaim,
4343
} from "./todo_claim.ts";
44+
import { editCoordinationTodo, TODO_COMPATIBILITY_EDIT_RESULT_SCHEMA } from "./todo_compatibility_edit.ts";
4445

4546
export const LOCAL_COORDINATION_TODO_CLAIM_REQUEST_SCHEMA =
4647
"loopx_local_coordination_todo_claim_request_v0";
@@ -577,6 +578,27 @@ export async function claimLocalCoordinationTodo(
577578
}
578579
}
579580

581+
/** Embedded file adapter; no Markdown input or projection write is accepted. */
582+
export async function editLocalCoordinationTodo(
583+
value: unknown,
584+
dependencies: LocalAuthorityRuntimeDependencies = {},
585+
): Promise<JsonObject> {
586+
try {
587+
const input = requireJsonObject(value, "local compatibility edit");
588+
const {runtime_root, ...request} = input;
589+
const root = runtimeRoot(runtime_root);
590+
const goalId = requireAuthorityStoreId(input.goal_id, "goal id");
591+
const store = dependencies.createStore?.(authorityDirectory(root), goalId) ??
592+
new FileAuthorityStore(authorityDirectory(root), goalId);
593+
return {...await editCoordinationTodo(store, request),
594+
source_authority: "file_v0", decision_read_from_provider: true, legacy_fallback_used: false};
595+
} catch (error) {
596+
return {schema_version: TODO_COMPATIBILITY_EDIT_RESULT_SCHEMA, status: "failed",
597+
reason_code: "invalid_local_compatibility_edit", changed: false,
598+
reason: error instanceof Error ? error.message : "invalid local compatibility edit"};
599+
}
600+
}
601+
580602
/** Provider-first exact Todo read. Missing/unavailable state never falls back. */
581603
export async function readLocalCoordinationTodo(
582604
value: unknown,
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import type { JsonObject } from "../effect_program.ts";
2+
import type { AuthorityStore } from "./authority_store.ts";
3+
import { canonicalAuthorityBytes, canonicalAuthorityObject, canonicalAuthoritySha256, requireAuthorityStoreId } from "./authority_store_codec.ts";
4+
import { prepareCoordinationProjectionCommit, indexCoordinationProjection, validateCoordinationTodoReadModel } from "./coordination_projection.ts";
5+
6+
export const TODO_COMPATIBILITY_EDIT_SCHEMA = "loopx_todo_compatibility_edit_request_v0";
7+
export const TODO_COMPATIBILITY_EDIT_RESULT_SCHEMA = "loopx_todo_compatibility_edit_result_v0";
8+
9+
/**
10+
* A compatibility editor proposes only text/note changes against an exact
11+
* provider revision. It cannot submit a snapshot, change ownership/lifecycle,
12+
* clear unrepresented fields, or make Markdown the commit authority.
13+
*
14+
* This trusted embedded entrypoint does not grant remote service authority.
15+
* Conflict requires rereading and rerunning the editor, never snapshot rebasing.
16+
*/
17+
export async function editCoordinationTodo(
18+
store: AuthorityStore,
19+
value: unknown,
20+
): Promise<JsonObject> {
21+
const failure = (reason_code: string, reason: string): JsonObject => ({
22+
schema_version: TODO_COMPATIBILITY_EDIT_RESULT_SCHEMA,
23+
status: "failed", changed: false, reason_code, reason,
24+
});
25+
try {
26+
const input = canonicalAuthorityObject(value, "Todo compatibility edit");
27+
if (input.schema_version !== TODO_COMPATIBILITY_EDIT_SCHEMA) {
28+
throw new Error("Todo compatibility edit schema mismatch");
29+
}
30+
const allowed = new Set(["schema_version", "goal_id", "todo_id", "operation_id",
31+
"expected_provider_revision", "actor_agent_id", "registered_agents", "patch", "dry_run", "observed_at"]);
32+
if (Object.keys(input).some((key) => !allowed.has(key))) {
33+
throw new Error("Todo compatibility edit contains unsupported fields");
34+
}
35+
const goalId = requireAuthorityStoreId(input.goal_id, "goal id");
36+
const todoId = requireAuthorityStoreId(input.todo_id, "todo id");
37+
const operationId = requireAuthorityStoreId(input.operation_id, "operation id");
38+
const revision = requireAuthorityStoreId(input.expected_provider_revision, "expected provider revision");
39+
const actor = requireAuthorityStoreId(input.actor_agent_id, "actor agent id");
40+
if (typeof input.observed_at !== "string" ||
41+
!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/u.test(input.observed_at) ||
42+
Number.isNaN(Date.parse(input.observed_at))) {
43+
throw new Error("observed_at must be an ISO timestamp");
44+
}
45+
const updatedAt = new Date(input.observed_at).toISOString();
46+
if (!Array.isArray(input.registered_agents) ||
47+
input.registered_agents.some((agent) => typeof agent !== "string") ||
48+
new Set(input.registered_agents).size !== input.registered_agents.length) {
49+
throw new Error("registered_agents must be a unique string array");
50+
}
51+
if (typeof input.dry_run !== "boolean") throw new Error("dry_run must be a boolean");
52+
const patch = canonicalAuthorityObject(input.patch, "compatibility patch");
53+
if (Object.keys(patch).length === 0 || Object.entries(patch).some(([key, item]) =>
54+
!["text", "note"].includes(key) || typeof item !== "string" || !item.trim()
55+
)) {
56+
throw new Error("compatibility patch accepts only non-empty text and note strings");
57+
}
58+
const requestSha = canonicalAuthoritySha256({
59+
goal_id: goalId, todo_id: todoId, actor_agent_id: actor,
60+
expected_provider_revision: revision, patch,
61+
});
62+
const readReceipt = async (status: string): Promise<JsonObject | null> => {
63+
const receipt = await store.readReceipt(operationId);
64+
if (receipt.status === "missing") return null;
65+
if (receipt.status !== "found") return {schema_version: TODO_COMPATIBILITY_EDIT_RESULT_SCHEMA, ...receipt, changed: false};
66+
const original = receipt.receipts[0];
67+
if (receipt.receipts.length !== 1 ||
68+
original?.schema_version !== "loopx_todo_compatibility_edit_receipt_v0" ||
69+
original.operation_id !== operationId || original.request_sha256 !== requestSha ||
70+
typeof original.changed !== "boolean") {
71+
return failure("coordination_operation_identity_mismatch", "operation id names a different compatibility edit");
72+
}
73+
return {
74+
schema_version: TODO_COMPATIBILITY_EDIT_RESULT_SCHEMA,
75+
status: status === "applied" && !original.changed ? "no_change" : status,
76+
changed: status !== "replayed" && original.changed,
77+
provider_revision: receipt.provider_revision, cursor: receipt.cursor,
78+
projection_delivery: original.changed ? "pending" : "not_required",
79+
projection_source: "committed_authority_journal",
80+
};
81+
};
82+
// Preview evaluates current eligibility and never consumes/replays identity.
83+
if (!input.dry_run) {
84+
const replay = await readReceipt("replayed");
85+
if (replay !== null) return replay;
86+
}
87+
if (!input.registered_agents.includes(actor)) {
88+
return failure("actor_not_registered", "compatibility edit requires a registered actor");
89+
}
90+
const head = await store.loadAuthority();
91+
if (head.status !== "loaded") return {schema_version: TODO_COMPATIBILITY_EDIT_RESULT_SCHEMA, ...head, changed: false};
92+
validateCoordinationTodoReadModel(head.head, goalId);
93+
const index = indexCoordinationProjection(head.head, goalId);
94+
if (head.provider_revision !== revision) return {
95+
schema_version: TODO_COMPATIBILITY_EDIT_RESULT_SCHEMA,
96+
status: "conflict", changed: false, conflict_kind: "provider_revision_mismatch",
97+
current_provider_revision: head.provider_revision, current_cursor: head.cursor,
98+
};
99+
const todo = index.todos.get(todoId);
100+
if (todo === undefined) return failure("todo_not_found", "canonical Todo is missing");
101+
if (todo.role !== "agent" || todo.status !== "open" || todo.archive_state !== "active") {
102+
return failure("unsupported_compatibility_edit_target", "compatibility editing requires an active open agent Todo");
103+
}
104+
if (todo.claimed_by !== actor ||
105+
(Array.isArray(todo.excluded_agents) && todo.excluded_agents.includes(actor)) ||
106+
(typeof todo.removed_continuation_policy === "string" && todo.removed_continuation_policy)) {
107+
return failure("compatibility_edit_owner_mismatch", "compatibility editing requires the current non-excluded claim owner");
108+
}
109+
// Lease-bearing edits need a separate execution-instance proof; do not
110+
// infer that proof merely from actor identity or bypass the existing fence.
111+
if (![undefined, "legacy", "soft_claim"].includes(head.head.handoff_mode as string | undefined) || index.leases.has(todoId)) {
112+
return failure("compatibility_edit_lease_unsupported", "lease-bearing compatibility edits are not yet supported");
113+
}
114+
const next = {...todo, ...patch};
115+
const changed = !canonicalAuthorityBytes(next).equals(canonicalAuthorityBytes(todo));
116+
if (input.dry_run) return {
117+
schema_version: TODO_COMPATIBILITY_EDIT_RESULT_SCHEMA, status: changed ? "planned" : "no_change",
118+
changed, provider_revision: revision, cursor: head.cursor,
119+
};
120+
if (changed) next.updated_at = updatedAt;
121+
const commit = changed ? prepareCoordinationProjectionCommit({
122+
goal_id: goalId, operation_id: operationId,
123+
expected_provider_revision: revision,
124+
projection: head.head,
125+
mutations: [{kind: "todo_upsert", todo: next}],
126+
}) : {operation_id: operationId, expected_provider_revision: revision,
127+
next_projection: head.head, events: [], receipts: []};
128+
commit.receipts = [{schema_version: "loopx_todo_compatibility_edit_receipt_v0",
129+
operation_id: operationId, request_sha256: requestSha, changed}];
130+
const result = await store.commitAuthority(commit);
131+
const readback = await readReceipt(result.status === "applied" ? "applied" : "recovered");
132+
if (readback !== null) return readback;
133+
if (result.status === "applied") return failure("compatibility_commit_readback_mismatch", "applied edit lacks its durable receipt");
134+
return {
135+
schema_version: TODO_COMPATIBILITY_EDIT_RESULT_SCHEMA, ...result,
136+
changed: false,
137+
};
138+
} catch (error) {
139+
return failure("invalid_todo_compatibility_edit", error instanceof Error ? error.message : "invalid compatibility edit");
140+
}
141+
}

‎loopx/control_plane/effect_runtime_handlers.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ import {
114114
} from "./coordination/runtime_shadow.ts";
115115
import {
116116
claimLocalCoordinationTodo,
117+
editLocalCoordinationTodo,
117118
mutateLocalCoordinationAuthority,
118119
listLocalCoordinationTodos,
119120
promoteLocalCoordinationAuthority,
@@ -391,6 +392,7 @@ export function createEffectRuntimeHandlers(
391392
["coordination.runtime_shadow.rollback", rollbackCoordinationRuntimeShadow],
392393
["coordination.local_authority.promote", promoteLocalCoordinationAuthority],
393394
["coordination.local_authority.todo_claim", claimLocalCoordinationTodo],
395+
["coordination.local_authority.todo_compatibility_edit", editLocalCoordinationTodo],
394396
["coordination.local_authority.mutate", mutateLocalCoordinationAuthority],
395397
["coordination.local_authority.todo_read", readLocalCoordinationTodo],
396398
["coordination.local_authority.todo_list", listLocalCoordinationTodos],
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""In-memory Markdown editing adapter; the TS provider owns the commit.
2+
3+
Only the explicitly requested text/note fields cross back. Unrepresented
4+
canonical fields, section/index provenance and lease records never round-trip
5+
through Markdown. The on-disk projection is not an input or a commit target.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
from pathlib import Path
11+
from typing import Any
12+
from uuid import uuid4
13+
14+
from ...agent_registry import registered_agent_ids_from_registry
15+
from ...state_refresh import now_local
16+
from ..coordination.local_authority import (
17+
LocalCoordinationAuthorityUnavailable,
18+
read_canonical_todos_if_promoted,
19+
)
20+
from ..effect_runtime import effect_runtime_result
21+
from .active_state_editing import find_todo_block, set_todo_text
22+
from .contract import format_todo_metadata_line, metadata_line_for_todo_block
23+
from .line_update import upsert_todo_metadata
24+
25+
26+
def edit_canonical_todo_if_promoted(
27+
*, registry_path: Path, runtime_root: Path, goal_id: str, todo_id: str,
28+
actor_agent_id: str | None, role: str | None, text: str | None,
29+
note: str | None, dry_run: bool,
30+
) -> dict[str, Any] | None:
31+
canonical = read_canonical_todos_if_promoted(runtime_root=runtime_root, goal_id=goal_id)
32+
if canonical is None:
33+
return None
34+
todo = next((item for item in canonical["todos"] if item["todo_id"] == todo_id), None)
35+
if todo is None:
36+
raise ValueError("Todo is missing from canonical authority")
37+
if role is not None and role != todo["role"]:
38+
raise ValueError("Todo does not have the requested role")
39+
# Reuse the existing Markdown text/metadata editor as a codec, not an
40+
# authorization engine. This buffer is synthetic and is never persisted.
41+
lines = ["## Agent Todo", "", f"- [ ] {todo['text']}",
42+
format_todo_metadata_line(todo_id=todo_id, note=todo.get("note"))]
43+
found = find_todo_block(lines, todo_id=todo_id, role="agent")
44+
if found is None:
45+
raise ValueError("canonical Todo cannot be represented by the compatibility editor")
46+
block = found[4]
47+
if text is not None:
48+
set_todo_text(lines, block, text, status="open")
49+
if note is not None:
50+
upsert_todo_metadata(lines, block, metadata_line_for_todo_block(block, {"note": note}))
51+
edited = find_todo_block(lines, todo_id=todo_id, role="agent")
52+
if edited is None:
53+
raise ValueError("compatibility editor lost Todo identity")
54+
patch = {field: edited[4].get(field) for field, requested in
55+
(("text", text), ("note", note)) if requested is not None}
56+
result = effect_runtime_result("coordination.local_authority.todo_compatibility_edit", {
57+
"schema_version": "loopx_todo_compatibility_edit_request_v0",
58+
"runtime_root": str(runtime_root.resolve()), "goal_id": goal_id,
59+
"todo_id": todo_id, "actor_agent_id": actor_agent_id,
60+
"registered_agents": registered_agent_ids_from_registry(registry_path, goal_id),
61+
"operation_id": f"todo-edit:{uuid4().hex}",
62+
"expected_provider_revision": canonical["provider_revision"],
63+
"patch": patch, "dry_run": dry_run,
64+
"observed_at": now_local(),
65+
})
66+
if not isinstance(result, dict) or result.get("status") not in {
67+
"applied", "recovered", "replayed", "no_change", "planned",
68+
} or result.get("source_authority") != "file_v0" or (
69+
result.get("decision_read_from_provider") is not True
70+
or result.get("legacy_fallback_used") is not False
71+
):
72+
payload = result if isinstance(result, dict) else {}
73+
raise LocalCoordinationAuthorityUnavailable(
74+
str(payload.get("reason") or "canonical compatibility edit failed; reread before retry"),
75+
code=str(payload.get("reason_code") or payload.get("conflict_kind")
76+
or "compatibility_edit_failed"), payload=payload,
77+
)
78+
return {"ok": True, "goal_id": goal_id, "todo_id": todo_id,
79+
"role": todo["role"], "dry_run": dry_run, **result}

‎loopx/todos.py‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@
125125
local_authority_is_promoted,
126126
read_canonical_todos_if_promoted,
127127
)
128+
from .control_plane.todos.provider_compatibility_edit import edit_canonical_todo_if_promoted
128129
from .control_plane.todos.handoff_mode import (
129130
enter_added_todo_ownership_handoff_gate,
130131
enter_todo_ownership_handoff_gate,
@@ -1102,6 +1103,27 @@ def update_goal_todo(
11021103
)
11031104
if canonical_claim is not None:
11041105
return canonical_claim
1106+
# A narrow compatibility editor is admitted through provider CAS. All
1107+
# other legacy writes still encounter the existing promotion fence.
1108+
if not claim_only and (text is not None or note is not None) and not any((
1109+
monitor_metadata,
1110+
goal_bound, clear_blocks_agent, clear_excluded_agents, global_gate,
1111+
clear_global_gate, clear_resume_when, clear_claim, authority_reason,
1112+
)) and all(value is None for value in (
1113+
status, evidence, reason, task_class, action_kind, task_domain,
1114+
task_repository, continuation_policy, required_write_scopes,
1115+
required_capabilities, target_capabilities, explore_result_node_refs,
1116+
decision_scope, required_decision_scopes, claimed_by, bound_agent,
1117+
blocks_agent, excluded_agents, unblocks_todo_id, successor_todo_ids,
1118+
resume_when, no_followup, authority_reason,
1119+
)):
1120+
canonical_edit = edit_canonical_todo_if_promoted(
1121+
registry_path=registry_path, runtime_root=shadow_runtime_root,
1122+
goal_id=goal_id, todo_id=normalize_todo_id(todo_id) or todo_id,
1123+
actor_agent_id=agent_id, role=role, text=text, note=note, dry_run=dry_run,
1124+
)
1125+
if canonical_edit is not None:
1126+
return canonical_edit
11051127
resolved_project, resolved_state_file = resolve_todo_state_path(
11061128
registry_path=registry_path,
11071129
goal_id=goal_id,

0 commit comments

Comments
 (0)