v0.147.0
The telemetry-truth release. Every cost, success rate, and latency percentile protoAgent reported was wrong in at least one way — Claude prices had drifted up to 3x, cached tokens were billed at full price against a 73% cache-hit ratio, and turns from /v1, /api/chat and the plugin HOST.invoke() seam were never recorded at all. This release fixes the measurement layer end to end, and adds fleet-shared delegates (ADR 0105), scheduled-task delivery back to the originating chat, and expiring schedules.
Added
-
Fleet-shared delegates — register a coder once on the hub, it's on every member's bench (#2987, ADR 0105). A delegate saved with Share with fleet (
scope: host) lives in the box'shost-config.yaml(secrets in an owner-onlyhost-secrets.yaml) and every agent on the machine reads it live — no per-member copy to drift when a key rotates. Members see shared rows read-only and may shadow one with their own; only the hub writes the shared layer. A Project Manager created with a shared coder picked needs no copy at all. -
Scheduled-task results come back to the chat that asked for them (#2990). A schedule created in a chat now stamps that session as the job's
origin_session— distinct fromcontext_id(which still controls where the fire runs: a cron in Activity, await/resume back in its thread). When the fire completes, the server pushes ascheduler.completedevent carrying a result summary (job id, fire time, truncated turn text, link to the full Activity turn) to that chat, where the console renders it as a ScheduledReportCard — a calendar/clock card (distinct from the background-report chip). A recurring schedule's first fire renders the full card; later fires collapse to a compact, dismissable ScheduledChip ("Scheduled task {id} ran — …") so an hourly check can't spam the thread. A chat idle for >24h is skipped (the result stays in Activity — no ghost sessions), and a schedule created outside a chat carries no origin and delivers nothing (backward compatible). -
resolve_frictiondismisses fixed entries from the friction backlog (#2990). Resolving stamps every ledger record whose summary contains the given substring withresolved_atin place — the JSONL ledger stays append-only, so the audit trail survives.friction_review,grouped_entriesandGET /api/frictionnow hide resolved entries by default;include_resolved=True/?resolved=trueshows them, marked[resolved]in the review. -
Scheduled tasks auto-expire (#2992).
schedule_tasknow takes optionalttl(a
duration like7d/24h/2wor ISO-8601P7D) andmax_fires; the scheduler
bumps a per-jobfire_countafter each successful fire and auto-cancels the job once
it outlives its TTL (from creation) or hits its firing cap, logging the expiry.
Recurring cron schedules default to a 7-day TTL unless overridden, so a forgotten
schedule no longer runs indefinitely; one-shots are unaffected.list_schedules
shows the expiry state (ttl=/max_fires=/fires=) per job.
Changed
-
The console e2e harness covers the archetype picker's hard gate and choose-time capability contract on both pickers (#2986). The mock catalog gained a Project-Manager-shaped archetype (
tier: advanced,requires_tools, a preview with requiredpath+delegateconfig_inputs) and an a2a delegate, and the fixtures' bundle ids follow the<name>-archetypeconvention. New specs prove Create/Finish stay disabled while a required answer is blank, the coder dropdown lists only acp delegates, Enter honours the gate, the collapsed-state hint shows, and the answers ride the request even after collapsing Configure — in the New-agent panel and, for the first time, the Setup Wizard. -
The wizard's capability contract is pinned on the wire (#2998). The setup-wizard e2e now asserts
requires_toolsridesPOST /api/config/setup(#2989's host-path record), closing the #2986 follow-up.
Fixed
-
Linux desktop: the server sees your login-shell PATH —
gh,br, and per-user ACP adapters resolve (#2988). The sidecar PATH augmentation was macOS-only; it now applies on every unix desktop and adds~/.cargo/bin,~/.local/binand linuxbrew. launchd autostart jobs get the same per-user dirs. -
A wizard-installed archetype now gets the capability-contract banner too (#2989).
POST /api/fleetrecorded an archetype'srequires_toolson the member'sworkspace.yaml, but the Setup Wizard installs onto the host — which has no such record — so a host Project Manager whosegithub_create_issuenever bound stayed silent.POST /api/config/setupnow acceptsrequires_tools(the wizard sends the picked archetype's) and records it in the host'sconfig/archetype-contract.yaml(gitignored runtime state, cleared by re-running setup); the operator-status check falls back to that record when there's no workspace one. -
Chat answers stream smoothly word-by-word on every provider (#2993). Claude OAuth models rendered answer text in visible ~8-9 word blocks: the Anthropic SDK (via langchain-anthropic) yields multi-word chunks upstream of the server's executor, so its (correct, unchanged)
_FLUSH_CHARS=24flush logic had nothing finer to send. The console now paces streamed answer deltas through a client-side reveal queue — word-sized chunks dripped on a requestAnimationFrame loop at ~35ms/word, with elapsed-time catch-up and a backlog ceiling so the render never lags the wire by more than ~2s. Tool/reasoning/component frames, the terminal REPLACE frame, Stop, and the stream-end paths all flush the queue instantly, so part ordering and the final answer are never delayed. A[stream-delta]DEBUG log inserver/chat.pyrecords each model delta's size/timing for future provider profiling. -
Subagent LLM calls now share the lead agent's model failover chain (#2995).
routing.fallback_modelswiredObservableModelFallbackMiddlewareonto the lead agent only, so a subagent whose primary model errored failed the wholetask()/task_batch()delegation instead of retrying the next model. The subagent middleware stack now mirrors the lead: whenrouting.fallback_modelsis non-empty it appends the same observable failover middleware (built from the same model list, before the native-OAuth wire-shape transforms), and stays byte-for-byte unchanged when the list is empty. -
Clearing a conversation now asks first (#2996). The ⌘K/⌃K
chat.clearkeybinding and the/clearslash command used to purge the session and wipe every message the instant they fired — no undo, no prompt. Both now open the same confirm dialog tab-close uses (Clear this conversation? This cannot be undone.) with an opt-in "Harvest into the knowledge base first" checkbox; the destructivedeleteChatSession+ history wipe runs only after you confirm, and dismissing the dialog leaves the conversation untouched. -
Turns from
/v1,/api/chat, and plugin surfaces are no longer invisible to
telemetry (#3000). Only A2A turns were ever recorded. Everything running
through the non-streaming driver — the OpenAI-compatible
/v1/chat/completions(how the agent registers into a LiteLLM gateway or
OpenWebUI, and how the evals harness drives it), the/api/chatfallback, and
the ADR 0018 pluginHOST.invoke()seam behind Discord peer channels and every
relaying plugin view — produced no store row, no Prometheus sample, and no cost
at all. It computed real token usage and threw it away after filling the OpenAI
response body, so every cost total, success rate, and latency percentile
silently described a subset of real traffic with no sign that it was a subset.
Both drivers now record through one shared writer
(server/turn_telemetry.py::record_turn), so a new turn surface either routes
through it or is visibly unmeasured. Each row is tagged with the surface that
spent the tokens (v1:,api-chat:,plugin:), and cost is summed per model
so a turn that routed across a pinned subagent bills each at its own rate. -
A HITL turn's pre-approval spend no longer disappears when it resumes (#3001).
Both legs of a park/resume carry the same A2A task id, and the telemetry store
upserted ontask_id— so the resumed leg overwrote the parked one. #2943 fired
the terminal hook for both legs, but only one row survived: a turn that paused
for approval reported just what happened after the human answered. The tool
calls were the worst of it, since an approval-gated turn does all its tool work
before it asks — the surviving row showedtool_calls: 0. The store now keeps
one row per turn leg, identified by a new surrogaterow_id, withtask_id
an ordinary indexed column that several rows may share. Existing stores are
rebuilt in place on open, preserving history. The console keys its telemetry
rows onrow_id(two legs sharing atask_idwould otherwise collide as
duplicate React keys), and the #2943 regression test now asserts the durable
rows rather than stopping at the hook that writes them. -
Cost telemetry now prices every current Claude model correctly (#3002).
The rate table had drifted badly: Opus 4.6/4.8 were still billed at the pre-4.6
$15/$75 per-Mtok rate (3x over), Haiku 4.5 at $0.25/$1.25 against a real
$1/$5 (a quarter), and the Claude 5 family had no entry at all, so
claude-opus-5andclaude-fable-5silently fell through to the mid-tier
defaultrate and were undercounted. Everycost_usdin the telemetry store,
the console dashboard, the CSV export, the A2A cost-v1 extension, and the
*_llm_cost_usd_totalPrometheus counter derives from this table, so all of
them were wrong for the models most people actually run. Rates are now written
in the units vendors publish (USD per million tokens) so an entry can be
checked against a price list at a glance, the current set is pinned by test,
and a model that resolves todefaultlogs once instead of being billed at the
wrong rate in silence — the fallback direction that hid Opus 5 and Fable 5. -
Cached prompt tokens are no longer billed at full price (#3003).
cost_usd
charged the whole ofinput_tokensat the full input rate — but that count
already includes the tokens served from the prompt cache, which really cost
about a tenth as much. On live stores the dominant model was running a 73%
cache-hit ratio, making this the largest single error in the cost column, and
it pointed the wrong way from the "cache savings" figure shown right next to
it. Cost is now split by component: full rate for uncached prompt tokens, ×0.1
for cache reads, ×1.25 for cache writes. The deferral note inpricing.py
("until the gateway's cache-token semantics are validated end-to-end") is
retired — gateways disagree at the raw provider layer, but LangChain reconciles
them before the shape protoAgent reads, so no provider branch is needed.
The storedinput_tokenscolumn is now cache-EXCLUSIVE, so it, the cache-read
count, and the cache-write count are disjoint and sum to the turn's real prompt
size;cache_hit_ratiodivides by that sum. Rows recorded before this change
keep the old meaning and are not rewritten, so a hit ratio over older history
reads slightly low. Thecost-v1A2A extension is unchanged. -
A turn waiting on a human no longer counts as a failed turn (#3004).
#2943wrotesuccess = NULLfor a parked HITL leg so it would stay out of
the success numerator — but the denominator was still every recorded row, so
the parked leg was counted anyway. Any agent using approvals showed a
permanently depressed success rate, and the metric most likely to be watched
for "is this agent healthy" instead tracked how often it politely asked.
success_rateis now over turns that actually resolved; the summary also
carries aresolvedcount alongsideturns, so the gap (legs awaiting a
human) is visible rather than merely excluded. -
Latency percentiles are percentiles again (#3005).
_percentilesat one
rank above nearest-rank — but only sometimes, because Python's banker's
rounding flipped the error with the parity of the half-value. At a 100-turn
sample that made p50 correct and p99 return the maximum, so the column an
operator reads to decide whether a tool is slow was really "the single worst
turn in the window". Affects the summary cards, the per-model breakdown
(#2678), the per-tool breakdown (#2697— which is also sorted by p95, so
the "slowest tools" ordering shifted), and the context-fill series (#2773).
Reported percentiles will move by one rank; they are now correct.
Removed
- Dropped the ACP usage frame's unconsumed
context_used_tokens/
context_window_tokensfields (#3006). They were built on every ACP turn and
documented as being there "so the console can render a context indicator" — but
nothing consumed them: the executor's usage handler reads a fixed key set and
drops the rest, and no console surface was ever built. The unit test asserted
the dict the producer had just constructed, so it stayed green while the fields
went nowhere. Removed rather than wired up, since ACP runtime mode is deprecated
(#2548) — a comment describing an indicator that doesn't exist is worse than
no comment. The runtime'slast_usage()accessor is unchanged and still reads
live client state for any caller that wants to sample it.
Full Changelog: v0.146.0...v0.147.0