Repository navigation
feat(antd-py): pointer API (REST + gRPC) - #335
Conversation
RestClient, AsyncRestClient, GrpcClient and AsyncGrpcClient gain the daemon's pointer surface with the same signatures, models and errors: - pointer_keys, pointer_create, pointer_update, pointer_transfer - pointer_get, pointer_resolve, pointer_finality, pointer_controller - pointer_prepare (keyword options, include_signed_quotes among them) and pointer_finalize, for an external signer The models are frozen dataclasses: PointerWriteResult, PointerRecord, PointerTarget, PointerFinality, PointerFinalState, PointerController, PointerTransferResult, PointerPrepareResult, PointerFinalizeResult and SignedQuoteEntry, with the PointerKind, PointerOp and PointerFinalityStatus enums. Finality status has an UNKNOWN member for statuses a newer daemon may add, with the daemon's string kept in raw_status. REST nulls and gRPC empty strings or absent messages both read as None. The shared mapping lives in a new _pointers module, so a REST-only install still never imports grpc. Counters are Python ints. The daemon sends them as decimal strings, since a final state sits at u64::MAX (POINTER_FINAL_COUNTER). They are parsed strictly: ASCII digits only, no larger than u64::MAX. int() alone would take "+1", " 1" and "1_000", so anything that is not a decimal u64 raises InternalError rather than reading as zero. Errors map by the daemon's code where it shares a status with older errors. POINTER_FINAL, POINTER_FORKED and CONFLICT are 409s and no longer read as AlreadyExistsError: they raise PointerFinalError, PointerForkedError (a ForkError subclass) and ConflictError. CLOSE_GROUP_SHORTFALL is a 503 raising CloseGroupShortfallError, whose retryable flag says a pointer finalize kept the paid state. Over gRPC the same classes come from the x-antd-error-code trailing metadata, on both the sync RpcError and the aio AioRpcError, but only when the tag agrees with the status code; retryable comes from the message's closing "(paid attempt retained" hint. The mapping applies to every RPC, so a chunk store whose close group fell short raises CloseGroupShortfallError too. The new classes are additive, and the codes they catch come only from a daemon with the pointer API. No write is ever retried; the README says each one pays. Stubs: pointers added to scripts/generate_proto.py, regenerated with the pinned grpcio-tools 1.80.0 (protobuf gencode 6.31.1). The existing stubs regenerate byte-identically. Tests: 146 new unit tests over a mock REST daemon and an in-process gRPC PointerService, sync and async: every method, each op shape prepare sends, signed quotes, the error codes both ways with mismatched and unknown tags, and counters up to u64::MAX and the malformed ones. The suite is 509 passed, 8 skipped. tests/test_live_pointers.py is gated on ANTD_TEST_URL, ANTD_TEST_GRPC and ANTD_TEST_POINTER_OWNERS. It passed against a 10-node devnet over all four clients: - create, update, get, resolve, with bytes fetched back through the pointer - finality, and a gRPC create and get - a transfer, then POINTER_FINAL over REST, gRPC and aio gRPC - a prepare up to the quote, with CONFLICT while it is pending examples/09_pointers.py (ANTD_POINTER_OWNER, optional ANTD_BASE_URL) stores two values, creates a pointer at the first, updates it to the second, then resolves it and prints its finality (open, 7/7 answered, counter 1) against the same devnet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dirvine
left a comment
There was a problem hiding this comment.
REQUEST_CHANGES — incorrect payment guarantee on pointer CONFLICT
Reviewed head: 8a4938650722e93f27fb504bd2428884ed0ea601. Frozen base: cea236950084f8738106cc37247c148f0b411d18. Head and base were rechecked before publication.
P2 — the public ConflictError contract promises an unpaid outcome
antd-py/src/antd/exceptions.py:175–178 documents both pending-write conflicts and superseded updates, then says Nothing was paid. A superseded update can return this error after payment. This is a false payment guarantee in the SDK's public error contract, not a request to change the error code or exception mapping.
Minimum fix: correct this docstring so it does not infer payment status from CONFLICT. Distinguish the unpaid admission conflict from the potentially paid superseded-write path, and tell callers to read the current pointer and check payment state before submitting a new paid write. Keep the REST/gRPC error mapping. The user-visible MCP hint and its regression are reported separately on #342.
Confirmed causal path
- At the pinned ant-core revision,
pointer_putpays before storing. Its paid storage path can then return the newer-state conflict, viapointer_put_paid/pointer_put_once(lines 1250–1272 and 1320–1323). - In this ant-sdk revision,
antd/src/pointers.rs:172–174maps that specific error toAntdError::Conflict. This daemon source is byte-identical in #335 and #342. - A separate admission conflict for a prepared write awaiting payment is genuinely pre-payment. The same error code therefore does not prove that nothing was paid.
Evidence limits and participation
- Focused two-role panel: 4/6 substantive opinions — 2/3 named models reviewed; provisional team coverage. Qwen 3.8 and DS4 completed correctness and adversarial reviews; all four requested changes. GLM 5.3 returned no complete verdict in either role after one same-model retry. There is no full-team consensus claim.
- The parent checked the source chain and the exact locations below. Some panel outputs used old path labels (
server.py/_rest.py); the actual targets areerrors.py/exceptions.pyas linked here. - No live daemon payment, wallet access or paid E2E was run. The source establishes payment ordering. The offline mock proves only REST parsing and user-visible hint formatting. It does not prove an actual financial loss or a second payment.
Review coverage and checks
- This formal change request is focused on the confirmed error-contract defect. It is not a claim that all Python pointer operations passed a complete broad panel. The broad per-SDK source inspection exists, but its general panel was not run before this blocker was found.
- The offline bridge exercised the real Python REST error parsing into ConflictError and the companion MCP formatter; it was not a standalone full Python SDK validation or a paid E2E run.
- GitHub CI at preflight: 9 SUCCESS. Local source checkout is clean at the reviewed head.
No merge, deployment, payment or Linear ticket-state change was performed. Re-review the fix at its new exact head.
… unpaid A pointer write pays before it stores, and the store can still fail: - an update that loses to a newer state while in flight comes back as CONFLICT after paying, the same code as the unpaid refusal of a write while a prepared write awaits payment; - a pointer that became final while a paid write was in flight is refused by the network as POINTER_FINAL after paying; - a transfer's close-group shortfall can come before or after paying. ConflictError, PointerFinalError and the pointer_update / pointer_transfer docstrings now say which cases are refused before paying and which may already be paid, and tell callers to read the pointer before paying again. The error mapping is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ls (#342) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dirvine
left a comment
There was a problem hiding this comment.
APPROVE — Python pointer API; payment wording re-review
Reviewed #335 at bd87bc809bcfda16535337f6cefb01f653c41823, frozen base cea236950084f8738106cc37247c148f0b411d18. The corrected CONFLICT / POINTER_FINAL wording no longer promises that every refused write avoided payment. No unresolved material code defect remains in this reviewed scope. This is not merge authorisation.
Participation and coverage
4/6 full-PR model/role seats reviewed — partial panel. GLM 5.3/OpenRouter (z-ai/glm-5.3) and local Qwen 3.8 (qwen38-27b) cover correctness and adversarial roles across both source scopes. DS4 Flash (deepseek-v4-flash) additionally completed correctness for source-1 only; it is not counted as a complete full-PR seat. Source-role ledger: 9/12 completed (source-1 5/6; source-2 4/6). Remaining DS4 coverage is incomplete, not agreement; bounded recovery is separate. No full-team consensus is claimed.
Scope source-1 covers models, pointer mappers, typed errors, protocol definitions and exports. Scope source-2 covers sync/async REST and gRPC transports, generation script and example. Generated outputs are checked by exact regeneration, not falsely claimed as line-by-line generated-code review. Follow-ups replace no independent votes and never double-count a seat.
Dissent and resolution
Qwen's initial adversarial review requested changes for supposedly undefined retained-attempt constants, possible double-base64 signed quotes and missing mapper context. The parent checked the exact-head definitions and executed protobuf-descriptor checks. The constants exist; SignedQuoteEntry.quote and commitment_sidecar are bytes, so encoding them once is correct. Qwen re-reviewed that exact context and returned APPROVE. Its source-2 UNCERTAIN opinions were due to missing helper/model/stub context, not a demonstrated defect; both same-model roles returned APPROVE after receiving the complete helper, actual model/error definitions and exact-head regeneration/test results. The original objections and uncertainty receipts remain on disk; they were not discarded by majority vote.
Checks and findings
- Executed offline Python suite: 509 passed, 8 skipped; exit 0. Skips include live/network checks; no paid or live-daemon flow was run.
- grpcio-tools 1.80.0 regenerated all nine protocol inputs without a tracked diff. Quote/sidecar field types and retained-attempt matching were checked with the generated descriptors/runtime.
- Counters reject malformed decimal/u64 values rather than silently returning zero. Unknown finality states remain UNKNOWN.
- REST body codes and matching gRPC status trailers preserve POINTER_FINAL, POINTER_FORKED, CONFLICT and CLOSE_GROUP_SHORTFALL types. Paid-finalize retry hints retain the existing attempt instead of authorising a new payment.
- Live head/base and CI are checked immediately before publication.
Limits / watch-outs
No wallet, paid upload, live-daemon or production-network test was performed. Non-object REST response handling is permissive in shared plumbing, although strict pointer counter parsing fails loudly. gRPC retained-attempt parsing still depends on the existing literal suffix. Those limits are not presented as test coverage.
Receipt ledger: ant-sdk-review-20261019/335/ready-review-ledger.json. No branch changes, merge, release, deployment or payment was performed.
Part of the pointer API fan-out. Stacked on #326; review that first.
RestClient, AsyncRestClient, GrpcClient and AsyncGrpcClient gain the
daemon's pointer surface with the same signatures, models and errors:
and pointer_finalize, for an external signer
The models are frozen dataclasses: PointerWriteResult, PointerRecord,
PointerTarget, PointerFinality, PointerFinalState, PointerController,
PointerTransferResult, PointerPrepareResult, PointerFinalizeResult and
SignedQuoteEntry, with the PointerKind, PointerOp and
PointerFinalityStatus enums. Finality status has an UNKNOWN member for
statuses a newer daemon may add, with the daemon's string kept in
raw_status. REST nulls and gRPC empty strings or absent messages both
read as None. The shared mapping lives in a new _pointers module, so a
REST-only install still never imports grpc.
Counters are Python ints. The daemon sends them as decimal strings, since
a final state sits at u64::MAX (POINTER_FINAL_COUNTER). They are parsed
strictly: ASCII digits only, no larger than u64::MAX. int() alone would
take "+1", " 1" and "1_000", so anything that is not a decimal u64 raises
InternalError rather than reading as zero.
Errors map by the daemon's code where it shares a status with older
errors. POINTER_FINAL, POINTER_FORKED and CONFLICT are 409s and no longer
read as AlreadyExistsError: they raise PointerFinalError,
PointerForkedError (a ForkError subclass) and ConflictError.
CLOSE_GROUP_SHORTFALL is a 503 raising CloseGroupShortfallError, whose
retryable flag says a pointer finalize kept the paid state. Over gRPC the
same classes come from the x-antd-error-code trailing metadata, on both
the sync RpcError and the aio AioRpcError, but only when the tag agrees
with the status code; retryable comes from the message's closing "(paid
attempt retained" hint. The mapping applies to every RPC, so a chunk store
whose close group fell short raises CloseGroupShortfallError too. The new
classes are additive, and the codes they catch come only from a daemon
with the pointer API. No write is ever retried; the README says each one
pays.
Stubs: pointers added to scripts/generate_proto.py, regenerated with the
pinned grpcio-tools 1.80.0 (protobuf gencode 6.31.1). The existing stubs
regenerate byte-identically.
Tests: 146 new unit tests over a mock REST daemon and an in-process gRPC
PointerService, sync and async: every method, each op shape prepare
sends, signed quotes, the error codes both ways with mismatched and
unknown tags, and counters up to u64::MAX and the malformed ones. The
suite is 509 passed, 8 skipped. tests/test_live_pointers.py is gated on
ANTD_TEST_URL, ANTD_TEST_GRPC and ANTD_TEST_POINTER_OWNERS. It passed
against a 10-node devnet over all four clients:
pointer
examples/09_pointers.py (ANTD_POINTER_OWNER, optional ANTD_BASE_URL)
stores two values, creates a pointer at the first, updates it to the
second, then resolves it and prints its finality (open, 7/7 answered,
counter 1) against the same devnet.
Version bumps happen at release time; nothing here bumps a package version.
🤖 Generated with Claude Code