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
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,15 @@ or passing accelerated volume tests is not ten-day continuity evidence.
Local promotion waits for both volume and elapsed-time qualification; it does
not wait for a PostgreSQL service and never expires receipts at day ten.

Bounded caller recovery now has [claim argument guidance](../../reference/source-cli-entrypoint.md#claim-argument-recovery--claim-参数恢复):
one rejected invocation exposes missing/unsupported flags and preserves the
original argv bindings, without inferring executors or changing source/lease
admission. This stays in the existing CLI grammar/formatting adapter; TypeScript
retains claim authority. Real Legacy/File/SQLite retry and negative cases qualify
that boundary. Larger errors versus avoided help, actual model token/retry costs,
source-mode repair and short normal/full replan context remain separate evidence
questions; this does not close long-goal or provider-performance acceptance.

### Delivery semantics: correctness before migration

The replan obligation outcome policy now lives in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,13 @@ promotion gate。
本地晋升等待容量与自然时间双重资格化,不等待 PostgreSQL 服务,也不在第十天使
receipt 过期。

调用方恢复的有界切片现有 [claim 参数指引](../../reference/source-cli-entrypoint.md#claim-argument-recovery--claim-参数恢复):
一次拒绝同时列出缺失/非法参数,保留原 argv 绑定,不猜 executor 或改变 source/lease
准入。语法/展示归现有 CLI adapter,claim 权威保留在 TypeScript。真实
Legacy/File/SQLite 重试与负例只验收这个边界。较大错误与省掉 help 的取舍、实际模型
token/重试成本、source-mode 修复和常规短上下文/replan 完整历史仍是分别待验证的
问题;不代表长目标或 provider 性能验收完成。

### 交付语义:先修正规则,再迁移

Replan 的义务结果规则现收敛到 `work_items/replan_semantics.ts`:接受结果选择、
Expand Down
57 changes: 57 additions & 0 deletions docs/reference/source-cli-entrypoint.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,63 @@ No frontend asset or layout changes, so no repackaging is needed for this slice.
transport。现有 usage-settings HTTP 交互验证共享机器设置。没有前端资源或布局
改动,因此此切片无需重新打包前端。

## Claim argument recovery / Claim 参数恢复

Parsed `todo claim` usage errors now include `error_code=todo_claim_invalid_arguments`
and one `recovery` object. Its `cli_args` is an argv array with the original
registry/runtime, Goal/Todo, supplied actor/executor, project/state path,
preview, operation identity and lease/CAS values. Review `remove_flags`, then
append each `requires_flags` entry with an explicit value before retrying.
Retry from the original working directory when supplied paths are relative.
A missing executor is never inferred from the actor. The command can still
fail admission, registration, ownership, source-mode or lease validation.
This is recovery guidance, not an authority grant or automatic retry.
Unknown flags, invalid flag values and parser-required global inputs retain
their existing argparse diagnostics before this handler is reached.

For example, a claim with an actor but no `--claimed-by`, plus an unsupported
`--turn-instance-id`, reports both in the same packet. The retry excludes the
Turn flag; it neither starts a new Turn nor re-fetches a quota packet. A lease
expected version of `0` is retained, including when its missing idempotency key
must be supplied. Canonical-only flags on legacy state remain a source-mode
rejection; this grammar projection does not remove them or promote state.

The existing Python CLI grammar/formatting adapter owns this local error code
and argv projection. The shared TypeScript claim transaction remains the
authority owner; no new capability, setting, provider or bridge RPC is added.
JSON and Markdown expose the same repair facts. Source and console CLI callers
gain the guidance; frontend/Lark controls and persisted contracts do not change.
Successful commands retain their output. Invalid claim option combinations now
use the claim-specific validator before shared checks, so their first human
diagnostic can differ; existing claim-specific and Turn diagnostics remain.

已解析的 `todo claim` 用法错误现在返回上述 error code 和一个 `recovery`:`cli_args` 用 argv
数组保留原路由、Goal/Todo、已提供的 actor/executor、project/state、preview、
operation 身份和 lease/CAS。先检查 `remove_flags`,再为每个 `requires_flags`
补入显式值后重试;路径为相对路径时,沿用原调用的工作目录。不会从 actor 猜执行者。
修复语法后仍可能被注册、所有权、source mode 或 lease 校验拒绝。这是恢复指引,
不授予权限,也不自动执行。
未知 flag、非法 flag 值和缺 parser 必填输入仍先走既有 argparse 诊断。
缺 `--claimed-by` 又误带 Turn 参数时,一次 packet 同时列出两处;修复不另建 Turn
或重新读取 quota。CAS=0 保留,缺 lease key 时必须填写;legacy 上的 canonical 参数
继续按模式拒绝,不静默删除或 promote。语法/展示沿用 Python CLI adapter,
事务权限仍归 TS claim owner;不新增 capability、设置、provider 或 RPC。
JSON/Markdown 展示相同事实,source/console 获得指引,frontend/Lark 控件和持久合同
不变。成功输出保持原行为;非法参数先经过 claim validator,首条人类诊断可能改变,
既有 claim 专属和 Turn 诊断保留。

Real CLI tests execute the returned argv against isolated Legacy, File and
SQLite state, including preview/replay, actor mismatch, canonical-mode refusal,
hard-lease admission and CAS=0. They do not qualify PostgreSQL transaction
changes, sustained provider performance, model retry rates or benchmark scores.
Measure the error-plus-recovery journey: the richer error costs more bytes;
avoiding a separate help lookup is a consumer benefit, not backend IO savings.

真实 CLI 测试在隔离 Legacy/File/SQLite 上执行返回的 argv,覆盖 preview/replay、
actor 错配、canonical 模式拒绝、hard lease 和 CAS=0;不代表 PostgreSQL 事务改动、
长程 provider 性能、模型重试率或 benchmark 得分验收。应测整个错误到恢复路径:
错误本身增加字节,省掉一次 help 是调用方收益,不等于后端 IO 优化。

## Qualification / 验收

Fresh-interpreter regressions compare source and console entries, preserve
Expand Down
7 changes: 5 additions & 2 deletions loopx/cli_commands/todo.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,8 @@ def handle_todo_command(
"`loopx todo claim`, `loopx todo update`, or another command shown "
"by `loopx todo --help`"
)
if args.todo_command == "claim":
validate_todo_claim_options(args)
validate_shared_todo_options(args)
validate_capability_gap_options(args)
if getattr(args, "turn_instance_id", None):
Expand Down Expand Up @@ -437,7 +439,6 @@ def handle_todo_command(
if not payload.get("dry_run"):
payload["host_action"] = "end_current_heartbeat"
elif args.todo_command == "claim":
validate_todo_claim_options(args)
payload = update_goal_todo(
registry_path=registry_path,
runtime_root_arg=runtime_root_arg,
Expand Down Expand Up @@ -715,7 +716,9 @@ def handle_todo_command(
except Exception as exc:
from ..usage_ping import capture_failure
capture_failure(exc)
payload = todo_error_payload(args, exc)
payload = todo_error_payload(
args, exc, registry_path=registry_path, runtime_root_arg=runtime_root_arg,
)
append_todo_rollout_event(
payload,
args=args,
Expand Down
76 changes: 61 additions & 15 deletions loopx/cli_commands/todo_argument_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import argparse
from collections.abc import Iterable
from pathlib import Path

from ..control_plane.todos.contract import TODO_CONTINUATION_POLICY_VALUES

Expand Down Expand Up @@ -352,28 +353,73 @@ def validate_todo_add_options(args: argparse.Namespace) -> None:
raise ValueError("todo add does not support --successor-todo-id; use todo update/complete to link existing successor work")


_TODO_CLAIM_FIELDS = frozenset({
"role", "todo_id", "claimed_by", "agent_id", "state_file",
"claim_operation_id", "task_lease_idempotency_key", "task_lease_expected_version",
})


class TodoClaimArgumentError(ValueError):
"""CLI grammar rejection; does not decide whether a claim is authorized."""

def recovery(
self, args: argparse.Namespace, *, registry_path: Path,
runtime_root_arg: str | None,
) -> dict[str, object]:
missing = [
flag for flag, field in (("--todo-id", "todo_id"), ("--claimed-by", "claimed_by"))
if not getattr(args, field, None)
]
if args.task_lease_expected_version is not None and not args.task_lease_idempotency_key:
missing.append("--task-lease-idempotency-key")
cli_args = ["--registry", str(registry_path), "--format", "json"]
if runtime_root_arg is not None:
cli_args.extend(["--runtime-root", runtime_root_arg])
cli_args.extend(["todo", "claim", "--goal-id", args.goal_id])
# Reuse the grammar owner; preserve identities and zero-valued CAS versions.
for flag, field in TODO_OPTION_FIELDS:
value = getattr(args, field, None)
if field in _TODO_CLAIM_FIELDS and value is not None and value != "":
cli_args.extend([flag, str(value)])
if args.project:
cli_args.extend(["--project", args.project])
if args.dry_run:
cli_args.append("--dry-run")
return {
"command": "loopx todo claim",
"cli_args": cli_args,
"requires_flags": missing,
"remove_flags": unsupported_todo_options(args, allowed_fields=_TODO_CLAIM_FIELDS),
"reason": (
"Review removed flags, append missing flags with explicit values, then retry. "
"Claim authority and canonical lease checks still apply; this is not an admission."
),
}


def validate_todo_claim_options(args: argparse.Namespace) -> None:
if getattr(args, "turn_instance_id", None):
raise TodoClaimArgumentError(
"--turn-instance-id is supported only by todo complete/supersede settlement"
)
if not args.todo_id:
raise ValueError("todo claim requires --todo-id")
raise TodoClaimArgumentError("todo claim requires --todo-id")
if not args.claimed_by:
raise ValueError("todo claim requires --claimed-by")
raise TodoClaimArgumentError("todo claim requires --claimed-by")
if args.clear_claim:
raise ValueError(
raise TodoClaimArgumentError(
"todo claim requires --claimed-by and does not support --clear-claim"
)
_validate_todo_option_subset(
args,
{
"role", "todo_id", "claimed_by", "agent_id", "state_file",
"claim_operation_id", "task_lease_idempotency_key",
"task_lease_expected_version",
},
"todo claim only accepts --todo-id, --claimed-by, --agent-id, optional --role, "
"--claim-operation-id, --task-lease-idempotency-key, "
"--task-lease-expected-version, --project, --state-file, and --dry-run; unsupported: ",
)
unsupported = unsupported_todo_options(args, allowed_fields=_TODO_CLAIM_FIELDS)
if unsupported:
raise TodoClaimArgumentError(
"todo claim only accepts --todo-id, --claimed-by, --agent-id, optional --role, "
"--claim-operation-id, --task-lease-idempotency-key, "
"--task-lease-expected-version, --project, --state-file, and --dry-run; unsupported: "
+ ", ".join(unsupported)
)
if args.task_lease_expected_version is not None and not args.task_lease_idempotency_key:
raise ValueError(
raise TodoClaimArgumentError(
"--task-lease-expected-version requires --task-lease-idempotency-key"
)

Expand Down
13 changes: 11 additions & 2 deletions loopx/cli_commands/todo_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from ..control_plane.coordination.shadow_management import ShadowManagementError
from ..control_plane.coordination.runtime_shadow_writer_adapter import ActiveStateAuthorityMutationError
from ..control_plane.coordination.local_authority import LocalCoordinationAuthorityUnavailable
from .todo_argument_validation import TodoClaimArgumentError


RolloutEventAppender = Callable[..., dict[str, object]]
Expand All @@ -30,7 +31,10 @@
}


def todo_error_payload(args: argparse.Namespace, exc: Exception) -> dict[str, object]:
def todo_error_payload(
args: argparse.Namespace, exc: Exception, *, registry_path: Path,
runtime_root_arg: str | None,
) -> dict[str, object]:
payload: dict[str, object] = {
"ok": False,
"dry_run": not bool(args.execute)
Expand All @@ -44,7 +48,12 @@ def todo_error_payload(args: argparse.Namespace, exc: Exception) -> dict[str, ob
"error": str(exc),
**lock_timeout_error_fields(exc),
}
if isinstance(exc, (TaskLeaseError, HandoffModeError, LegacyCoordinationWriterFenced, ShadowManagementError, ActiveStateAuthorityMutationError, LocalCoordinationAuthorityUnavailable)):
if isinstance(exc, TodoClaimArgumentError):
payload["error_code"] = "todo_claim_invalid_arguments"
payload["recovery"] = exc.recovery(
args, registry_path=registry_path, runtime_root_arg=runtime_root_arg,
)
elif isinstance(exc, (TaskLeaseError, HandoffModeError, LegacyCoordinationWriterFenced, ShadowManagementError, ActiveStateAuthorityMutationError, LocalCoordinationAuthorityUnavailable)):
payload["error_code"] = exc.code
payload.update(exc.payload)
elif isinstance(exc, LocalCoordinationAuthorityRejection):
Expand Down
10 changes: 10 additions & 0 deletions loopx/control_plane/todos/markdown.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import json
from typing import Any

from .contract import TODO_STATUS_OPEN, todo_marker_for_status
Expand Down Expand Up @@ -248,6 +249,15 @@ def render_todo_markdown(payload: dict[str, Any]) -> str:
)
if payload.get("error"):
lines.append(f"- error: {payload.get('error')}")
if payload.get("error_code") == "todo_claim_invalid_arguments":
recovery = payload["recovery"]
lines.extend([
f"- recovery: {recovery['reason']}",
f"- requires_flags: `{', '.join(recovery['requires_flags']) or 'none'}`",
f"- remove_flags: `{', '.join(recovery['remove_flags']) or 'none'}`",
"- cli_args (append required values before running):",
"```json", json.dumps(recovery["cli_args"]), "```",
])
lines.extend(_render_lease_recovery(payload))
lines.extend(_render_settlement_plan(payload.get("settlement_plan")))
if payload.get("operator_action"):
Expand Down
14 changes: 7 additions & 7 deletions loopx/semantics/project_registry_io_manifest_v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -879,55 +879,55 @@
},
{
"site": "loopx/cli_commands/todo.py::<module>.handle_todo_command::codec_read:load_registry#1",
"line": 331,
"line": 333,
"column": 49,
"kind": "codec_read",
"api": "load_registry",
"classification": "codec_api"
},
{
"site": "loopx/cli_commands/todo.py::<module>.handle_todo_command::codec_read:load_registry#2",
"line": 347,
"line": 349,
"column": 24,
"kind": "codec_read",
"api": "load_registry",
"classification": "codec_api"
},
{
"site": "loopx/cli_commands/todo.py::<module>.handle_todo_command::codec_read:load_registry#3",
"line": 355,
"line": 357,
"column": 24,
"kind": "codec_read",
"api": "load_registry",
"classification": "codec_api"
},
{
"site": "loopx/cli_commands/todo.py::<module>.handle_todo_command::codec_read:load_registry#4",
"line": 535,
"line": 536,
"column": 53,
"kind": "codec_read",
"api": "load_registry",
"classification": "codec_api"
},
{
"site": "loopx/cli_commands/todo.py::<module>.handle_todo_command::codec_read:load_registry#5",
"line": 666,
"line": 667,
"column": 25,
"kind": "codec_read",
"api": "load_registry",
"classification": "codec_api"
},
{
"site": "loopx/cli_commands/todo.py::<module>.handle_todo_command::codec_read:load_registry#6",
"line": 734,
"line": 737,
"column": 13,
"kind": "codec_read",
"api": "load_registry",
"classification": "codec_api"
},
{
"site": "loopx/cli_commands/todo.py::<module>.handle_todo_command::codec_read:load_registry#7",
"line": 779,
"line": 782,
"column": 38,
"kind": "codec_read",
"api": "load_registry",
Expand Down
Loading
Loading