The reader always reported a live queue, so a queue_oldest_age watch on a deleted queue sat
pending for its whole window and an engine failure read as a healthy zero wait.
The errors page cites `error_<fingerprint>` and the agent's tools cite the bare one, so
the same error could carry two watches — two wakes and two emails per recurrence — and the
prefixed spelling produced an unresolvable link in the wake.
The queue detail page presents a task queue with its `task/` prefix stripped, but
`TaskQueue.name` keeps it, so every watch the page offered on a task's own queue was
refused as a missing target.
Each test replays the migrations inside its own budget, which overruns vitest's
5s default on a loaded CI host and times out a different test each run.
A watch wake was the only thing that raised the dot and the highlight. An answer or a settled card that landed while the panel was closed now does the same — without a toast, which stays a wake's alone.
Most tools spend an environment JWT with a hardcoded scope list; the delegated token's cap only ceilings that exchange. Queues were missing from the list, so the live lookup 403'd and read as a queue that does not exist.
"Tell me when this run finishes", "ping me if that error comes back". A watch
checks on its own cadence, reports once, and stops within 24 hours. The user
confirms a pre-filled card, so nothing starts behind their back; the answer
lands in the chat, and by email if they asked for it.
Restores the feature this branch's base PR set aside, unchanged: the card and
chips, the wake banner and toast, the unread badge, the checks for runs, queues,
errors and health, the batch scheduler and its backstops, the alert channel and
its email, and the agent's schedule_watch tool with the prompt that governs it.
Pure move: the watch contracts and queries, the delivery-claim id, the chat
lock, markChatRead and the watch cancellation inside softDeleteChat, plus the
four contract types the watch schema annotates, re-narrowed here.
The fixture still set `facts.staleReason`, renamed to `untrustworthyReason` three commits
before the caveat started reading it, so the branch's only trust snapshot fell back to
"could not be verified" for a report whose reason is known.
flag() ignores a per-org override that fails the schema and falls through to the global
default, which is on. For an entitlement that is right; for this flag it means an org that
tried to turn judging off with a stringified boolean kept being judged. The eval policy now
refuses on an override it cannot parse, matching what the flag's own comment already claims.
reportQueryTables fell back to the union of every report's tables, so a key naming no
report was checked against all of them. everyResource is a conjunction and checkAuth
denies an empty one, so that happened to be strict rather than permissive - but only by
accident, and the docstring justified it with a claim about empty lists that checkAuth
contradicts. An unknown key now declares no tables and is refused outright.
An investigation card could vanish from a reply that contained step
separators: the winning revision was keyed by its position in the
untouched reply, while rendering walked the stripped parts. Both now
index the same stripped list.
A host-written `data-view` card counted towards winning a revision but
was never drawn as a card, so it could suppress the tool-rendered card
it competed with into nothing. Both carriers now render.
`apiGet` left `fetch` and `res.json()` unguarded, so a connection reset
threw out of the tool, and a transport failure that did land could read
as a definite 404. It now returns a transport failure the way the
sibling paths do.
Also: guard the card's Hypotheses section on length, as Evidence
already is, and move the reports auth resource out of the serializer so
rendering a report doesn't pull the route builder in.
The watch tables and the 0002 migration stay here, so the committed drizzle
snapshot keeps describing them. The four contract types the schema annotates
are widened here and re-narrowed where the watch code lands.
`collectDurableState` read `data-view` parts only, but `render_view` writes
every investigation card as a `tool-render_view` part, so compaction pinned
nothing and an open card could be summarised away.
It now resolves cards through `latestCards`, the resolver the panel and the
watch actions already use: highest revision per id wins whatever order the
renders arrive in, so a stale `in_progress` render landing after the settling
one no longer reopens a closed card. `latestCards` reads host-written
`data-view` blocks too, matching the panel.
`curateReport` still read `facts.staleReason`, renamed to `untrustworthyReason`
in dc3b50260 and split into telemetry_stale / telemetry_absent / flow_unmeasured.
The read had been undefined since, so the agent got "untrustworthy" with no why,
and the prompt still told it every such case was stale telemetry.
`facts` is `z.record(z.unknown())`, so nothing typechecked the key. The new test
goes presenter -> reports route JSON -> curateReport without naming a facts key
on the way in, and asserts curation carries every key the presenter emits.
An unmeasurable depth made the whole flow finding unassessable, so a
critical start latency measured off runs showed as crit in format=json
while the rendered report said 'Flow unknown ... nothing to do' and
dropped the row that earned it. The depth still buys no cause, no
attribution and no drain ETA, but the measured symptom is now reported.
A missing telemetry feed said the run aggregates were a point-in-time
snapshot; they are measured over the window either way, and what is
actually unknown is how current they are. An unmeasured queue depth
blamed throughput, which the report does measure, and contradicted its
own headline. Tests pin the claim rather than the wording.
The agent could not read anything on a preview or dev branch. Its environment name
is derived from the environment's type, and every branch shares its parent's type —
so the name identifies a family, not a row. The API's env routes are name-addressed,
so a bare "preview" or "dev" resolved to the parent, and the delegated token's
`environmentId` claim then correctly refused it. The guard was the detector, not the
defect: the exchange was never given enough identity to resolve the environment the
dashboard had selected.
Name and branch are now resolved together into one address, so no caller can take
the name without the branch, and both mint sites share the one type map instead of
keeping a copy each. The address travels to the JWT exchange and to the three
delegated-token reads that resolve by name (list_tasks, correlate_version, the repo
snapshot). The env-JWT reads address the environment by id and are untouched.
Second, and why nobody saw the first: the exchange reported the same "no
environment" for a genuine absence and for any failure, and cached the failure for
the whole turn. Following the queue live-read precedent, only a missing environment
is stated as one; anything else says the read didn't land and carries its status.
The helper checks no scopes and no capability context, so a read-only user-actor token
reached an alert-channel write and every admin route behind requireAdminApiRequest.
Actor-aware routes are unaffected: they authenticate the token through the route
builders, which enforce its claims.
flag() queried the FeatureFlag row before looking at the caller-supplied overrides, so a
per-org hit still paid a round-trip. Check the override first and only fall through to the
query when it fails the schema, which keeps today's resolution order intact.
A start that rejects dispatched no handover and sent no message, so the chat
row is taken back. A failed access-token mint is left alone: the session is
live by then and removing the chat would hide a running agent.
A token signed for a branch child listed nothing: the scope filter and the
base-environments-only filter could never hold together. Unscoped callers
still see base environments only.
The environment lookup, repo lookup and token mint now all run before the chat
row is created, so a 404 or a mint failure can't orphan a chat in the history.
Absent telemetry and an unmeasured flow were both labelled stale data, so every
snapshot-based report claimed staleness it could not have measured. Choose the
badge and caveat from the reason instead.
A fall's multiplier rounds to 0 or 1, so every drop rendered as "flat" — a metric
that collapsed from 100 to 5 read as unchanged. Measure the fall against the
baseline instead, and show a bare arrow when it collapsed to nothing.
The per-queue metrics route mapped ClickHouse rows straight to an array, so a
bucket with no sample shortened the trend and shifted every later point in time.
Fill a fixed-width grid the way the two sibling callers do.
Dropping the column was irreversible and blocked on a production row count; retiring it is not. The avatar host is an exact origin the app already knows, like the GitHub one.
The route served a deployment's git blob — commit message, author, branch,
PR title — with no ability check, while the deployments list serves the same
blob behind read on deployments. Apply that check here too.
assertUserActorScope returned early whenever the passed scope carried no
org, project or environment, and the route builder passes {} for any route
that declares no context — so the guard was a no-op there. api.v1.orgs's
action is such a route and has no authorization block either, letting a
read-only agent token create an organization.
Fail closed instead, with an explicit identityOnly opt-in for the two
contextless loaders that answer with the caller's own identity, and give
org creation the gate its siblings have.
The scope ceiling rewrite landed here and was reverted two PRs up, leaving the
stack asserting both directions. The exchange intersects requested scopes with the
token's cap again, a capless token passes through like a PAT, and the route keeps
only the environment claim check and the acting client.
The dashboard agent asks for a queue's live row — paused, depth, limit — through the environment JWT it exchanges for. The metrics route has accepted that JWT all along; the retrieve route answered 401, so the agent saw no queue at all.
Review of #4418: the cloud path builds the ability from the user's role, so a read-only delegated token could exchange it for a write JWT; and the finalisable set was the whole replayed transcript rather than what the turn produced.
A turn stores its messages before the model finishes, so the completed bodies arrived against ids that already existed and were skipped. Reopening a chat then replayed a tool call that never ends.
The system — contracts, storage, auth, the agent package and its webapp routes — lands first; the panel, the page-context marks and the entry points follow in their own PR.