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 @@ -3079,6 +3079,14 @@ provider conformance cover the consumer family. See [operation and semantic
changes](../../reference/todo-continuation-readback.md). This closes a bounded
L5/L7 gap; permanent projection delivery/recovery, D2 and D3 are still open.

D1 delivery confirmation now follows durable Markdown readback with a typed
canonical revision check. Unpinned settlement retries up to three times using
the returned complete snapshot; pinned projection never silently retargets.
Overlap, churn and confirmation outage remain pending without repeating business
commits. This qualifies the bounded delivery/retry boundary, not permanent
freshness, a background drainer, all L5 consumers or D2/D3. See the
[projection contract](../../reference/protocols/active-state-structured-projection-v0.md).

**D2 — qualify exactly one local profile; independent of PostgreSQL deployment.**

- Reconcile the SQLite candidate #4121 with Section 7.2 before adding code.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2429,6 +2429,11 @@ route planner 本身仍不授予权限。CLI 将已提交回执交给既有 jour
- caller 迁走后才删除旧 projection repair/receipt 路径。退出条件是可复核的
freshness/readback 和可操作修复路径,不能只证明成功渲染过一次。

D1 交付确认现于 Markdown 耐久读回后核对 canonical revision。未固定版本的结算
最多追赶三次,复用返回的完整快照;固定版本不擅自换目标。并发、持续变化及确认故障
保留 pending,不重做业务提交。这闭合有界交付/重试,不代表永久新鲜度、后台 drain、
全部 L5 或 D2/D3;见[投影合同](../../reference/protocols/active-state-structured-projection-v0.md)。

**D2 — 资格化一个本地 profile,不等待 PostgreSQL 部署。**

- 写代码前对齐 #4121 SQLite 候选与第 7.2 节;资格化及批准前保持 opt-in。
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 @@ -1766,3 +1766,15 @@ new capability/provider, or Python storage migration is introduced. Python keeps
input normalization and rendering until their actual host consumers migrate.
See [the read contract](../../reference/todo-work-counts.md); broader L5/D1 and
local-default qualifications remain open.

### T2 canonical read and display confirmation boundary

Canonical single-Todo and full-source reads now have one read-only TypeScript
module, separate from mutation orchestration and sharing the provider opening
boundary. Projection delivery composes a revision confirmation with the existing
full-source read; ordinary callers retain their response shape. Python owns
physical Markdown durability/retry, not the current-head comparison. Three-attempt
recovery and pinned-intent preservation use the existing journal-backed path;
no new RPC method, durable ACK or provider default. The stronger confirmation
costs one additional read on a stable delivery. Full L5/D1 qualification, D2 and
cutover remain open; see the [projection contract](../../reference/protocols/active-state-structured-projection-v0.md).
Original file line number Diff line number Diff line change
Expand Up @@ -219,11 +219,14 @@ freshness guarantee.
Each section includes a compact `loopx:todo-section-projection-v0` marker with
the canonical provider revision and a SHA-256 digest of the complete canonical
records for that role. The marker is lineage evidence, not a write API.
The command proves that the rendered records came from the exact provider head
observed at read time. It does not claim that the revision remains the current
head after that read; a later canonical mutation makes the Markdown projection
stale until the journal-backed delivery replays. Consumers must always read the
provider, never the Markdown marker, when they need current authority state.
The command proves that the rendered records came from an exact provider head.
Execution now also reads authority **after** durable file readback: `delivered`
and `current` require the rendered revision to match that observed head. This
strengthens the previous read-time provenance contract; a successful file write
alone no longer acknowledges delivery when an overlapping commit is observed.
`observed_provider_revision` names the confirmation point, not a lock on future
commits. Later mutations can still make the display stale. Consumers must always
read the provider, never the Markdown marker, for current authority state.

Rollback is intentionally asymmetric. Before promotion, the existing shadow
rollback quarantines the candidate provider lineage and Markdown remains
Expand Down Expand Up @@ -278,11 +281,44 @@ concurrently restored document. When bytes already match, execution still syncs
the file and parent directory before reporting `current`: a previous failure
may have occurred after rename but before directory durability. A failed barrier
keeps delivery `pending` and does not acknowledge or repeat the business mutation.
Preview remains read-only.
Preview remains read-only and does not request a delivery confirmation.

Unpinned mutation settlement makes at most three delivery attempts under the
existing display lock, reusing a newer complete read for the next attempt. A
pinned `project-markdown --provider-revision` checks its basis before writing and
never silently retargets another revision. An overlap after its write returns
`pending`, the rendered and observed revisions, `delivery_attempts`, and
`retry_business_mutation=false`. Persistent churn also returns pending rather
than looping indefinitely. A confirmation outage preserves the successful
business commit and remains retryable through the existing projection path.
Archived Monitor material generations use the same numeric decoder as active
reads and capture. Textual metadata such as `material_change_generation=12`
round-trips to the canonical integer; zero remains present and mismatched values
still fail parity. This fixes full-document recovery rejected by retained archived
Monitors without rewriting their authority records.

No new queue, persistent ACK, background worker, authority write or provider
default is introduced. Ordinary list/exact reads keep their response shape;
only the internal projection readback request opts into confirmation metadata.

The TypeScript read owner validates complete canonical data and compares the
host's durable readback revision with the same loaded head. Python retains
Markdown ownership, durability, bounded IO retry and rendering. A missing
confirmation from a downlevel runtime cannot be treated as delivery success.
The normal successful execution adds one provider read; each caught-up attempt
reuses the already returned full snapshot. This is a freshness cost, not a
latency improvement or atomic transaction across the database and filesystem.

中文:普通状态与投影共用原子落盘;缺失展示通过仅创建方式发布,避免覆盖并发恢复。
字节相同的执行重试也重新完成文件和目录耐久化,之后才报告 `current`;失败继续
保留“业务已提交、展示 pending”,不确认投影交付、不重执行业务。预览不写入。
字节相同的执行重试也重新完成文件和目录耐久化。现在还必须在落盘后重新读取 authority,
由 TS 核对版本,才能确认 `current/delivered`。这加强了旧的“读取时来源正确”合同;
确认只对应一次观察点,不承诺之后永不变旧。未固定版本的交付最多尝试三次,复用较新
完整快照;显式 `--provider-revision` 不自动换目标。持续并发或确认失败保留业务提交,
展示返回 pending,重试只恢复展示,不重复业务。缺失文件的第二次追赶使用普通原子
替换,不能继续误用仅创建写入。预览不写入,也不确认交付。未新增队列、持久 ACK、
后台任务或默认 provider;普通读取形状不变,正常交付增加一次真实 provider 读取。
归档 Monitor 的代数元数据复用现有整数解码,修复字符串与整数比较造成的整份恢复失败;
零值仍保留,语义不一致仍拒绝,不改写 canonical 记录。

Supported non-Monitor Agent updates include action/domain/repository and required
write scopes, required/target capabilities and Explore node references. These
Expand Down
3 changes: 2 additions & 1 deletion loopx/control_plane/coordination/handoff_mode_runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import type {JsonObject} from "../effect_program.ts";
import {requireJsonObject} from "../runtime_decode.ts";
import {requireAuthorityStoreId} from "./authority_store_codec.ts";
import {openLocalAuthorityStore, localAuthorityOpenFailure} from "./local_authority_provider.ts";
import {runtimeRoot, sourceAuthorityFor} from "./local_authority_runtime.ts";
import {requireLocalAuthorityRuntimeRoot as runtimeRoot} from "./local_authority_provider.ts";
import {authorityStoreSourceAuthority as sourceAuthorityFor} from "./authority_store.ts";
import {withCanonicalWriter} from "./local_authority_write.ts";
import {ShadowManagementError} from "./shadow_management.ts";
import {executeHandoffModeSet, HANDOFF_MODE_SET_SCHEMA} from "./handoff_mode_transaction.ts";
Expand Down
12 changes: 12 additions & 0 deletions loopx/control_plane/coordination/local_authority.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ def claim_canonical_todo_if_promoted(

def read_canonical_todos_if_promoted(
*, runtime_root: Path, goal_id: str, include_leases: bool = False,
projection_readback: Mapping[str, Any] | None = None,
) -> dict[str, Any] | None:
"""Return canonical Todos after cutover, or ``None`` before cutover.

Expand All @@ -202,6 +203,7 @@ def read_canonical_todos_if_promoted(
"runtime_root": str(runtime_root.expanduser().resolve(strict=False)),
"goal_id": goal_id,
**({"include_leases": True} if include_leases else {}),
**({"projection_readback": dict(projection_readback)} if projection_readback is not None else {}),
},
)
if not isinstance(result, Mapping):
Expand Down Expand Up @@ -245,6 +247,16 @@ def read_canonical_todos_if_promoted(
"canonical Todo/lease snapshot is incomplete", code="local_authority_snapshot_incomplete",
payload=payload,
)
if projection_readback is not None:
confirmation = payload.get("projection_readback")
if (not isinstance(confirmation, Mapping)
or confirmation.get("provider_revision") != projection_readback["provider_revision"]
or confirmation.get("observed_provider_revision") != payload.get("provider_revision")
or confirmation.get("status") not in {"pending", "delivered", "current"}):
raise LocalCoordinationAuthorityUnavailable(
"canonical projection confirmation is missing or invalid",
code="local_authority_projection_confirmation_invalid", payload=payload,
)
return payload


Expand Down
21 changes: 21 additions & 0 deletions loopx/control_plane/coordination/local_authority_provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ export type LocalPostgreSqlAuthorityFactory = (
) => Promise<AuthorityStore> | AuthorityStore;

export interface LocalAuthorityProviderDependencies {
/** Existing injected runtime store seam; production uses the configured provider. */
createStore?: (directory: string, goalId: string) => AuthorityStore;
/** Service-owned hook for the medium-term PostgreSQL profile. */
openPostgresqlStore?: LocalPostgreSqlAuthorityFactory;
}
Expand Down Expand Up @@ -265,6 +267,25 @@ export async function selectLocalSqliteAuthority(root: string, goalId: string, e
});
}

/** One runtime seam owns provider construction for every local command. */
export async function openRuntimeAuthorityStore(
root: string,
goalId: string,
dependencies: LocalAuthorityProviderDependencies,
): Promise<AuthorityStore> {
if (dependencies.createStore !== undefined) {
return dependencies.createStore(join(root, "authority", "file-v0"), goalId);
}
return await openLocalAuthorityStore(root, goalId, dependencies);
}

export function requireLocalAuthorityRuntimeRoot(value: unknown): string {
if (typeof value !== "string" || value.trim() !== value || !isAbsolute(value)) {
throw new Error("runtime_root must be an absolute path");
}
return value;
}

// Narrow administrative entrypoint; business writes continue through loopx todo.
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
try {
Expand Down
139 changes: 139 additions & 0 deletions loopx/control_plane/coordination/local_authority_read.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/** Canonical Todo reads and projection confirmation share one provider snapshot. */
import type {JsonObject} from "../effect_program.ts";
import {requireJsonObject} from "../runtime_decode.ts";
import {acceptanceWorkGuard, projectGoalAcceptance} from "../goals/acceptance_contract.ts";
import {authorityStoreSourceAuthority as sourceAuthorityFor} from "./authority_store.ts";
import {requireAuthorityStoreId} from "./authority_store_codec.ts";
import {openRuntimeAuthorityStore as openRuntimeStore, requireLocalAuthorityRuntimeRoot as runtimeRoot,
localAuthorityOpenFailure, type LocalAuthorityProviderDependencies} from "./local_authority_provider.ts";
import {indexCoordinationProjection, indexCoordinationProjectionTodos, validateCoordinationTodoReadModel} from "./coordination_projection.ts";
import {LOCAL_COORDINATION_TODO_LIST_REQUEST_SCHEMA, LOCAL_COORDINATION_TODO_LIST_RESULT_SCHEMA,
LOCAL_COORDINATION_TODO_READ_REQUEST_SCHEMA, LOCAL_COORDINATION_TODO_READ_RESULT_SCHEMA} from "./coordination_state_contract.generated.ts";
import {decodeProjectionReadback, confirmProjectionReadback} from "../todos/projection_delivery.ts";

/** Provider-first exact Todo read. Missing/unavailable state never falls back. */
export async function readLocalCoordinationTodo(
value: unknown,
dependencies: LocalAuthorityProviderDependencies = {},
): Promise<JsonObject> {
let sourceAuthority = "file_v0";
try {
const input = requireJsonObject(value, "local coordination Todo read request");
if (input.schema_version !== LOCAL_COORDINATION_TODO_READ_REQUEST_SCHEMA) {
throw new Error("local coordination Todo read request schema mismatch");
}
const root = runtimeRoot(input.runtime_root);
const goalId = requireAuthorityStoreId(input.goal_id, "goal id");
const todoId = requireAuthorityStoreId(input.todo_id, "todo id");
const store = await openRuntimeStore(root, goalId, dependencies);
sourceAuthority = sourceAuthorityFor(store);
const head = await store.loadAuthority();
if (head.status !== "loaded") {
return {
schema_version: LOCAL_COORDINATION_TODO_READ_RESULT_SCHEMA,
...head,
source_authority: sourceAuthority,
decision_read_from_provider: true,
legacy_fallback_used: false,
};
}
const projection = indexCoordinationProjectionTodos(head.head, goalId);
validateCoordinationTodoReadModel(head.head, goalId);
const todo = projection.todos.get(todoId);
const acceptance = todo === undefined ? null : acceptanceWorkGuard(head.head, goalId, todoId);
return {
schema_version: LOCAL_COORDINATION_TODO_READ_RESULT_SCHEMA,
status: todo === undefined ? "missing" : "found",
todo_id: todoId,
...(todo === undefined ? {} : { todo }),
...(acceptance === null ? {} : {goal_acceptance_guard: acceptance}),
todo_ids: projection.todo_ids,
provider_revision: head.provider_revision,
cursor: head.cursor,
source_authority: sourceAuthority,
decision_read_from_provider: true,
legacy_fallback_used: false,
};
} catch (error) {
return {
schema_version: LOCAL_COORDINATION_TODO_READ_RESULT_SCHEMA,
status: "failed",
reason_code: "invalid_local_coordination_todo_read_request",
reason: error instanceof Error ? error.message : "invalid Todo read request",
source_authority: sourceAuthority,
decision_read_from_provider: true,
legacy_fallback_used: false,
...localAuthorityOpenFailure(error),
};
}
}

/** Provider-first Todo collection read. Missing/unavailable state never falls back. */
export async function listLocalCoordinationTodos(
value: unknown,
dependencies: LocalAuthorityProviderDependencies = {},
): Promise<JsonObject> {
let sourceAuthority = "file_v0";
try {
const input = requireJsonObject(value, "local coordination Todo list request");
if (input.schema_version !== LOCAL_COORDINATION_TODO_LIST_REQUEST_SCHEMA) {
throw new Error("local coordination Todo list request schema mismatch");
}
const readback = input.projection_readback === undefined ? null : decodeProjectionReadback(input.projection_readback);
if (input.include_leases !== undefined && typeof input.include_leases !== "boolean") {
throw new Error("include_leases must be a boolean");
}
const root = runtimeRoot(input.runtime_root);
const goalId = requireAuthorityStoreId(input.goal_id, "goal id");
const store = await openRuntimeStore(root, goalId, dependencies);
sourceAuthority = sourceAuthorityFor(store);
const head = await store.loadAuthority();
if (head.status !== "loaded") {
return {
schema_version: LOCAL_COORDINATION_TODO_LIST_RESULT_SCHEMA,
...head,
source_authority: sourceAuthority,
decision_read_from_provider: true,
legacy_fallback_used: false,
};
}
const projection = indexCoordinationProjectionTodos(head.head, goalId);
const todoReadModel = validateCoordinationTodoReadModel(head.head, goalId);
const leaseIndex = input.include_leases === true
? indexCoordinationProjection(head.head, goalId) : null;
const acceptance = projectGoalAcceptance(head.head, goalId);
return {
schema_version: LOCAL_COORDINATION_TODO_LIST_RESULT_SCHEMA,
status: "loaded",
...(readback === null ? {} : {projection_readback: confirmProjectionReadback(readback, head.provider_revision)}),
todos: projection.todo_ids.map((todoId) => projection.todos.get(todoId)!),
todo_ids: projection.todo_ids,
todo_read_model: todoReadModel,
...(acceptance.enabled !== true ? {} : {goal_acceptance_contract: acceptance,
goal_acceptance_work_guards: Object.fromEntries(projection.todo_ids.flatMap(id => {
const guard = acceptanceWorkGuard(head.head, goalId, id);
return guard === null ? [] : [[id, guard]];
}))}),
...(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,
source_authority: sourceAuthority,
decision_read_from_provider: true,
legacy_fallback_used: false,
};
} catch (error) {
return {
schema_version: LOCAL_COORDINATION_TODO_LIST_RESULT_SCHEMA,
status: "failed",
reason_code: "invalid_local_coordination_todo_list_request",
reason: error instanceof Error ? error.message : "invalid Todo list request",
source_authority: sourceAuthority,
decision_read_from_provider: true,
legacy_fallback_used: false,
...localAuthorityOpenFailure(error),
};
}
}
Loading
Loading