98 lines
5.6 KiB
Markdown
98 lines
5.6 KiB
Markdown
# W02 error, idempotency, and fencing contract
|
|
|
|
This document belongs to the current Agent gRPC contract. The package name is
|
|
not a protocol version; `RequestMeta.protocol_version` remains meaningful. It
|
|
does not change the SaaS/MQ contract.
|
|
|
|
## Transport and domain errors
|
|
|
|
Handlers return normal gRPC status codes and, when a response message exists,
|
|
put the stable `FailureCode` in `Failure.code` as well:
|
|
|
|
| `FailureCode` | gRPC status | Retry rule |
|
|
| --- | --- | --- |
|
|
| `INVALID_ARGUMENT` | `InvalidArgument` | Fix the request; never retry unchanged |
|
|
| `UNAUTHENTICATED` | `Unauthenticated` | Re-establish mTLS/session; do not replay business work |
|
|
| `PERMISSION_DENIED` | `PermissionDenied` | Stop; require a new authorization |
|
|
| `FAILED_PRECONDITION` | `FailedPrecondition` | Refresh state/barrier, then use the original operation ID only if allowed |
|
|
| `ABORTED` | `Aborted` | Re-read the CAS revision; do not assume the operation applied |
|
|
| `RESOURCE_EXHAUSTED` | `ResourceExhausted` | Wait for durable quota/resource release |
|
|
| `UNAVAILABLE` | `Unavailable` | Reconnect and reconcile the original operation before any retry |
|
|
| `DEADLINE_EXCEEDED` | `DeadlineExceeded` | Result is unknown unless a durable receipt exists |
|
|
| `NOT_FOUND` | `NotFound` | Do not create a substitute execution |
|
|
| `ALREADY_EXISTS` | `AlreadyExists` | Read the existing operation/result; do not create a second one |
|
|
|
|
A transport success is not an application receipt. `RESULT_CODE_ACCEPTED` means
|
|
that the receiving side durably recorded the request; `APPLIED` requires the
|
|
specified state transition and barrier evidence.
|
|
|
|
## Idempotency keys
|
|
|
|
`RequestMeta.idempotency_key` is required for mutating methods. The key is
|
|
scoped by `(agent_id, operation_id, idempotency_key)` and the durable operation
|
|
record also stores the request content digest. Reusing a key with different
|
|
content returns `ABORTED`/`RESULT_CODE_CONFLICT`; it never overwrites the first
|
|
request.
|
|
|
|
- `ActivateAgent`: `activation_operation_id` is the idempotency key. A repeated
|
|
identical request returns the same session generation and credential metadata.
|
|
- `SetAdmissionState`: `barrier_id` plus the expected admission generation is
|
|
persisted. A replay cannot move the generation twice.
|
|
- `Execute`: the execution binding and permit ID are the deduplication identity.
|
|
An unknown result is reconciled with `QueryExecution`; it is never retried as
|
|
a new originate.
|
|
- `ExecuteAuthorized` (`agent-authorized-origination.v0.1`, isolated Mock only):
|
|
the Dispatcher first persists one `issued` or `refused` decision per execution.
|
|
The Agent durably records `UNKNOWN` **before** invoking the mock adapter;
|
|
identical operation replays return the stored receipt, changed content or a
|
|
second operation for the same execution is a conflict. A lost reply or mock
|
|
adapter failure requires `QueryExecution`, never another originate or a new
|
|
execution ID. An expired Dispatcher-issued deadline cannot invoke the adapter.
|
|
- `GetExecutionPermit`: the reservation, binding, expected revision and
|
|
idempotency key are persisted. A permit is not issued after the reservation is
|
|
released or fenced.
|
|
- `ApplyTaskControl`: `(execution_id, expected_task_revision, action,
|
|
idempotency_key)` is CAS-checked. `STOP` is terminal; `PAUSE` may be resumed
|
|
only by a new authorized request.
|
|
- `ReportExecutionEvent`: `(fact_id, content_sha256)` is the durable fact key.
|
|
An identical duplicate returns the original receipt; a digest mismatch is a
|
|
conflict.
|
|
- `RequestUpload`/`CompleteUpload`: `upload_id` and the asset checksum are
|
|
retained. Completion is returned only after the `recording.uploaded` fact is
|
|
durably queued; a local PUT success alone is not `recording.uploaded`.
|
|
|
|
Read-only methods may be retried, but callers still preserve the original
|
|
request and trace identity: `GetAgentStatus`, `GetBootstrap`, and
|
|
`QueryExecution`.
|
|
|
|
## Fencing and session rules
|
|
|
|
1. The Dispatcher creates a new opaque `dispatcher_epoch` when its active
|
|
instance changes. The Agent accepts mutations only for the active epoch.
|
|
2. The Agent identity is the mTLS certificate plus the Dispatcher-approved
|
|
`(agent_id, cell_id, boot_id, session_generation)` binding. Self-reported
|
|
identity or endpoint values are not authorization.
|
|
3. A newer boot or session generation fences older requests. The old request
|
|
returns `UNAUTHENTICATED` or `ABORTED` and cannot release an unknown lease.
|
|
4. The existing `Execute` permit contains the dispatcher epoch, session
|
|
generation, reservation and `fencing_token`; the Agent checks them before
|
|
that legacy path. The separate Mock-only `ExecuteAuthorized` path requires
|
|
an active session and the Dispatcher-issued exclusive deadline, without a
|
|
second business-policy calculation or an implicit permit/fallback. Its
|
|
mixed/real execution is disabled pending separate authorization.
|
|
5. Admission close/drain is a prerequisite barrier. `SetAdmissionState` and
|
|
`ApplyTaskControl` are applied only when their expected generation/revision
|
|
matches durable state.
|
|
6. When the result of a mutation is unknown, the caller first queries the
|
|
original operation/execution and records `UNKNOWN` if evidence is absent.
|
|
No new execution ID or new permit is invented for recovery.
|
|
|
|
## Upload boundary
|
|
|
|
The Agent receives a restricted `UploadGrant` and uploads directly to the
|
|
approved OSS target. The Dispatcher never receives audio bytes. The Agent
|
|
reports only asset metadata/checksum through `CompleteUpload`; the Dispatcher
|
|
reliably queues the original `recording.uploaded` fact in MQ. This project does
|
|
not request a SaaS upload session, wait for `verified`, or invent an OSS ID;
|
|
MQ publisher confirmation is not SaaS application receipt.
|