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
10 changes: 8 additions & 2 deletions docs/development/testing-and-quality.md
Original file line number Diff line number Diff line change
Expand Up @@ -1097,7 +1097,11 @@ the same completion intent and a corrected uncommitted vision. Checkpoint-only
recovery is not a substitute for unfinished settlement.

If a previously completed MCP Todo omitted its decision, call
`review_task_vision(todo_id, agent_id, agent_vision=...)` with that same Todo.
`review_task_vision(todo_id, agent_id)` with that same Todo to read its current
decision basis. Judge the returned basis, then submit `agent_vision=...` or
`vision_unchanged_reason=...` together with `read_context_id=...`. A stale or
replaced receipt requires another read and a new judgment; a lost response
requires the exact same receipt and decision, without reading again.
It uses the original host Turn and the same writeback command constructor,
delegating to the existing typed checkpoint recovery. It neither repeats Todo
completion nor spends again. Exact replay is idempotent; a conflicting committed
Expand All @@ -1115,7 +1119,9 @@ not a substitute for evidence; remaining acceptance gaps or gates still prevent
terminal quota. Kernel validation does not independently prove arbitrary prose
true, so behavior qualification must also inspect the delivered artifacts.

MCP 可随完成操作携带 vision 判断,也可用 `review_task_vision` 在原 Turn 补齐遗漏。
MCP 可随完成操作携带 vision 判断,也可用 `review_task_vision` 在原 Turn 补齐遗漏:
先只传 Todo 和 Agent 读取依据,重新判断后携带 `read_context_id` 与判断提交。
凭据过期或被替换时重读重判;响应丢失时原样重试凭据和判断,不重新读取。
复用 TS 的既有恢复规则,不新增结算引擎、不重扣额度;已提交的判断不能偷偷改写。
格式和预算预检在 Todo 完成前拒绝非法输入;若旧宿主已部分完成,则修正未提交的
vision 并重试原 `complete_task`,不能用仅补 checkpoint 的操作替代未完成结算。
Expand Down
91 changes: 90 additions & 1 deletion docs/reference/protocols/goal-vision-replan-contract-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,11 +265,100 @@ Valid checkpoint decisions are:
A material closeout should carry its own vision patch or evidence-backed unchanged
reason. If omitted, `refresh-state` still records the outcome and returns the
checkpoint repair action. Follow that action in the same turn with the original
settlement identity, removing already executed state mutations. The supplement
settlement identity: first read `checkpoint-context`, then echo its
`read_context_id` as `--checkpoint-read-context` with a newly judged vision
decision, removing already executed state mutations. The supplement
must satisfy the checkpoint before terminal closeout; it neither re-authors the
outcome nor spends a second time. Never invent an unchanged reason to clear a gap.
Typed in-flight continuations keep their existing exemption.

### Read basis for checkpoint-only recovery

Missing-checkpoint supplementation now requires an explicit read receipt. This is
a default admission change for both legacy and newly committed Turn writebacks;
normal first writebacks and non-Turn vision authoring retain their existing rules.
From the original working directory and with the original registry/runtime/project/
state-file options, read the basis for the exact settlement:

```sh
loopx checkpoint-context --goal-id example --agent-id agent-a \
--todo-id todo_page --turn-instance-id turn-1 --format json
```

Use `--replan-obligation-id` instead of `--todo-id` for an obligation-bound Turn.
Declared Todo dependencies are included; repeat `--dependency-todo-id` for any
additional upstream Todo results actually used in the judgment. Inspect the
returned `basis`, judge the direction again, and add
`--checkpoint-read-context <read_context_id>` to the checkpoint-only refresh.
The agent echoes this opaque receipt; LoopX retains the version manifest.

MCP hosts use the same protocol through `review_task_vision`: call with only
`todo_id` and `agent_id` to read, then submit the returned `read_context_id`
with one newly judged `agent_vision` or `vision_unchanged_reason`. Reading never
automatically submits a decision. Missing receipts fail closed; stale receipts
require another read and judgment, while lost replies use the exact original
receipt and decision. The Python `checkpoint_context_io` adapter gathers and
locks local sources. TypeScript derives canonical Todos and the complete owner
acceptance document from one authority head; `checkpoint_read_context` compares
the basis and `checkpoint_commit` owns the final append.

The basis covers the selected Todo, its dependency closure and recorded results,
shared Goal prose and User Todos, the owner acceptance document/revision when
configured, the current agent vision, and the local source binding. A replan
obligation covers the full Todo frontier. Archived dependencies remain inputs.
Todo display positions, source headings, and the Goal's global `updated_at` are
excluded; an unrelated Agent Todo or run-history append does not invalidate an
otherwise unchanged Todo-bound basis. Shared prose is deliberately conservative:
editing it requires another judgment even if the edit was only editorial.

The File/SQLite path retains the Goal index and local source protection, then
enters the real provider's writer fence: File uses the same mutation lock as
`commitAuthority`; SQLite uses one connection's `BEGIN IMMEDIATE`. Final head
read, version comparison and checkpoint append complete before release. SQLite
performs this short section synchronously, with no `await` while holding the
transaction. Model reasoning and projection sync remain outside it. The provider
revision is returned for diagnostics, but only relevant component changes or a
different store identity invalidate the basis. Old v0 receipts require a new read.

The ordinary local Todo command wrapper already takes the maintenance lock
before committing. The provider fence additionally covers transactions through
the exported provider boundary that do not take that outer lock; these are
distinct concurrency tests. Provider failures stay closed. This adds no
PostgreSQL or cross-Goal transaction support and does not move checkpoint
authority into the Todo provider. SQLite cannot roll back the external run files.

Index lock order is kernel then mutation marker for Python writers; existing
quota adapters retain their kernel lock around the native marker owner. Native
writers never wait for the kernel lock. Source writers retain marker then kernel,
in maintenance/Todo/state order. History append/repair, refresh, feedback,
operator-gate, project-map and runtime projection use this shared index boundary;
feedback takes the index before state. The checkpoint effect claims the caller's
index/source markers and owns their release through the durable append. Caller
exit or timeout does not release an in-flight effect's claims. Runtime death
allows the existing conservative PID/token reclaim; a live stalled owner times
out contenders rather than losing its lock. No model or Agent holds a store lock.

Receipts are bound to the exact Goal/Agent/Todo or obligation/Turn. A new read for
that Turn replaces its previous receipt, so its confirmation operations must be
serial; other work may remain parallel. A missing, replaced, or stale receipt
rejects the supplement without appending delivery or spending quota. Rerun
`checkpoint-context`, reread, and rejudge. Never attach a new receipt to an old
judgment. The committed decision includes the receipt identity in its replay
digest: an exact retry returns the original result even if state changed after
commit. Acquiring a receipt for an already satisfied checkpoint is rejected.
Replay also verifies the committed artifact references. A malformed/torn index,
conflicting checkpoint rows, or inconsistent artifacts returns an explicit
unknown/error; prepared JSON/Markdown alone never authorizes a blind append.

Versions are content revisions of the declared decision inputs, including native
revision fields where present. They cannot detect an unobserved change-and-revert
in legacy Markdown, raw writes bypassing the writer locks, or changed bytes behind
an unversioned external link. Upstream deliveries must be represented by their
recorded Todo results/references. The receipt verifies the declared basis, not
whether the model actually understood or used it. It grants no new permissions,
task-completion authority, or evidence of acceptance. Older binaries do not enforce
this admission rule; rolling back loses its freshness protection.

`missing_required` is not a chat reminder. Status keeps it in compact run
history, quota filters it by current `agent_id`, and goal-frontier projection
turns it into `acceptance_gaps[]`. If the current agent has no runnable
Expand Down
16 changes: 13 additions & 3 deletions docs/state-interaction-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -771,8 +771,18 @@ For an accountable, Turn-bound refresh, a successful writeback and a satisfied
vision checkpoint are separate facts. `ok=true` does not imply that an omitted
vision decision was supplied. Inspect `vision_checkpoint.satisfied`.

If the checkpoint is `missing_required`, submit a checkpoint-only refresh with
the **same** Goal, Agent, Todo/obligation, Turn, and delivery fields. Preserve
If the checkpoint is `missing_required`, first run `checkpoint-context` for the
**same** Goal, Agent, Todo/obligation and Turn. Read its returned decision basis
and judge again. Echo its `read_context_id` as `--checkpoint-read-context` in a
checkpoint-only refresh with the original identity and delivery fields. Include
additional used upstream Todos with repeatable `--dependency-todo-id` on the read.
The control plane now rejects supplements with missing, replaced, or stale read
receipts. Reread and rejudge on conflict; do not repeat completed work or attach a
fresh token to an old judgment. A new read replaces the old receipt for that Turn,
so checkpoint confirmations within one Turn must be serial. An exact committed
retry remains idempotent, including when state changed after its commit. See the
[read-basis contract](reference/protocols/goal-vision-replan-contract-v0.md#read-basis-for-checkpoint-only-recovery)
for covered versions and concurrency boundaries. Preserve
the original working directory and explicit target (`--registry`, `--runtime-root`,
`--project`, `--state-file`), scope (`--progress-scope`, `--agent-lane`), and
isolation (`--no-global-sync`, `--suppress-external-sinks`) options, with their
Expand Down Expand Up @@ -822,7 +832,7 @@ without them. Repeating mutations is rejected as
`checkpoint_supplement_must_not_repeat_mutations`; do not simply append vision
arguments to an original command that contains these options.

Add only one vision decision:
Add the read receipt and only one vision decision:

- `--vision-unchanged-reason 'Existing scope and acceptance still apply.'` when
a persisted vision genuinely remains applicable;
Expand Down
3 changes: 2 additions & 1 deletion examples/control_plane/heartbeat-prompt-smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ def assert_sole_notification_authority(task_body: str, *, mode: str) -> None:
body = normalized(task_body)
assert "no-change=surface_only/no spend" in body, mode
assert "material=outcome+vision" in body, mode
assert "缺则同轮按返回命令补齐再terminal" in body, mode
assert "缺则同轮checkpoint-context重判" in body, mode
assert "按凭据仅补vision;过期重读" in body, mode
assert "unchanged→真实--vision-unchanged-reason" in body, mode

if mode == "full":
Expand Down
6 changes: 4 additions & 2 deletions loopx/claude_goal_mode/scripts/goalmode_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,10 @@ def loop_execution_content(goal_id, agent_id) -> str:
"Use interaction_contract.mcp_channel for tool ownership and vision input limits.\n"
"At material delivery, compare the Goal's vision/acceptance with actual evidence.\n"
"Pass the resulting agent_vision or a justified vision_unchanged_reason to\n"
"complete_task. If omitted, use review_task_vision on that completed Todo to\n"
"repair its missing checkpoint without another spend. This is not a Goal-stop\n"
"complete_task. If omitted, call review_task_vision with only that Todo and\n"
"Agent to read the basis; rejudge, then submit read_context_id with the decision.\n"
"Stale/replaced receipts require rereading; lost replies retry the same receipt\n"
"and decision. Recovery never spends again. This is not a Goal-stop\n"
"shortcut: open acceptance needs replan; vision_closed closes a stage and needs\n"
"a successor vision; no_followup requires evidence of no remaining scoped work.\n"
"For new replan work not covered by these tools, use the exact live\n"
Expand Down
1 change: 1 addition & 0 deletions loopx/cli_commands/project_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@

PROJECT_LIFECYCLE_COMMANDS = {
"refresh-state",
"checkpoint-context",
"read-only-map",
"reward",
"operator-gate",
Expand Down
35 changes: 35 additions & 0 deletions loopx/cli_commands/project_lifecycle_refresh_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,19 @@ def register_refresh_state_command(
subparsers: argparse._SubParsersAction,
add_subcommand_format: Callable[[argparse.ArgumentParser], None],
) -> None:
context_parser = subparsers.add_parser(
"checkpoint-context", help="Read a fresh decision basis for an existing Turn's missing checkpoint.",
)
add_subcommand_format(context_parser)
for option in ("goal-id", "agent-id", "turn-instance-id"):
context_parser.add_argument(f"--{option}", required=True)
binding = context_parser.add_mutually_exclusive_group(required=True)
binding.add_argument("--todo-id")
binding.add_argument("--replan-obligation-id")
context_parser.add_argument("--project")
context_parser.add_argument("--state-file")
context_parser.add_argument("--dependency-todo-id", action="append", default=[],
help="Additional upstream Todo result used in the judgment; declared dependencies are included automatically.")
refresh_state_parser = subparsers.add_parser(
"refresh-state",
help="Append a read-only run from active goal state after state-only updates.",
Expand Down Expand Up @@ -280,6 +293,10 @@ def register_refresh_state_command(
"Compact reason why a required vision checkpoint is intentionally unchanged."
),
)
refresh_state_parser.add_argument(
"--checkpoint-read-context", metavar="READ_CONTEXT_ID",
help="Echo the fresh checkpoint-context receipt when supplementing a missing checkpoint on the original Turn.",
)
refresh_state_parser.add_argument(
"--agent-id",
help=(
Expand Down Expand Up @@ -369,6 +386,23 @@ def handle_refresh_state_command(
post_writeback_hooks: Sequence[PostWritebackHookRegistration] | None = None,
post_writeback_projection_builder: PostWritebackProjectionBuilder | None = None,
) -> int | None:
if args.command == "checkpoint-context":
from ..control_plane.goals.checkpoint_context_io import read_checkpoint_context, render_checkpoint_context
try:
payload = read_checkpoint_context(
registry_path=registry_path, runtime_root_override=args.runtime_root,
goal_id=args.goal_id, agent_id=args.agent_id, todo_id=args.todo_id,
turn_instance_id=args.turn_instance_id, replan_obligation_id=args.replan_obligation_id,
project=Path(args.project).expanduser() if args.project else None,
state_file=Path(args.state_file).expanduser() if args.state_file else None,
dependency_todo_ids=args.dependency_todo_id,
)
except Exception as exc:
payload = {"ok": False, "error": str(exc),
**({"error_code": exc.code, **getattr(exc, "payload", {})}
if isinstance(getattr(exc, "code", None), str) else {})}
print_payload(payload, output_format(args), render_checkpoint_context)
return 0 if payload.get("ok") else 1
if args.command != "refresh-state":
return None
fmt = output_format(args)
Expand Down Expand Up @@ -461,6 +495,7 @@ def handle_refresh_state_command(
agent_vision_packet=agent_vision_packet,
merge_agent_vision_patch=merge_agent_vision_patch,
vision_unchanged_reason=args.vision_unchanged_reason,
checkpoint_read_context_id=getattr(args, "checkpoint_read_context", None),
progress_observation=progress_observation,
usage_measurement=usage_measurement,
usage_codex_session=(
Expand Down
15 changes: 15 additions & 0 deletions loopx/control_plane/coordination/file_authority_store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
AuthorityStoreCommitResult,
AuthorityStoreIdentityResult,
AuthorityStoreLoadResult,
AuthorityStoreHead,
AuthorityStoreReadFailure,
AuthorityStoreReceiptResult,
AuthorityStoreScanResult,
Expand Down Expand Up @@ -249,6 +250,20 @@ export class FileAuthorityStore implements AuthorityStore {
}
}

/** Checkpoint-only external append: retain the real writer lock through the
* synchronous callback. This neither commits nor advances authority revision. */
async withCheckpointHead(save: (head: AuthorityStoreHead, identity: string) => JsonObject): Promise<JsonObject> {
return await withFileMutationLock(this.path, async () => {
const identity = await this.readStoreIdentity(false);
const current = await this.readDocument();
if (!current) throw new FileStoreUnavailableError("checkpoint authority is missing");
const result = save({head: structuredClone(current.head),
provider_revision: current.provider_revision, cursor: current.cursor}, identity);
if (result instanceof Promise) throw new Error("checkpoint save must be synchronous");
return result;
});
}

async commitAuthority(commit: AuthorityStoreCommit): Promise<AuthorityStoreCommitResult> {
let normalized: AuthorityStoreCommit;
try {
Expand Down
24 changes: 23 additions & 1 deletion loopx/control_plane/coordination/sqlite_authority_store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { DatabaseSync } from "node:sqlite";

import type { JsonObject } from "../effect_program.ts";
import type { AuthorityStore, AuthorityStoreCommit, AuthorityStoreCommitResult, AuthorityStoreCommittedTransaction,
AuthorityStoreIdentityResult, AuthorityStoreLoadResult, AuthorityStoreReadFailure,
AuthorityStoreIdentityResult, AuthorityStoreLoadResult, AuthorityStoreReadFailure, AuthorityStoreHead,
AuthorityStoreReceiptResult, AuthorityStoreScanResult } from "./authority_store.ts";
import { AuthorityStoreProtocolError, canonicalAuthorityBytes, canonicalAuthorityObject,
canonicalAuthorityObjectList, canonicalAuthoritySha256, normalizeAuthorityStoreCommit,
Expand Down Expand Up @@ -422,6 +422,28 @@ export class SqliteAuthorityStore implements AuthorityStore {
finally { db?.close(); }
}

/** No await between BEGIN and ROLLBACK: another DatabaseSync request must not
* block this event loop while the transaction holder awaits filesystem I/O.
* The transaction excludes writers; it cannot roll back external run files. */
async withCheckpointHead(save: (head: AuthorityStoreHead, identity: string) => JsonObject): Promise<JsonObject> {
const db = this.open(true);
if (!db) throw new Error("checkpoint authority is missing");
let active = false;
try {
db.exec("BEGIN IMMEDIATE");
active = true;
const current = this.current(db);
if (!current) throw new Error("checkpoint authority head is missing");
const result = save({head: current.state.projection,
provider_revision: current.provider_revision, cursor: current.state.cursor.toString()}, current.identity);
if (result instanceof Promise) throw new Error("checkpoint save must be synchronous");
return result;
} finally {
try { if (active) db.exec("ROLLBACK"); }
finally { db.close(); }
}
}

async commitAuthority(commit: AuthorityStoreCommit): Promise<AuthorityStoreCommitResult> {
let normalized: AuthorityStoreCommit;
try {
Expand Down
Loading
Loading