Skip to content

Releases: crier-dev/crier

v0.2.0

Choose a tag to compare

@github-actions github-actions released this 21 Oct 03:43

[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
    model and provider — the pair a consumer wants, or the members a bunker
    task-router interprets when one or both are default — plus reply_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, and absent/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 plus reply_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 deliberate send grants (scheduler →
    the router's inbox, router → the scheduler's inbox), unrelated pairs are
    refused with 403 DELIVERY_FORBIDDEN, and a reply_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}/tasks lists 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), and GET /sessions/{id}/tasks/{task_id} reads one — 404 TASK_NOT_FOUND for an id that is not a task in the session. The POST /sessions/{id}/tasks 201 response also names the task's OWN id and state at
    the TOP level (task_id, state) in addition to the existing nested task
    object: the message view's id is the MESSAGE id, so before this a caller
    reading the obvious top-level state saw null and a caller that claimed the
    top-level id was refused TASK_NOT_FOUND on a task it had created moments
    earlier. The current state is the latest record-version over the task id
    (§3.7 keep-LAST). Covered by TestTaskLifecycle_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 four TaskPushNotificationConfig
    operations of the A2A specification (CreateTaskPushNotificationConfig,
    GetTaskPushNotificationConfig, ListTaskPushNotificationConfigs,
    DeleteTaskPushNotificationConfig, §9.4.7) are served as methods of the
    existing POST /a2a binding — 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 body PATCH /agents/{id} already accepts, handed to that route's OWN
    handler, so crier's strict webhook member decode, its webhook.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 as capabilities.pushNotifications: false — gets
    PushNotificationNotSupportedError (-32003) from all four operations, never
    a silent success. authentication.credentials and token are 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.taskPushNotificationConfig on a send stays refused, now naming
    the operations that do configure a channel. The notification itself is
    §4.3.3's StreamResponse envelope, 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: with CR_A2A_ENABLED unset 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=solver could 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 review DISPATCH · CRI-001 by Carter measured live at
    ca28523d). POST /capabilities/{capability}/inbox takes the same body as
    POST /agents/{id}/inbox and picks the holder: candidates are every agent
    whose advertised capabilities include 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_AGENT naming 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 an idempotency_key is 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, same target — 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 by TestCapabilityDelivery* 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
    review DISPATCH · CRI-001 by 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 priority on a delivery (POST /agents/{id}/inbox,
      integer 0..9, default 0) — GET /agents/{id}/inbox hands 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_GLOBAL
      with a Retry-After header (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-After header the review found ...
Read more

v0.1.0-rc2

Choose a tag to compare

crier v0.1.0-rc2 (2026-09-21)

First make release-tagged release candidate.

Added

  • Release tooling (RELEASE-001): a make release VERSION=vX.Y.Z target that
    refuses anything but a clean main with a free vX.Y.Z tag, runs
    make build && make lint && make test-short and creates the annotated tag
    crier VERSION — printing the push commands for the configured remotes
    instead of pushing them; this changelog; and docs/releases.md.

Full Changelog: v0.1.0-rc1...v0.1.0-rc2