Agent-First PSQL v0.11.0: A Cancel Is a Request
Cancelling a query took its terminal event slot before the CancelRequest was even sent, so a write that had already committed came back as 'cancelled' — the worst answer this program can give. That, a COMMIT sent down a brand-new connection after the backend died, and inspection panels rebuilt around objects instead of the snapshot query's row shape.
Two of the fixes here are about the same thing from opposite ends: a database telling you an operation did not happen when it did. An agent that believes it redoes the work, or tells a person nothing happened. Both answers are worse than an error.
A cancel is a request, not an outcome
Cancelling a query claimed the query’s terminal event slot before the
CancelRequest had even been sent, emitted cancelled, and dropped whatever the
worker eventually reported.
The protocol says nearly the opposite. A CancelRequest gets no reply. It can
arrive after the backend has already finished. The client has to keep waiting on
the original query to learn what actually happened — which means “I asked for a
cancel” and “the query was cancelled” are different facts, and only the second
one is an outcome.
So a committed write could be reported as cancelled. That is the worst answer
this program can give: the agent either does the work twice or reports to a
person that nothing happened, and the database disagrees with both.
Now the two cases are separated by whether anything reached the database at all:
- a query still queued is claimed atomically and does get a terminal
cancelled, because nothing was ever sent; - a running query gets a
cancel_requestedacknowledgement, keeps its id, and its own terminal event still arrives — success, error, or a cancellation the backend actually performed.
The first attempt at this mapped SQLSTATE 57014 straight to cancelled, and
the gate caught it: PostgreSQL sends the same code when statement_timeout
expires, and the only thing separating the two is message text callers are
explicitly told not to read. It settles on what this side already knows — whether
a cancel was requested for that query at all.
A lost COMMIT is not a retry
tx_commit and tx_rollback called ensure_connected.
Read that with a dead backend in mind. The connection carrying the open
transaction is gone; ensure_connected opens a new one; COMMIT goes down it.
PostgreSQL answers a COMMIT outside a transaction block with a warning and a
success tag — so the caller was told a transaction committed when the
transaction, and everything in it, had been rolled back by the disconnect.
Neither call reconnects now. A transaction that lost its connection reports that, which is a thing an agent can act on.
Panels shaped like objects, not like the query
ui table, ui schema and ui indexes handed the template the schema-snapshot
query’s own row shape: eight kinds of object unioned into one set of generic
columns, most of them null on any given row, with nullable, primary key, unique
and owner packed into a payload object a person had to read as JSON.
That is the right shape for an agent and the wrong one for the reader these panels exist for.
The translation happens in a document builder now rather than in a template,
because the interesting parts are judgements about what is true: grouping rows
by relation, dropping the constraint rows that only restate a column’s own not null, and reading estimated_rows: -1 as “never analyzed” rather than as a
count. A template still decides whether a column list renders as a table or a
list; it never has to know that -1 is not a number of rows.
One shape per panel: a table opens on its object, a schema is a scannable list of
collapsed summaries, indexes group under their table. document.table still
carries the untouched result set behind a details block, so nothing that was
summarized is thereby hidden. templates/relation.html.j2 joins the contract,
shared by the table and schema pages and includable by an override that ships no
copy of it. ui_api_version is 2.
Where a panel can reach, and where it cannot
The five ui panels take --mode window|session now, resolved through AFUI’s
own delivery plan — explicit --mode first, then AFUI_DELIVERY, then window.
Before this they built a session and opened a window directly, so a machine with
no display could not open one at all.
link is refused, and refused before the panel does any work or the readiness
event goes out. Publishing a database panel over link binds every interface and
advertises the result on the network with no tunnel — a wider, unencrypted exit
for exactly the rows a locked profile already withholds.
That reasoning also replaced the locked-profile refusal’s old one. It used to say that opening a window spawns a browser and writes a profile directory, which is true and covers only window delivery — a published session would have been waved through. Judging by where the data behind a panel can leave the process holds for every delivery a panel could ever gain, without growing a case per delivery.
Smaller things
In a framed session the monitor hides its own header and sends up the live line, since the frame already says whose program this is and what the session is on. A panel that is waiting for a decision keeps its head wherever it is shown, because a person is being asked to answer for what that page says.
Apple’s container joins the transports — --container-apple-name,
--container-apple-user, --container-apple-runtime — and the local gates run
on it.
Trusting a frontend in a test names the config directory instead of pointing a process-wide variable at it. Cargo runs test functions on several threads, so two tests trusting at once wrote into each other’s store, and whichever lost served the built-in page — under an assertion that said “my override did not serve”, which points nowhere near the cause. Three parallel runs of the old suite failed 2, 0 and 1 times.
And the pipe test stopped counting a cancel acknowledgement as an outcome, which made it pass or fail on whether the cancel happened to land while the query was still queued. It asserts the acknowledgement arrives at most once instead.
Breaking changes
- A running query’s cancel returns
cancel_requested, notcancelled. Keep waiting on the query id for its terminal event. tx_commitandtx_rollbackno longer reconnect. A transaction whose connection died reports that instead of returning success.ui_api_versionis 2, and the table, schema and indexes documents are object-shaped. An override reading the old row shape needs updating;document.tablestill holds the raw result set.- Agent-First UI 0.5.0 and Agent-First Data 0.34.0 are the pinned dependencies.
Getting it
$ brew install agentfirstkit/tap/afpsql
$ cargo install agent-first-psql