The six columns
unverified is a terminal state, and it is not a failure. It means the work
was claimed done and the platform could not independently confirm it. Every card
in that column carries unverified_because with the reason — a terminal state a
user cannot explain becomes “the product is broken”.How the six columns are computed — hermes
On a hermes tenant the six columns are derived from two legacy columns,tasks_mirror.status and tasks_mirror.verification_status, by a single reader:
There is no database constraint making those two columns agree, so an unrecognised
legacy status is reported as
open rather than dropped. This mapping disappears
the day the constraint lands; until then it is the one place the rule lives.
How the six columns are computed — durable
A durable tenant’s board is not read fromtasks_mirror. It is the runtime’s
own board, fetched over the control seam and translated. The runtime’s task state
is a closed domain — a SQL CHECK of exactly six values — and the translation is
total over it:
Six states in, five columns out.
unverified is not in the image of that map,
which is why the warning above is a statement about what can happen rather than
about what has been seen to happen.
The runtime’s own state is carried through, so nothing is lost silently. Every
durable row carries
runtime_status beside status, holding the untranslated
value. The two partitions do not agree in either direction: review — the work is
done and the reviewer has not accepted it — has no column of its own here and maps
onto awaiting_check, and unverified has no runtime state at all. A reader that
needs the distinction reads runtime_status.Review and retries — durable only
A durable card carriesattempts. When the reviewer rejects a completion the card
goes back to ready and the attempt count advances; the card is blocked with
block_reason: "exhausted" once the attempts run out.
Query parameters
Paging is hermes-only
A durable tenant’s board comes back whole, in one answer: the runtime’s board route has no cursor and takes no limit. The response says so rather than leaving anull to be misread as “that was the last page”:
limit and cursor are accepted and ignored on that lane. A client that pages
until next_cursor is null terminates correctly on both runtimes; a client that
sizes its own buffer from limit does not.
task_id vs row_id
Two ids, on purpose.
task_idis the id the runtime addresses a card by, and the id every existing caller already holds.GET …/board/{card}takes this one.row_idis the naive UUID of the mirror row.
task_id means would break the mirror close path.
On a durable tenant there is no mirror row, so both fields carry the runtime’s
card id and are equal. Address the card by task_id on either runtime and the
code is the same; do not use the pair’s equality to detect the provider, because
provider already says it.
blocked_reason is inferred — on hermes
When a hermes card is blocked, blocked_reason is "assignment" and
blocked_reason_is_inferred is true. There is no stored reason column; the
value is the reader’s guess, and the sibling boolean is how you know not to quote
it back to a user as fact.
On a durable tenant the reason is real: the runtime stores it under a closed
CHECK, blocked_reason_is_inferred is false, and a longer blocked_detail
accompanies it where the runtime supplies one. The seven values are
blocked_reason_is_inferred is the field that tells you which of the two you are
reading; branch on it rather than on the provider.
"exhausted" is the durable runtime’s report of a completion it refused to
accept, not of work that could not be done. The card was attempted, reviewed and
rejected until the attempts ran out. Whether the rejections were right is a
question the board cannot answer — GET …/events carries the
reviewer’s reason for each one, and that is the only place it is recorded.One card
{card} is the task_id, not row_id. Returns the same card object plus:
runs— up to 50 runs against this card, newest firstevents_url— the exact path to page this card’s framestrace_id_unavailable_because— there is no trace id column on the run mirror
404 not_found means no card with that id exists for this tenant.
Frames
source is always platform, and that is a claim you can rely on. These rows
are written by the API from the runtime mirror and never from a request body, so
no caller can append a frame to this list claiming to be someone else. This is not
true of every event table in the product, which is why the field is stated rather
than assumed — and it is the reason
GET …/runs/{id}/stream is refused on a hermes
tenant rather than approximated from a table a caller can write to.That argument does not carry to the durable lane, and the stream is not refused
there: a durable tenant’s transcript is proxied frame for frame from the runtime,
which is not naive’s mirror and is not caller-writable. Read
Runs for what the durable stream does and does not
carry — it is served with two of its stated prerequisites still absent, and the
page names both.seq is monotonic per frame and is what the cursor pages on. It is a mirror row
id, not a per-run sequence number.