Repository navigation
Releases: crier-dev/crier
Release list
v0.2.0
[0.2.0] - 2026-10-21
Range: v0.1.0-rc2..v0.2.0 — 660 non-merge commits (75 feat, 121 fix,
43 docs, 12 test, 4 ci, 262 chore, 97 board, plus qa/dogfood/gitreins/
security/refactor). No BREAKING CHANGE footer and no ! type token anywhere in
the range, so this is a minor release. The entries below are the release content
as it was written while the work landed.
Added
-
Optional model/provider hints and a correlated reply on the dispatch
envelope (CR-FEAT-036,docs/router-dispatch.md). A lane dispatch may carry
modelandprovider— the pair a consumer wants, or the members a bunker
task-router interprets when one or both are default — plusreply_to, the
Crier message id a returned result answers. Crier TRANSPORTS the two hints and
never chooses one: an explicit value survives delivery byte for byte (no
trimming, no validation against any catalogue of models), a blank string is
preserved as sent, andabsent/null/blank are all DEFAULT hints. Omission
is the canonical spelling: absent/null become omitted members, while blank
strings remain present and consumers must also treat them as default. No
concrete model or provider is filled in. The members ride the request, the stored message
(one JSONB column plusreply_to; migration 013) and every read of it —
retrieve/lease, peek, list — so the polling router sees exactly what the
scheduler wrote; a delivery that names none of them is byte-identical to
before, and the remote-proxy deliver path forwards them verbatim instead of
dropping them. Authorization is unchanged and stays out of the payload: the two
sends a router deployment needs are two deliberatesendgrants (scheduler →
the router's inbox, router → the scheduler's inbox), unrelated pairs are
refused with403 DELIVERY_FORBIDDEN, and areply_to— in the envelope or
inside the payload — confers nothing. Covered by
internal/registry/crfeat036_test.go: all four selection combinations across
every supported spelling, over the real HTTP surface, plus the round trip
(lease → correlated reply → ack), the signed-identity read gate, and the
lease-expiry redelivery. -
The session task lifecycle is now READABLE, not fire-and-forget
(DF-CRIER-304,specs/CHAT-ADDRESSING.md§2.5). Two read routes:
GET /sessions/{id}/taskslists every task in a session at its CURRENT state
(one entry per task id, each with the task id the transitions take, the state,
the owner once claimed, and the message record-version and thread the state
lives in), andGET /sessions/{id}/tasks/{task_id}reads one —404 TASK_NOT_FOUNDfor an id that is not a task in the session. ThePOST /sessions/{id}/tasks201 response also names the task's OWN id and state at
the TOP level (task_id,state) in addition to the existing nestedtask
object: the message view'sidis the MESSAGE id, so before this a caller
reading the obvious top-levelstatesaw null and a caller that claimed the
top-levelidwas refusedTASK_NOT_FOUNDon a task it had created moments
earlier. The current state is the latest record-version over the task id
(§3.7 keep-LAST). Covered byTestTaskLifecycle_Verifiable, on both the JSONL
log and the SQLite view. -
A2A push-notification configuration as a view over crier's webhook config
(INT-A2A-005,specs/A2A-OPTION.md§5.5). The fourTaskPushNotificationConfig
operations of the A2A specification (CreateTaskPushNotificationConfig,
GetTaskPushNotificationConfig,ListTaskPushNotificationConfigs,
DeleteTaskPushNotificationConfig, §9.4.7) are served as methods of the
existingPOST /a2abinding — no new route — and they configure the push
channel crier already has: the agent's webhook config. There is no second
store, no second delivery engine and no second write path. A create or a delete
is the bodyPATCH /agents/{id}already accepts, handed to that route's OWN
handler, so crier's strictwebhookmember decode, itswebhook.Config
validation and its agent-owned signature gate apply to an A2A write exactly as
they do to a direct one, and every field the A2A object has no member for
(retries,timeout_ms,batch,delivery_mode,schema_template) is
PRESERVED rather than reset. The configuration id is derived
(wh-<sha256(tenant‖url)>), never stored, so there is no second source of
truth to disagree with the row. The capability answer is a MUST, not a
nicety (§3.3.4): an agent with no webhook — the same condition its Agent Card
reports ascapabilities.pushNotifications: false— gets
PushNotificationNotSupportedError(-32003) from all four operations, never
a silent success.authentication.credentialsandtokenare refused rather
than stored or dropped, because crier references a secret by NAME
(auth_value_ref) and holds no credential to return; an inline
configuration.taskPushNotificationConfigon a send stays refused, now naming
the operations that do configure a channel. The notification itself is
§4.3.3'sStreamResponseenvelope,Content-Type: application/a2a+json,
delivered by the shipped webhook driver: the payload shape is the config's
custom_schema, so the driver, its retries, its batching and its headers are
untouched. Default-off and additive: withCR_A2A_ENABLEDunset nothing here
exists, no route or field was added, and a refused write leaves the row
byte-identical. A2A remains an extra, not first-class support. -
Capability-routed delivery — the registry's capability index as a dialable
worker pool (CR-FEAT-026).GET /agents?capability=solvercould already tell
you who advertises a capability, but delivery still required naming ONE agent
id, so a pool of interchangeable workers could not be addressed as a pool (the
gap the external reviewDISPATCH · CRI-001by Carter measured live at
ca28523d).POST /capabilities/{capability}/inboxtakes the same body as
POST /agents/{id}/inboxand picks the holder: candidates are every agent
whose advertisedcapabilitiesinclude the name (matched exactly like the
discovery filter), live holders rank first (liveness is the registry's own
derived status, so a crashed worker absorbs no work while a live one exists),
and the choice round-robins over that pool — one holder per delivery,
agent-id ascending, one cursor per capability, advanced once per dispatched
delivery. The accept names what it chose ("capability":"solver",
"target":"worker-b") because the sender has to know which worker took the
work; a by-id delivery's body is unchanged. Zero holders is a named error,
never a silent drop:404 NO_CAPABLE_AGENTnaming the capability and stating
that selection is local to the relay (federation forwards by agent id) — while
a capability whose holders are all merely stale still ACCEPTS, because the
inbox is durable and an untaken message is carried by the existing expiry
receipt. A holder that dies mid-lease needs no case of its own: the lease
expires and the message returns to the queue it was delivered to. A retry
carrying anidempotency_keyis scoped to the CAPABILITY (the holder is not
known when the key is resolved), so a repeated key is answered with the first
attempt's accept — same id, sametarget— instead of dispatching the same job
to a second worker, and it is answered even if the pool has since emptied.
Everything after the choice is the existing deliver path (guard choke point,
webhook driver, durable inbox, lease/ack/TTL, federation fallback), and the
detection layer observes a routed delivery with the RESOLVED holder as its
target and a zero-holder refusal with an empty one. Two counters
(capability_routed_total,capability_unheld_total) make a pool that has
gone empty visible. Proved byTestCapabilityDelivery*in
internal/registry(landing + signed retrieve + ack, the exact rotation, a
stale holder skipped, the named refusal with nothing stored, capability-scoped
idempotency, lease requeue after a holder dies, one observation per request,
and a concurrent 40-delivery rotation under-race). Source: the external
reviewDISPATCH · CRI-001by Carter, delivered via Bane. -
Priority lanes and real backpressure (CR-FEAT-035) — the three gaps the
external review (DISPATCH · CRI-001, by Carter) named in passing, closed
without changing what an existing deployment does:- an optional
priorityon a delivery (POST /agents/{id}/inbox,
integer 0..9, default 0) —GET /agents/{id}/inboxhands back the highest
priority claimable messages first, ties keep arrival order, and a delivery
that names no priority is exactly the FIFO message it always was (the key is
omitted from the wire at the default, so a pre-existing client parses
identical bytes). Out of range is a 400 naming the range, never a clamp. The
ordering is stored with the message (PostgreSQL migration 007,priority NOT NULL DEFAULT 0, range-checked), so it survives a restart and every
message an existing database already holds reads back in the order it has
always had. - a global ingest budget (
CR_RATE_LIMIT_GLOBAL_PER_MINUTE, default 0 =
no budget) — a shed on the delivery path itself, across every agent and
sender, which the per-agent publish cap could never be: a flood from many
ids spends every agent's own budget. A delivery over budget is refused 429
RATE_LIMITED_GLOBALwith aRetry-Afterheader (whole seconds) and the
same wait in the body, before any transport, guard call or store write —
and the check sits after decode/validation/containment, so a malformed
delivery still answers 400 and a contained agent still answers 403
AGENT_QUARANTINED. The pre-existing per-agent publish 429 gained the
Retry-Afterheader the review found ...
- an optional
v0.1.0-rc2
crier v0.1.0-rc2 (2026-09-21)
First make release-tagged release candidate.
Added
- Release tooling (
RELEASE-001): amake release VERSION=vX.Y.Ztarget that
refuses anything but a cleanmainwith a freevX.Y.Ztag, runs
make build && make lint && make test-shortand creates the annotated tag
crier VERSION— printing the push commands for the configured remotes
instead of pushing them; this changelog; anddocs/releases.md.
Full Changelog: v0.1.0-rc1...v0.1.0-rc2