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
3 changes: 3 additions & 0 deletions loopx/control_plane/goals/checkpoint_context_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,9 @@ def read_checkpoint_context(
identity = readback.identity.value
with _source_guard(root, goal_id, path):
result = _resolve(root, phase="read", identity=identity.as_dict(), prior=readback.writeback_run,
writeback_committed=readback.writeback.failure is None,
quota_spend_committed=readback.spend_run is not None or readback.spend_event is not None,
semantic_replan_guard=readback.semantic_replan_guard,
read_context_id=uuid4().hex, dependency_todo_ids=dependency_todo_ids or [],
facts=_local_source_facts(root, registry_path, path, identity, goal_ref))
receipt = result.pop("receipt")
Expand Down
15 changes: 13 additions & 2 deletions loopx/control_plane/goals/checkpoint_read_context.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
/** Read basis for a missing-checkpoint supplement, not an execution/permission lease.
/** Read basis for a missing decision/qualification checkpoint, not an execution/permission lease.
* The commit effect holds source/index claims and the real provider fence. */
import type {JsonObject} from "../effect_program.ts";
import {settlementIdentity} from "../effect_program.ts";
import {jsonObject, requireJsonObject, requireNonEmptyString} from "../runtime_decode.ts";
import {EffectRuntimeRequestError} from "../effect_runtime_errors.ts";
import {canonicalAuthoritySha256} from "../coordination/authority_store_codec.ts";
import {isUnqualifiedBlockedReplanWriteback} from "../quota/settlement_phase.ts";

const RECEIPT_SCHEMA = "checkpoint_read_context_v1";
// Exact presentation fields only. Unknown future fields remain part of the basis.
Expand Down Expand Up @@ -88,7 +90,16 @@ export function evaluateCheckpointReadContext(value: unknown): JsonObject {
if (request.phase === "read") {
const prior = requireJsonObject(request.prior, "committed writeback");
const checkpoint = jsonObject(prior.vision_checkpoint);
if (checkpoint?.decision !== "missing_required" || checkpoint.satisfied !== false) {
const missingQualification = request.writeback_committed === true && request.quota_spend_committed === false &&
typeof identity.replan_obligation_id === "string" &&
jsonObject(request.semantic_replan_guard)?.selected_obligation_id === identity.replan_obligation_id &&
isUnqualifiedBlockedReplanWriteback(prior, settlementIdentity({
goal_id: requireNonEmptyString(identity.goal_id, "goal_id"),
agent_id: requireNonEmptyString(identity.agent_id, "agent_id"),
turn_instance_id: requireNonEmptyString(identity.turn_instance_id, "turn_instance_id"),
replan_obligation_id: identity.replan_obligation_id,
}));
if ((checkpoint?.decision !== "missing_required" || checkpoint.satisfied !== false) && !missingQualification) {
return rejected("checkpoint_context_not_missing");
}
const projected = snapshot(request);
Expand Down
15 changes: 15 additions & 0 deletions loopx/control_plane/quota/refresh_recovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export function refreshRecovery(
receiptPresent: boolean,
workspaceRequirement: string | undefined,
laterAgentVision: boolean,
missingBlockedReplanQualification = false,
): JsonObject {
const wantsVision = request.vision !== null || Boolean(request.unchanged_reason);
const digest = wantsVision ? createHash("sha256").update(canonical({
Expand Down Expand Up @@ -129,6 +130,20 @@ export function refreshRecovery(
(changesMutation && mutationDigest !== jsonObject(prior.refresh_recovery)?.mutation_digest)) {
return result("reject", "committed_writeback_payload_conflict");
}
// Older callers could commit a blocked replan without qualifying its exact
// admitted source. Re-run that owner through the existing additive checkpoint
// path, preserving the original payload/receipt and executing no mutations.
// The readback owner proves that this identity has no committed debit.
if (missingBlockedReplanQualification && wantsVision) {
const judgmentDigest = createHash("sha256").update(canonical({
vision: request.vision, unchanged_reason: request.unchanged_reason, merge_patch: request.merge_patch,
})).digest("hex");
if (judgmentDigest === priorDigest) {
if (laterAgentVision) return result("reject", "checkpoint_superseded_by_later_vision");
if (changesMutation) return result("reject", "checkpoint_supplement_must_not_repeat_mutations");
return result("supplement_checkpoint", "complete_missing_replan_qualification_on_original_turn");
}
}
if (wantsVision && digest !== priorDigest) {
if (checkpoint?.decision !== "missing_required" || checkpoint.satisfied !== false) {
return result("reject", "committed_vision_decision_conflict");
Expand Down
12 changes: 12 additions & 0 deletions loopx/control_plane/quota/settlement_phase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ export function isAcceptedBlockedReplanWriteback(
});
}

/** A legacy observation may need qualification, never acceptance by inference.
* Callers separately prove its guard/writeback receipts and absence of debit. */
export function isUnqualifiedBlockedReplanWriteback(value: unknown, identity: SettlementIdentity): boolean {
const run = jsonObject(value);
return identity.binding_kind === "autonomous_replan" && identity.replan_obligation_id !== null &&
!!run && run.goal_id === identity.goal_id && run.agent_id === identity.agent_id &&
run.turn_instance_id === identity.turn_instance_id && run.replan_obligation_id === identity.replan_obligation_id &&
run.delivery_outcome === "outcome_gap" &&
isTurnScopedSettlementOutcome(run.delivery_outcome, run.progress_observation, identity.replan_obligation_id) &&
jsonObject(run.autonomous_replan_ack)?.recorded !== true;
}

/** Classify persisted Run facts for history selection, without granting a
* settlement, debit or execution right. Reuse the settlement owner's exact
* semantic qualification; a caller-supplied "administrative" flag is no proof.
Expand Down
4 changes: 4 additions & 0 deletions loopx/control_plane/quota/settlement_readback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
import {
isBoundedBlockedRetry,
isAcceptedBlockedReplanWriteback,
isUnqualifiedBlockedReplanWriteback,
qualifySettlementHistory,
isCommittedMonitorPollEffect,
isAcceptedProgressWriteback,
Expand Down Expand Up @@ -1321,6 +1322,9 @@ function readQuotaSettlementFromRequest(
run.goal_id === identity.goal_id && run.agent_id === identity.agent_id &&
(jsonObject(run.agent_vision) !== null || jsonObject(run.vision_checkpoint)?.required === true)
),
writeback.failure === null && spendRun === null && spendEvent === null &&
semanticReplanGuard.selected_obligation_id === identity.replan_obligation_id &&
isUnqualifiedBlockedReplanWriteback(writebackRun, identity),
);
// A non-material poll need not appear as findWriteback's advancement run.
// Its exact committed observation still fences an incompatible unavailable
Expand Down
7 changes: 5 additions & 2 deletions loopx/control_plane/work_items/replan_semantics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { EffectRuntimeRequestError } from "../effect_runtime_errors.ts";
import { optionalNonEmptyString, requireJsonObject, requireNonEmptyString } from "../runtime_decode.ts";
import { visionAuthoringContract } from "../goals/vision_checkpoint.ts";
import { parseTodoTimestampMicros } from "../runtime_timestamp.ts";
import { TODO_COMPLETION_RECOVERIES } from "../todos/completion_state.ts";
import {normalizeStrideDecision, normalizeStrideStrategy, STRIDE_DECISION_SCHEMA, strideControlContract, strideReviewMismatch} from "./stride_policy.ts";
import { projectProgressDelta } from "./progress_delta.ts";

Expand Down Expand Up @@ -141,7 +142,9 @@ function lifecycleSource(request: JsonObject): JsonObject {
item.claimed_by === agent && item.last_actor_agent_id === agent &&
completed !== null && completed <= admitted &&
strings(item.successor_todo_ids).length === 0 && !item.superseded_by &&
(item.no_followup !== true || (item.completion_recovery === "lifecycle_reentry_terminal_closeout" &&
// Either canonical closeout path can finish after this review was
// admitted. The exact trigger/identity checks below still own reentry.
(item.no_followup !== true || (TODO_COMPLETION_RECOVERIES.some(recovery => recovery === item.completion_recovery) &&
updated !== null && updated >= admissionSecond));
}).sort((left, right) => {
// Terminal reentry changes updated_at, never the original completed_at.
Expand All @@ -154,7 +157,7 @@ function lifecycleSource(request: JsonObject): JsonObject {
item.no_followup === true && item.completion_continuation === "no_followup" &&
typeof item.todo_id === "string" && !!item.todo_id &&
typeof item.completion_turn_key === "string" && !!item.completion_turn_key &&
item.completion_recovery === "lifecycle_reentry_terminal_closeout");
TODO_COMPLETION_RECOVERIES.some(recovery => recovery === item.completion_recovery));
return {items, settled};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,11 @@ def qualify_replan_writeback(
context.get("replan_transition_candidates", [])
if isinstance(candidate.get("obligation"), dict) and
candidate["obligation"].get("obligation_id") == guard_semantic_replan_obligation_id), None)
if guard_scoped and receipt_bound_replan_review is not None:
# Every selected replan retains its exact receipt-bound source. A missing
# reconstruction must not fall through as an unselected future obligation
# and commit an unqualified writeback. Plain outcome reviews additionally
# carry a scalar source; lifecycle/transition reviews need the same owner.
if guard_scoped and guard_semantic_replan_obligation_id:
obligation = project_replan_semantics({
"operation": "receipt_bound_obligation", "guard_scoped": True,
"selected_obligation_id": guard_semantic_replan_obligation_id,
Expand Down
1 change: 1 addition & 0 deletions skills/loopx-self-repair/references/repair-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ teaches a reusable control-plane lesson.
| `normal_no_user_todo_misflagged` | `action_required=false` and user open count is zero, but the agent reports a state projection fault. | quota payload and user todo summary. | Rule over-applied the "todo not projected" fallback. | Allow "无用户待办/无需通知"; update heartbeat/prompt smoke if needed. |
| `todo_no_followup_global_terminal_overreach` | A completed scoped Todo records `done + no_followup`, but `refresh-state --repair-delta-kind no_followup` becomes `replan_noop` because unrelated advancement or long-running monitor Todos remain open. | rejected repair-delta claim, complete Agent Todo projection, scoped Todo metadata, and the latest local no-follow-up completion. | The repair-delta validator reused Goal-level terminal-frontier completeness to validate a Todo-local closure. | Keep Goal `terminal_no_followup` strict, but validate repair-delta no-follow-up against the latest scoped completed Todo in a complete parser projection; reject other-agent, continuation-required, malformed, or truncated evidence and keep the emitted Todo evidence bounded. Cover the real CLI path with an open monitor regression. |
| `terminal_settlement_ordering_gap` | Final Todo completion succeeds, then the matching refresh or spend is rejected because the Goal is already terminal; different hosts may expose different closeout orders or retry the material effect. | Typed settlement plan and effect identity, Todo completion event, accountable refresh and spend receipts, Turn journal phase prefix, and host task-body projection. | Final `no_followup` was modeled like ordinary pre-settlement Todo completion even though it mutates Goal terminal state and activates the strict no-more-spend guard. | Keep Goal terminal guards strict. Treat ordinary successor completion as Todo lifecycle work, but make final `no_followup` a conditional post-spend terminal closeout under the same effect identity. Require matching writeback and spend receipts at the mutation boundary, prove the closeout event itself carries `no_followup`, and journal it so closeout-only retry cannot repeat writeback or spend. Cover CLI, visible Goal, Codex App, Turn driver, adapter order, failure short-circuit, and replay. |
| `receipt_bound_lifecycle_source_gap` | A lifecycle review is admitted, then closing the original Todo through its settled Turn makes same-review reentry fail with an identity conflict; a blocked refresh may commit without a qualified replan ACK. | Exact original completion identity and closeout receipt, admitted review identity/time, canonical Todo readback, typed blocked observation and writeback/debit receipts. | Lifecycle reconstruction recognized only the completion-key reentry form; writeback could treat a missing selected source like an unrelated future obligation. | Reuse the canonical completion-recovery vocabulary for both legal paths and retain exact actor, identity and timestamp checks. Revalidate every selected replan through the shared owner before writeback. For an already committed unqualified blocked observation with no debit, permit only an exact additive checkpoint requalification; preserve the original run and all receipt history, reject payload changes or later superseding vision, and never infer acceptance or erase a committed debit. |
| `host_settlement_source_projection_gap` | A visible Goal host packet requires `source=visible-goal` without an explicit Turn receipt, but the nested interaction contract tells the agent to spend as `heartbeat` and binds the currently selected Todo. The command can fail after Todo completion or misattribute interactive usage. | Host runtime profile, canonical host quota commands, interaction-contract CLI actions, selected Todo lifecycle order, and the failed spend preview. | The generic interaction projection hard-coded heartbeat accounting and selected-Todo binding instead of deriving the accounting route from the scheduler execution context. | Route spend actions from the typed runtime profile. Native Goal runtimes preserve `source=visible-goal` and host-level attribution; heartbeat automation keeps its Todo- and Turn-bound settlement plan. Cover the exact CLI actions for each native Goal profile and retain the heartbeat regression. |
| `visible_goal_settlement_replan_attribution_gap` | An accountable visible-Goal delivery writeback moves the frontier into capability or control-plane repair; the following unbound spend is recorded against the new repair state with no Todo, Turn, effect, or delivery-workspace causality, and an exact retry cannot replay the committed effect. | Newest same-Agent accountable run, its complete nested settlement identity, top-level Todo/Turn fields, original heartbeat receipt, delivery-workspace causality, current quota frontier, spend classification, and retry run count. | The unbound visible-Goal path used the mutable current frontier as settlement authority and did not recover the completed delivery identity; commit classification also represented eligible, repair, and delivery semantics as overlapping booleans, allowing a later repair projection to overwrite attribution. | Allow unbound recovery only for visible-Goal spend from the newest fully typed same-Agent accountable run; require its top-level and nested identities to match the TypeScript-generated settlement payload and revalidate the original heartbeat receipt. Preserve Todo/Turn/effect/workspace causality, write the CLI receipt under the recovered Turn, and reduce commit semantics to one closed TypeScript disposition where proven delivery completion precedes later frontier repair. Cover missing/legacy identity, capability and control-plane replan, and exact one-run replay. |
| `runtime_capability_user_gate_overreach` | A missing launcher/runtime capability such as `network` becomes `capability_gate.action=ask_owner`, `requires_user_action=true`, or a user notification even though no credential, protected access, or production authority is needed. | quota `capability_gate`, heartbeat `--available-capability` inputs, launcher/runtime observations, interaction contract user channel. | Capability availability and permission authority were collapsed into one owner-held class. | Keep runtime provisioning gaps in the agent repair lane, require truthful runtime observation before declaring availability, and reserve `ask_owner` for credentials or protected/production authority. Cover all-blocked and mixed-capability cases with capability-gate and monitor smokes. |
Expand Down
Loading
Loading