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
44 changes: 44 additions & 0 deletions docs/quota-allocation.md
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,50 @@ empty results; missing/stale display and provider failure never authorize a
Markdown fallback. The guard does not append a second Markdown candidate list.
Historical receipt-bound recovery remains separate from new work admission.

The final current-selection admission and recovery predicate has one TypeScript
owner. CLI preflight and interaction construction use that projection; the Effect
interpreter, new TurnEnvelope construction and scoped-override projection also
reject an unadmitted deferred/rejected packet that still carries execution,
spend, safe-bypass or executable settlement fields. A missing typed recovery action
fails explicitly. The recovery command itself remains valid: these checks do not
classify command prose or remove the same-Turn re-entry route.

Admission reads the canonical agent channel: pending delivery requires both
`must_attempt=true` and `delivery_allowed=true`; pending workspace repair requires
its explicit repair route and `delivery_allowed=false`. Missing delivery permission
is unknown, not a grant. Compatibility `execution_obligation` annotations cannot
independently grant or veto final admission. Its producer rejects non-boolean
permission values before constructing the canonical channel; null/missing values
do not grant permission. This tightens malformed/contradictory selected-Todo inputs
without changing valid receipt-bound or current-work behavior.

Receipt-bound work stays with its reconciliation owner. Existing admitted current
work and explicitly qualified workspace repair retain their exceptions; an
unrelated pending selection cannot borrow those facts. These are consistency and
preflight projections over the current guard, not lease, Goal-identity or provider
authorization. Calls without a qualification keep their existing path. Python
retains command/structural adaptation and shares one recovery decision across the
two interaction channels; it does not keep a parallel admission predicate.

准入读取正式 agent channel:待选交付要求 `must_attempt=true` 且
`delivery_allowed=true`;待选工作区修复要求明确的修复路径及
`delivery_allowed=false`。缺失交付权限表示未知,不能当作授权。兼容字段
`execution_obligation` 不能独立授予或否决最终准入;构造正式通道前会拒绝
非布尔权限值,null/缺失值不授予权限。这会收紧非法或矛盾的选择输入,合法的
回执绑定和当前工作路径保持不变。

最终的当前工作准入和选择恢复判定统一由 TypeScript 持有,CLI preflight 与交互
构造复用该投影。Effect 解释器、新 TurnEnvelope 构造和 scoped-override 投影也
拒绝未获准的 deferred/rejected packet 中残留的执行、扣费、safe-bypass 或可执行
结算字段;缺失类型化 recovery action 会明确失败。恢复命令本身仍有效,这些检查
不按命令文案分类,也不删除同 Turn 的重新准入路径。

已绑定回执的工作仍由回执协调 owner 处理;既有已获准工作和明确允许的工作区
修复保留各自例外,不相关的待选择工作不能借用这些事实。此处只做当前 guard 的
一致性和 preflight 投影,不验证租约、Goal 身份或 provider 权限。没有 qualification
的调用保留原路径。Python 保留命令与结构适配,两条交互 channel 复用一次恢复
判定,不再维护平行的准入规则。

Hosts supplying their own `--turn-instance-id` use this same selection contract,
including `generic_cli`, `outer_controller`, and native CLI profiles. An explicit
`--todo-id` is qualified before receipt binding; it must not leave the response
Expand Down
5 changes: 3 additions & 2 deletions loopx/cli_commands/quota_action_selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,12 +161,13 @@ def _requested_quota_action_selection_preflight(
# which side of the delivery boundary refused it without re-deriving it from
# prose.
agent_must_attempt = agent_channel.get("must_attempt") is True
agent_delivery_allowed = agent_channel.get("delivery_allowed") is not False
delivery = agent_channel.get("delivery_allowed")
agent_delivery_allowed = delivery if isinstance(delivery, bool) else None
selected_todo_id, admitted = current_action_selection_admission(
payload,
requested_todo_id=requested_todo_id,
agent_must_attempt=agent_must_attempt,
agent_delivery_refused=not agent_delivery_allowed,
agent_delivery_allowed=agent_delivery_allowed,
)
if receipt_bound_replan_obligation_id:
if not receipt_identity_upgraded:
Expand Down
2 changes: 2 additions & 0 deletions loopx/control_plane/effect_program.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {assertActionSelectionConsistency} from "./work_items/action_selection_admission.ts";
import { EffectRuntimeRequestError } from "./effect_runtime_errors.ts";
import { isStringLiteral } from "./runtime_decode.ts";
import { decodeInteractionContract } from "./work_items/interaction_contract.ts";
Expand Down Expand Up @@ -305,6 +306,7 @@ export function interpretQuotaShouldRunPacket(
): EffectTurn<JsonObject, string> {
const packet = asObject(packetValue);
const interaction = decodeInteractionContract(packet.interaction_contract);
assertActionSelectionConsistency(packet, interaction);
const lane = asObject(packet.work_lane_contract);
const scheduler = asObject(packet.scheduler_hint);
const codexApp = asObject(scheduler.codex_app);
Expand Down
2 changes: 2 additions & 0 deletions loopx/control_plane/effect_runtime_handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,8 @@ export function createEffectRuntimeHandlers(
["turn.delivery_route.evaluate", lazyHandler(() => import("./turn_driver/delivery_continuity.ts"), ({evaluateDeliveryRoute}) => evaluateDeliveryRoute)],
["work_item.action_portfolio.project", lazyHandler(() => import("./work_items/action_portfolio.ts"), ({projectQuotaActionPortfolio}) => projectQuotaActionPortfolio)],
["work_item.action_selection.qualify", lazyHandler(() => import("./work_items/action_portfolio.ts"), ({qualifyActionSelection}) => qualifyActionSelection)],
["work_item.action_selection.admission.project", lazyHandler(() => import("./work_items/action_selection_admission.ts"), ({projectCurrentActionSelectionAdmission}) => projectCurrentActionSelectionAdmission)],
["work_item.action_selection.recovery.project", lazyHandler(() => import("./work_items/action_selection_admission.ts"), ({projectActionSelectionRecovery}) => projectActionSelectionRecovery)],
[
"work_item.action_selection.reconcile_retained",
lazyHandler(() => import("./work_items/action_portfolio.ts"), ({reconcileRetainedActionSelection}) => reconcileRetainedActionSelection),
Expand Down
4 changes: 4 additions & 0 deletions loopx/control_plane/quota/scoped_override.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import type { JsonObject } from "../effect_program.ts";
import { requireJsonObject, requireNonEmptyString } from "../runtime_decode.ts";
import { decodeInteractionContract } from "../work_items/interaction_contract.ts";
import {assertActionSelectionConsistency} from "../work_items/action_selection_admission.ts";

/** Project diagnostics after selection, guards, settlement and notification overlays.
* Gate admission metadata is historical context; it cannot select another Todo.
*/
export function projectScopedOverride(params: JsonObject): JsonObject {
const override = { ...requireJsonObject(params.override, "override") };
const interaction = decodeInteractionContract(params.interaction_contract);
if (params.selection_context !== undefined) {
assertActionSelectionConsistency(requireJsonObject(params.selection_context, "selection_context"), interaction);
}
delete override.selected_action;
if (interaction.agent_channel.delivery_allowed && params.selected_todo != null) {
const selected = requireJsonObject(params.selected_todo, "selected_todo");
Expand Down
2 changes: 2 additions & 0 deletions loopx/control_plane/quota/should_run_packet.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
)
from ..work_items.execution_obligation import build_execution_obligation
from ..work_items.action_selection_contract import (
action_selection_context,
apply_action_selection_recovery_projection,
)
from ..work_items.goal_route_hint import build_goal_route_hint
Expand Down Expand Up @@ -1643,6 +1644,7 @@ def _build_quota_should_run_payload(
"override": payload[kind],
"selected_todo": payload.get("selected_todo"),
"interaction_contract": payload["interaction_contract"],
"selection_context": action_selection_context(payload),
})
return payload

Expand Down
4 changes: 4 additions & 0 deletions loopx/control_plane/todos/identity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/** Canonical grammar only: existing callers retain normalization and error policy. */
export function isCanonicalTodoId(value: string): boolean {
return /^todo_[a-z0-9_-]{3,64}$/u.test(value);
}
99 changes: 99 additions & 0 deletions loopx/control_plane/work_items/action_selection_admission.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/** Final selected-work admission, shared by CLI recovery and packet readers.
* Receipt labels are reconciliation facts, never proof of current lease authority. */
import type {JsonObject} from "../effect_program.ts";
import {EffectRuntimeRequestError} from "../effect_runtime_errors.ts";
import {jsonObject, requireBoolean, requireJsonObject} from "../runtime_decode.ts";
import {EffectiveAction} from "../quota/effective_action.generated.ts";
import {isCanonicalTodoId} from "../todos/identity.ts";

interface AgentAdmissionFacts {must_attempt: boolean; delivery_allowed: boolean | null}
interface SelectionAdmission {selected_todo_id: string | null; admitted: boolean}

function selectionTodoId(value: unknown): string | null {
if (typeof value !== "string") return null;
// Preserve the Python selection reader's strip, not JS trim's BOM policy.
const whitespace = /[\p{White_Space}\u001c-\u001f]/u;
let start = 0, end = value.length;
while (start < end && whitespace.test(value[start]!)) start++;
while (end > start && whitespace.test(value[end - 1]!)) end--;
const candidate = value.slice(start, end).toLowerCase();
return isCanonicalTodoId(candidate) ? candidate : null;
}

export function currentActionSelectionAdmission(
packet: JsonObject, requestedTodoId: string, agent: AgentAdmissionFacts,
): SelectionAdmission {
const selected = jsonObject(packet.selected_todo) ?? {};
const qualification = jsonObject(packet.action_selection_qualification) ?? {};
let selectedId = selectionTodoId(selected.todo_id);
if (selectedId === null && qualification.state === "qualified") {
selectedId = selectionTodoId(jsonObject(qualification.selected_todo)?.todo_id);
}
const pending = selected.selection_binding === "pending_action_selection";
const delivery = pending && packet.normal_delivery_allowed === true
&& agent.must_attempt && agent.delivery_allowed === true;
const workspaceRepair = pending && packet.workspace_repair_allowed === true
&& packet.effective_action === EffectiveAction.AGENT_WORKSPACE_REPAIR
&& agent.must_attempt && agent.delivery_allowed === false;
const currentWork = !pending && agent.must_attempt;
return {selected_todo_id: selectedId,
admitted: selectedId !== null && selectedId === requestedTodoId
&& packet.ok === true && packet.should_run === true && (delivery || workspaceRepair || currentWork)};
}

export function actionSelectionNeedsRecovery(packet: JsonObject, agent: AgentAdmissionFacts): boolean {
const qualification = jsonObject(packet.action_selection_qualification);
if (qualification === null) return false;
if (qualification.state !== null && typeof qualification.state === "object") {
throw new EffectRuntimeRequestError("action selection qualification state must be scalar");
}
if (qualification.state !== "deferred" && qualification.state !== "rejected") return false;
const selected = jsonObject(packet.selected_todo), replan = jsonObject(packet.autonomous_replan_obligation);
if (selected?.selection_binding === "heartbeat_receipt" || replan?.selection_binding === "heartbeat_receipt") return false;
const requested = typeof qualification.requested_todo_id === "string" ? qualification.requested_todo_id : "";
if (currentActionSelectionAdmission(packet, requested, agent).admitted) return false;
if (qualification.recovery_action !== "reenter_guard_without_selection") {
throw new EffectRuntimeRequestError("rejected action selection omitted its typed recovery action");
}
return true;
}

function admissionRequest(request: JsonObject) {
return {packet: requireJsonObject(request.packet, "selection admission packet"), agent: {
must_attempt: requireBoolean(request.agent_must_attempt, "agent_must_attempt"),
delivery_allowed: request.agent_delivery_allowed === null ? null
: requireBoolean(request.agent_delivery_allowed, "agent_delivery_allowed"),
}};
}

export function projectCurrentActionSelectionAdmission(request: JsonObject): JsonObject {
const {packet, agent} = admissionRequest(request);
if (typeof request.requested_todo_id !== "string") throw new EffectRuntimeRequestError("requested_todo_id must be a string");
return {...currentActionSelectionAdmission(packet, request.requested_todo_id, agent)};
}

export function projectActionSelectionRecovery(request: JsonObject): JsonObject {
const {packet, agent} = admissionRequest(request);
return {required: actionSelectionNeedsRecovery(packet, agent)};
}

const ROOT_EXECUTION_FLAGS = ["should_run", "normal_delivery_allowed", "recovery_delivery_allowed",
"self_repair_allowed", "capability_repair_allowed", "workspace_repair_allowed", "safe_bypass_allowed",
"actionable_by_codex", "spend_allowed_now", "spend_after_validation"] as const;

/** Reject stale grants without inspecting command prose or consuming receipts. */
export function assertActionSelectionConsistency(packet: JsonObject, interaction: JsonObject): void {
const agent = jsonObject(interaction.agent_channel) ?? {}, cli = jsonObject(interaction.cli_channel) ?? {};
if (!actionSelectionNeedsRecovery(packet, {
must_attempt: agent.must_attempt === true,
delivery_allowed: typeof agent.delivery_allowed === "boolean" ? agent.delivery_allowed : null,
})) return;
const granted = (value: unknown) => value !== undefined && value !== null && value !== false;
if (ROOT_EXECUTION_FLAGS.some(field => granted(packet[field]))
|| granted(agent.must_attempt) || granted(agent.delivery_allowed)
|| granted(cli.spend_allowed_now) || granted(cli.spend_after_validation) || granted(cli.selection_required)
|| ["settlement_plan", "replan_settlement_contract", "selection_command", "selection_policy_ref"]
.some(field => cli[field] !== undefined && cli[field] !== null)) {
throw new EffectRuntimeRequestError("unadmitted action selection cannot carry execution or settlement authority");
}
}
Loading
Loading