Skip to content
32 changes: 24 additions & 8 deletions docs/quota-allocation.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,17 @@ Turn-scoped `refresh-state` and `quota spend-slot` expose
`settlement_progress` from the TypeScript receipt readback. The states are
`identity_required`, `writeback_required`, `writeback_receipt_required`,
`spend_required`, `spend_receipt_required`, and `settled`. A durable run without
its matching receipt is incomplete. `settled` certifies this writeback/spend
chain; Todo completion and Goal acceptance retain their separate checks.

After verified writeback, `settlement_owed.command` carries the original Goal,
Agent, Todo or replan obligation, Turn, registry/runtime route and spend source.
Execute it unchanged. In `spend_receipt_required`, the same idempotent spend
its matching receipt is incomplete. Ordinarily, `settled` certifies the
writeback/spend chain. An exact typed blocked writeback with a bounded retry
instead sets `closeout_kind=typed_blocked_writeback_no_spend` and settles the
Turn without a quota debit. Todo completion and Goal acceptance retain their
separate checks in both cases.

When a quota spend remains owed after verified writeback,
`settlement_owed.command` carries the original Goal, Agent, Todo or replan
obligation, Turn, registry/runtime route and spend source. Execute it unchanged.
The typed blocked no-spend closeout has no spend command. In
`spend_receipt_required`, the same idempotent spend
writer restores the receipt without another debit. Refresh and recovery never
spend automatically. JSON and normal/recovery Markdown expose the same step.
Rejected recovery reports observed progress without offering a spend command.
Expand Down Expand Up @@ -313,13 +318,24 @@ from `classification`. New writes should use one of:
execution-profile hints only as a compatibility fallback for historical runs;
new control-plane decisions should be driven by the enum above.

An `outcome_gap` does not become delivery progress. It may settle and spend one
exact Todo-bound Turn only when the same writeback includes a
An `outcome_gap` does not become delivery progress. A blocked writeback is
eligible for exact Todo-bound Turn settlement only when it includes a
`typed_progress_observation_v0` with `result_class=blocked`, the matching
`work_item_id`, a stable `blocker_id`, and a non-empty array of stable
`evidence_ids`. Missing schemas, prose-only blockers, malformed evidence, and
Todo identity mismatches remain fail-closed.

New Turn-bound blocked writebacks also require a bounded retry on the same
unfinished advancement Todo. A legacy Todo must have a pending
`resume_when=resume_at:<timezone-aware-time>` due in 1–30 minutes. With promoted
File/SQLite authority, an open Todo without its own resume condition can use
a five-minute retry stored on the committed Turn instead; the peer-gated Todo
and its completion validator stay unchanged. That exact writeback settles the
Turn without spending quota. The retry suppresses only the blocked Todo for
the same Agent until due or superseded by newer work, so independent eligible
Todos can still be selected. Historical blocked writebacks without a bounded
retry retain their prior spend readback; no debit is retroactively erased.

`quota should-run` also separates long-running observation from work that should
advance the selected goal. When the selected goal's current projection is a
dependency-only observation, the payload includes `work_lane_contract` with
Expand Down
12 changes: 12 additions & 0 deletions loopx/cli_commands/quota_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
)
from ..control_plane.quota.error_codes import QuotaCommandValidationError
from ..control_plane.runtime.status_projection_cache import (
cached_goal_run_index_is_current,
load_status_projection_cache,
resolve_status_projection_cache_runtime_root,
write_status_projection_cache,
Expand Down Expand Up @@ -321,6 +322,17 @@ def prepare_quota_command_context(
available_capabilities=args.available_capabilities,
agent_lane_id=args.agent_id,
)
if (
status_payload is not None
and command in QUOTA_SCHEDULER_COMMANDS
and status_goal_id
and not cached_goal_run_index_is_current(
status_payload, runtime_root=runtime_root, goal_id=status_goal_id,
)
):
status_payload = None
cache_metadata["hit"] = False
cache_metadata["miss_reason"] = "run_index_changed"
if status_payload is None:
collector = status_collector or collect_status
status_payload = collector(
Expand Down
2 changes: 2 additions & 0 deletions loopx/control_plane/goals/vision_checkpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ def build_vision_checkpoint(
todo_id: str | None = None,
completion_todo_id: str | None = None,
autonomous_replan_recorded: bool = False,
blocked_retry: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Finalize the TS-owned Vision transaction after replan qualification."""

Expand All @@ -149,6 +150,7 @@ def build_vision_checkpoint(
"todo_id": todo_id,
"completion_todo_id": completion_todo_id,
"autonomous_replan_recorded": bool(autonomous_replan_recorded),
"blocked_retry": blocked_retry,
},
)
except EffectRuntimeRejected as exc:
Expand Down
15 changes: 14 additions & 1 deletion loopx/control_plane/goals/vision_checkpoint.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { JsonObject } from "../effect_program.ts";
import { EffectRuntimeRequestError } from "../effect_runtime_errors.ts";
import { isBoundedBlockedRetry } from "../quota/blocked_retry.ts";
import {
DELIVERY_BOUNDARIES,
type DeliveryBoundary,
Expand Down Expand Up @@ -148,6 +149,7 @@ interface VisionRefreshFinalizeRequest {
todo_id: string | null;
completion_todo_id: string | null;
autonomous_replan_recorded: boolean;
blocked_retry: JsonObject | null;
}

export type VisionCheckpointDecision =
Expand Down Expand Up @@ -721,6 +723,7 @@ export function decodeVisionCheckpointRequest(
request.autonomous_replan_recorded,
"autonomous_replan_recorded",
),
blocked_retry: optionalObject(request.blocked_retry, "blocked_retry"),
};
}

Expand Down Expand Up @@ -763,10 +766,20 @@ export function buildVisionCheckpoint(value: unknown): JsonObject {
}
const request = decodeVisionCheckpointRequest(value);
validateInFlightBoundary(request);
if (request.blocked_retry !== null && (
request.delivery_outcome !== "outcome_gap" ||
request.delivery_boundary !== "semantic_closeout" ||
request.todo_id === null ||
request.completion_todo_id !== null ||
!isBoundedBlockedRetry(request.blocked_retry, request.todo_id)
)) {
throw new EffectRuntimeRequestError("blocked retry does not bind a typed outcome-gap Todo closeout");
}
const triggers: JsonObject[] = [];
if (
isMaterialDeliveryOutcome(request.delivery_outcome) &&
request.delivery_boundary === "semantic_closeout"
request.delivery_boundary === "semantic_closeout" &&
request.blocked_retry === null
) {
triggers.push({
kind: "material_delivery_outcome",
Expand Down
Loading
Loading