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
14 changes: 14 additions & 0 deletions docs/architecture/rfcs/agent-loop-effect-interpreter-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,20 @@ The existing R5 CLI captures full decisions before projection, with a private di

Under existing M7.4 and roadmap S2/S3/S6/S8, converge the **execution facts**
used by heartbeat and Turn hosts, while keeping host effect ownership separate.
When an admitted Todo later waits on a timer, PR or capability, the typed
receipt owner preserves its original writeback and settlement route. Reentry
projects only that closeout, including for deferred work and after the wait
becomes due; it cannot resume business work, select a successor or invent a
retry clock. Recovery reuses the committed receipt's positive work admission
and exact settlement binding. Workspace repair alone has no business result;
the same Turn must first requalify freshly selected work on that exact Todo
through the normal guard. Broad compatibility flags without that selection
cannot turn a retained binding into work admission. A Turn with
an original semantic replan keeps that decision instead of being mistaken for
an existing business result. Real File/SQLite CLI coverage exercises generated commands, refused
rebinding, one writeback/debit, preserved initial replan and settled replay.
This source-path repair does not qualify installed host adoption or
complete-cost acceptance.
Neither today's large quota packet nor the smaller TurnEnvelope is a target
shape merely because of its size. Optional memory participation now has one
compact, signed envelope projection; the Codex CLI adapter preserves default-off
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,15 @@ R5 短包投影也完整保留已有 CLI 结算计划,包括 effect identity
#### Heartbeat 与 TurnEnvelope 收敛

沿用既有 M7.4 与 roadmap S2/S3/S6/S8,统一 heartbeat 和 Turn host 消费的
**执行事实**,宿主效果的所有权仍分别保留。现有大 quota packet 与小 TurnEnvelope
**执行事实**,宿主效果的所有权仍分别保留。已准入 Todo 随后等待定时、PR 或能力时,
typed receipt owner 保留原回写与结算路径;即使工作已 deferred 或等待已到期,重入也
只投影原收尾,不恢复业务执行、不选择后继、不创建重试时钟。恢复复用原回执中的正向
工作准入和精确结算绑定;工作区修复本身没有业务结果,须先经同轮正常 guard 对精确
同一 Todo 的新鲜选择重新准入。没有该选择的宽泛兼容标志不能把原结算绑定变成工作
准入。原语义重规划轮次仍保留该决策,不会被误当成已有业务结果。真实 File/SQLite CLI 验证
返回命令、拒绝换绑、一次回写与扣额、初始重规划及已结算重放。此源码路径修复不代表
安装宿主采用或完整成本验收。
现有大 quota packet 与小 TurnEnvelope
都不是仅凭大小就合理的目标形态。可选 memory 参与事实现进入一个有界、签名覆盖的
envelope 投影,Codex CLI 保持关闭隔离;这不代表安装态 heartbeat/App 或模型收益
已验收。
Expand Down
57 changes: 51 additions & 6 deletions loopx/control_plane/quota/blocked_wait.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { JsonObject } from "../effect_program.ts";
import {settlementIdentity, type JsonObject} from "../effect_program.ts";
import { EffectRuntimeRequestError } from "../effect_runtime_errors.ts";
import { jsonObject, requireJsonObject } from "../runtime_decode.ts";
import {nativeChildReportAdmission} from "../capabilities/native_child_admission.ts";
import {heartbeatReceiptBinding, heartbeatReceiptDetails, heartbeatReceiptFactFromEvent} from "./heartbeat_receipt_identity.ts";
import {
evaluateTodoResumeConditions,
normalizeTodoResumeWhen,
Expand Down Expand Up @@ -208,6 +210,27 @@ export function prepareBlockedWait(value: unknown): JsonObject {

export const RECEIPT_BOUND_WAIT_REQUEST_SCHEMA = "loopx_quota_receipt_bound_wait_request_v0";

/** Reuse committed work admission; a bound repair/planning Turn has no business
* result merely because it has an identity and a missing writeback. */
function admittedBusinessResult(request: JsonObject): boolean {
const receipt = jsonObject(request.heartbeat_receipt);
if (typeof request.goal_id !== "string" || !request.goal_id ||
!receipt || receipt.goal_id !== request.goal_id || receipt.agent_id !== request.agent_id ||
(receipt.todo_id != null && receipt.todo_id !== request.todo_id) ||
receipt.run_id !== request.turn_instance_id) return false;
const identity = settlementIdentity({goal_id: String(request.goal_id), agent_id: String(request.agent_id),
todo_id: String(request.todo_id), turn_instance_id: String(request.turn_instance_id)});
const binding = heartbeatReceiptBinding(identity.goal_id, identity.agent_id, heartbeatReceiptFactFromEvent(receipt));
if (binding?.binding_kind !== "todo" || binding.binding_id !== identity.todo_id ||
binding.settlement_effect_id !== identity.effect_id) return false;
const facts = heartbeatReceiptDetails(receipt);
// An original semantic replan still owes that decision, even when the host
// permits planning tools. Neither a timer nor this projection discharges it.
if (facts.semantic_replan_obligation_id != null && facts.semantic_replan_obligation_id !== "") return false;
return nativeChildReportAdmission(facts, receipt.status, identity.effect_id, "open", false)
.report_permission === "new_operation";
}

/** A pending dependency changes executable work, never the Turn's receipts.
* settlement_progress is the existing settlement owner's exact-Turn readback,
* transported by the live guard, not an author-declared progress observation.
Expand All @@ -227,14 +250,16 @@ export function projectReceiptBoundWait(value: unknown): JsonObject {
return {status: "none"};
}
const progress = jsonObject(request.settlement_progress);
const writebackRequired = progress?.schema_version === "quota_settlement_progress_v0" &&
progress.next_step === "durable_writeback" &&
(progress.state === "writeback_required" || progress.state === "writeback_receipt_required");
// Ordinary completion is a validation receipt, not Turn settlement. The
// verified replay phase, rather than terminal Todo status alone, owns this
// recovery even after the row leaves the active presentation lane.
const completedSettlement = todo.status === "done" && todo.task_class === "advancement_task" &&
request.replay_phase === "settlement_pending" &&
progress?.schema_version === "quota_settlement_progress_v0";
if (completedSettlement && progress?.next_step === "durable_writeback" &&
(progress.state === "writeback_required" || progress.state === "writeback_receipt_required")) {
if (completedSettlement && writebackRequired) {
const reason = "The original Todo's completion is verified, but its Turn still owes durable writeback and settlement. Record only the validated original result, then spend once for that same identity. Inspect existing effects before retrying; do not repeat completed work, invent a successor, or rebind this Turn.";
return boundRecovery(request, "durable_writeback", reason);
}
Expand All @@ -255,13 +280,33 @@ export function projectReceiptBoundWait(value: unknown): JsonObject {
const reason = "The advancement Todo bound to this Turn is blocked. Inspect its existing effects and evidence, then close only this original attempt with a typed blocked writeback without spending. Keep the Todo blocked and its validator intact; no retry clock or execution is authorized. A fresh host Turn may select independent work.";
return boundRecovery(request, "blocked_writeback", reason, wait);
}
const condition = jsonObject(todo.resume_condition);
const kind = condition?.kind;
const resumeWhen = kind !== "monitor_changed" && kind !== "todo_done"
? normalizeTodoResumeWhen({
schema_version: TODO_RESUME_NORMALIZE_REQUEST_SCHEMA_VERSION, resume_when: todo.resume_when,
})
: null;
const externalWait = !!resumeWhen && condition?.kind === resumeWhen.split(":", 1)[0] &&
condition.resume_when === resumeWhen;
// A later external wait removes executable work, not the original Turn's
// verified writeback duty. A due wait can become runnable before settlement;
// it still cannot discard the original identity or authorize new work.
if (["open", "deferred"].includes(String(todo.status)) &&
todo.task_class === "advancement_task" &&
writebackRequired && request.replay_phase === "open" &&
admittedBusinessResult(request) && externalWait) {
const reason = "The original admitted Turn still owes durable writeback. Its later external wait cannot discard that identity. Inspect existing effects and record only the validated original result, then settle once under the original identity. Keep the wait unchanged; do not repeat business work, select a successor, or invent a retry clock.";
return boundRecovery(request, "durable_writeback", reason);
}
if (!["open", "deferred"].includes(String(todo.status)) ||
todo.task_class !== "advancement_task" || todo.resume_ready !== false) return {status: "none"};
// This projection repairs registered Todo dependencies. Other wait kinds
// retain their existing route: in particular, a valid long timer must not
// be rejected by the separate 1–30 minute blocked-retry writeback budget.
const kind = jsonObject(todo.resume_condition)?.kind;
if (kind !== "monitor_changed" && kind !== "todo_done") return {status: "none"};
if (kind !== "monitor_changed" && kind !== "todo_done") {
return {status: "none"};
}
if (currentCompletionCondition(todo, todos)?.invalid_state === "dependency_cycle") {
return boundRecovery(request, "dependency", CYCLE_REPAIR);
}
Expand Down Expand Up @@ -292,7 +337,7 @@ function boundRecovery(
delivery_allowed: false, notify: "DONT_NOTIFY",
...(repair === "quota_spend" ? {spend_allowed_now: true} : {}),
spend_policy: repair === "durable_writeback"
? "settle the original completed Todo after validated writeback; no independent work"
? "settle the original admitted Turn after validated writeback; no independent work"
: repair === "quota_spend"
? "settle the original verified writeback once; no independent work or new writeback"
: "no spend for blocked closeout",
Expand Down
4 changes: 4 additions & 0 deletions loopx/control_plane/quota/heartbeat_receipt_identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,10 @@ export function heartbeatWorkRequalification(
throw receiptIdentityConflict("work requalification must preserve the original settlement binding");
}
if (phase !== "open" || closeoutStarted) return replay;
// The retained settlement binding is not today's executable selection.
// A waiting Todo can leave broad compatibility flags set with no selected
// work; those flags cannot requalify a repair-only original receipt.
if (identity.todo_id !== null && currentDetails.selected_todo_id !== identity.todo_id) return replay;
// No legacy/status-label compatibility for new qualifications: all modern
// work facts must be explicit. This reuses the report-admission predicate.
if (nativeChildReportAdmission(currentDetails, null, identity.effect_id, phase, false)
Expand Down
17 changes: 16 additions & 1 deletion loopx/control_plane/quota/live_decision.py
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,20 @@ def bind_action_selection_cli_routes(
selection_command["route_prefix"] = shlex.join(tokens)


def _receipt_bound_recovery_required(
payload: Mapping[str, Any], receipt_bound_todo_id: str,
) -> bool:
selected_value = payload.get("selected_todo")
selected = selected_value if isinstance(selected_value, Mapping) else {}
if (
selected.get("todo_id") != receipt_bound_todo_id
or payload.get("normal_delivery_allowed") is not True
):
return True
wait_kind = str(selected.get("resume_when") or "").split(":", 1)[0]
return wait_kind not in ("", "monitor_changed", "todo_done")


def build_live_quota_should_run_decision(
status_payload: dict[str, Any],
*,
Expand Down Expand Up @@ -742,7 +756,7 @@ def build_live_quota_should_run_decision(
and payload.get("effective_action") != EffectiveAction.UNSETTLED_HOST_TURN_RECOVERY.value
and (
receipt_bound_replay_phase is ReceiptBoundReplayPhase.SETTLEMENT_PENDING
or (payload.get("selected_todo") or {}).get("todo_id") != receipt_bound_todo_id
or _receipt_bound_recovery_required(payload, receipt_bound_todo_id)
)
):
# A verified writeback seals this Turn even while its Todo stays open
Expand All @@ -761,6 +775,7 @@ def build_live_quota_should_run_decision(
monitor_phase=(receipt_bound_monitor_phase.value if receipt_bound_monitor_phase else None),
settlement_progress=settlement_readback.progress if settlement_readback else None,
replay_phase=receipt_bound_replay_phase,
heartbeat_receipt=settlement_readback.heartbeat_receipt if settlement_readback else None,
)
if hook_dispatch["failures"]:
payload["capability_hook_dispatch"] = {
Expand Down
1 change: 1 addition & 0 deletions loopx/control_plane/quota/settlement_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,7 @@ def quota_rollout_details(
"slots": payload.get("slots") or "",
"source": payload.get("source") or "",
"todo_id": todo_id or "",
"selected_todo_id": normalize_todo_id(selected_todo.get("todo_id")) if selected_todo else None,
"replan_obligation_id": replan_obligation_id or "",
"semantic_replan_obligation_id": semantic_replan_obligation_id or "",
"target_key": payload.get("target_key") or "",
Expand Down
6 changes: 4 additions & 2 deletions loopx/control_plane/quota/unsettled_host_turn.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ def apply_receipt_bound_wait_recovery(
monitor_phase: str | None = None,
settlement_progress: dict[str, Any] | None = None,
replay_phase: ReceiptBoundReplayPhase | None = None,
heartbeat_receipt: Mapping[str, Any] | None = None,
) -> bool:
"""Read full provider facts only when a replay lost its executable binding."""
from ..todos.list_readback import list_goal_todos
Expand All @@ -389,11 +390,12 @@ def apply_receipt_bound_wait_recovery(
items = compact_settlement_wait_todos(source_items, todo_id=todo_id)
verdict = effect_runtime_result("quota.settlement.read", {
"schema_version": "loopx_quota_receipt_bound_wait_request_v0",
"todos": items, "todo_id": todo_id, "agent_id": agent_id,
"todos": items, "todo_id": todo_id, "agent_id": agent_id, "goal_id": goal_id,
"turn_instance_id": turn_instance_id, "observed_at": now_utc_iso(),
"monitor_phase": monitor_phase,
"settlement_progress": settlement_progress,
"replay_phase": replay_phase.value if replay_phase else None,
"heartbeat_receipt": dict(heartbeat_receipt) if heartbeat_receipt is not None else None,
}, large_local_snapshot=True)
if verdict.get("status") == "none":
return False
Expand All @@ -410,7 +412,7 @@ def apply_receipt_bound_wait_recovery(
scheduler_execution_context=scheduler_execution_context)
if verdict["recovery"].get("repair") == "durable_writeback":
# Transport the typed recovery through the existing command owner. A
# completed Todo is not an executable selected_todo or a new grant.
# waiting or completed Todo is not executable work or a new grant.
from ..work_items.action_selection_contract import render_cli_command_prefix

assert settlement_progress is not None
Expand Down
2 changes: 1 addition & 1 deletion tests/control_plane/test_heartbeat_receipt.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ def test_concurrent_identityless_receipt_upgrades_append_one_correction(
def test_concurrent_work_requalification_appends_one_same_binding_receipt(tmp_path: Path) -> None:
proof = {"ok": True, "should_run": True, "must_attempt_work": True,
"delivery_allowed": True, "quiet_noop_allowed": False,
"todo_id": TODO_ID, "settlement_effect_id": EFFECT_ID}
"todo_id": TODO_ID, "selected_todo_id": TODO_ID, "settlement_effect_id": EFFECT_ID}
original = build_rollout_event(goal_id=GOAL_ID, event_kind="quota_should_run",
agent_id=AGENT_ID, todo_id=TODO_ID, run_id=TURN_ID,
status="successor_replan_required", summary="delivery denied",
Expand Down
Loading
Loading