-
Snapshot flatten trigger and the snapshot pool budget now measure the on-disk CHAIN cost (Σ layer bytes, via
docker history, cached per content-addressed image id) instead ofdocker image inspect .Size— the current filesystem view, which charges a superseded byte once however many copies overlay still holds in the interior layers. On server-b three live chains held ~65 GB for ~23 GB of content while reporting 6.6/9.0/7.6 GB, so neither the 12 GiB per-session budget nor the 200-layer depth ceiling could ever fire (zeroflattenedevents in 24 h), and the pool reclaimer loggedreclaimed_bytes: 0every tick against a budget it read as 28.6/60 GB used. A flatten now also fires onadded_chain > 2 × added_view— more than half of what the session added on top of its base is dead history — which is the one budget-less trigger short of the depth ceiling. That ratio is measured base-relative, like the projected-unique figure beside it: a flatten can only reclaim history THIS session added, never the shared base's own interior chain, which stays resident (shared) whatever the session does. A session that writes then deletes clamps its added view to zero, so the ratio's positivity guard is on the added chain: a fully dead added chain is the strongest case to flatten, not one exempt from the trigger. Measured over the whole chain instead, any base with the ordinaryapt-get install … && rm -rf /var/lib/apt/listsshape would force a reclaim-nothing flatten on every session's first snapshot, every idle. The flatten headroom gate still sizes on the view, since the export writes content, not history. -
A failed
docker historyprobe no longer mixes units. The backend now reports an image's view and its chain separately, withchain = Nonewhen history is unreadable, instead of passing the view off as the chain. Base-relative accounting (snapshot commit/flatten, GC pointer-heal, pool budget, per-account cap) subtracts the base only when BOTH chains were measured. Otherwise it charges the image's full best-known cost, failing safe for disk. Before, a child view minus a measured base chain clamped multi-GB sessions to 0 bytes, which silenced pool reclaim and thesnapshot_bytesnotice. The flatten trigger compares view against view when either chain is unmeasurable, so it can no longer invent dead history and flatten a session into a private copy of its base. -
The dead-history flatten no longer fires on a guess, and its break-even is the base's VIEW. A base that cannot be inspected (a transient daemon error or "No such image") used to read as a 0-byte base, so the base's own dead history was charged to the session and flattened it; now the trigger treats every base-relative figure as unknown — dead-history cannot fire, the budget sees only the writable layer, the depth ceiling still applies. A
docker historythat exits 0 with an unparseable body (N/A, an unknown unit, empty, fewer lines than layers) reportschain = Noneand is not cached, instead of caching the view as the chain; IEC units (MiB) parse, anything else fails closed. A flatten isexport | importof the current view: it copies the base's view, never its history, so it now fires when the reclaimable session-added history exceeds the base view, not the base chain. -
sessions.snapshot_bytesis now one quantity no matter which writer wrote it last. The snapshot commit/flatten path recorded the.Sizeview while the GC pointer-heal path recorded the on-disk chain cost, so the disk over-limit notice — which gates on the commit-path figure — stayed unreachable for exactly the superseded-history chains this work exists to catch. Both paths are chain-denominated and base-relative now. -
Operator triggers show up in
list_account_triggers; a non-finitebudget_usdis a 422 (#2525). The account-wide trigger read INNER JOINedsessions, so operator triggers (#2473, no owning session) never appeared and every liveness monitor built on it was blind to them. It now LEFT JOINs with the scheduler's own liveness predicate: session triggers on archived sessions are still excluded, and operator triggers are listed. Each row now carriesowner_kind(session|operator);owner_session_idisnullfor operator rows. Separately,budget_usdon trigger workflow actions (session and operator, create and replace) rejectsinf/nan(1e400parses toinf), and the 422 handler no longer crashes rendering a non-finiteinput, which had turned that 422 into a 500. -
Tool results carry a received time (#2282).
build_messagesnow renders every tool result with the same[received=…]envelope a user message gets, from the result event's owncreated_at(answer time, in the account's timezone). Before, a result sat right after its tool call with no time signal, so a call that stayed open for hours (anask_usercard, an approval) read as answered at ask time. String content gets a leading line. List content gets it on the first text part, or as a new leading text part. Blind-spot result injections get a[received=…]line under their[Tool result: …]header. Append-time token pricing now counts the envelope. -
toolCLI always writes parseable JSON to stdout on transport failure (#2227). A broker request that timed out used to print a Python traceback and nothing on stdout, so callers doingjson.loadssawExpecting value, and a lenient caller read the timeout as an empty result. Timeouts, an unreachable broker, a missing socket, non-JSON broker responses and non-2xx broker replies now write one JSON line to stdout, e.g.{"error": {"kind": "timeout", "timeout_s": 30.0, "path": "/builtins/http_request"}}, and exit non-zero (2 for transport failures, 1 for broker HTTP errors). The human-readable message goes to stderr. Successful calls are unchanged, and the 30 s timeout is unchanged too. -
Oversized request inputs are refused at the caller, never truncated (#2122).
agent()in a workflow now has a documented input limit: 1,000,000 characters (Unicode code points, the sameMAX_USER_MESSAGE_CHARSthe message endpoint enforces), measured on the serialized input. A string counts as-is. Any other value is measured afterjson.dumps, so JSON syntax and escaping count too. An input over the limit is refused before the child session exists. Theawaitraises a catchableAgentErrorwithkind="input_too_large", and its message gives the actual size and the limit. The other request writers check the same bound and return 413payload_too_largewhen it is exceeded: theinvokeAPI (checked before the servicer session is created) and a request to an existing session. The ~8k boundary in the issue was never a delivery cap. It is the obligations reminder's 8,192-character render budget. Since #2258, a reminder over that budget is marked[TASK ABRIDGED IN THIS REMINDER …]and points back to the full original request instead of telling the agent to refuse. -
Agents can address a role instead of a cached agent id (#1940). New
resolve_role(role)model tool (grantable as{"type": "resolve_role"}) returns the live agent id holding a role in the caller's account. An agent holds a role when itsnameor itsmetadata.roleequals it. Archived agents never match, so after a re-spawn (archive + re-create) the role resolves to the new id. Resolution fails loudly:404 no live binding for role 'X'when nothing holds it, and409listing the candidate ids when more than one live agent does. It never returns an empty result. This is the v0 cut from the addressing design, built on the existing agents table. The durablerole_bindingstable,list_rolesandwake_roleare still to come. -
Trigger writes warn when a monitor can disable itself (#2402). A standing (
cron/run_completion/external_event)sandbox_commandwhose command contains an explicitexit Nwhose bash status (N mod 256; signed or quoted literals included) is non-zero now gets a warning in the create/update response'swarnings. Each non-zero exit is a failed fire, and 5 in a row auto-disable the trigger. That is how all threecompany-alive-heartbeat*emitters went dark: each ranexit 1on the ping-failed path. The warning asks for the finding to be reported another way and anexit 0on purpose. It only warns and never rejects the write. It is a text check, so it cannot see a command whose last statement simply fails. Thesandbox_commandtool schema description now states the same rule. -
lane_activate sends a complete trigger update again, and lanes can be armed capped (#2463). The trigger PUT omitted
version,max_outstanding_runsandbudget_usd, whichWorkflowActionReplacenow requires, so every trigger update 422'd. The script now emits every key, with explicitnullfor any the lock does not declare (same behaviour as before). A lock'scron_trigger.actionmay now declaremax_outstanding_runsandbudget_usd; a change to either counts as trigger drift. The lanes test fake now validates trigger bodies against the realTriggerCreate/TriggerUpdatemodels. An invalid cap (max_outstanding_runsnot null / int >= 1,budget_usdnot null / finite > 0) is refused byLaneLock.from_dict(ValueError, usingWorkflowAction's own bounds) and bylane_activateat read-lock, before any object is created or updated. -
Run budgets now count the whole subtree, bind a parked run, and can be set per trigger (#2446, parts a-c). (a) New
WorkflowAction.budget_usd(> 0, defaultnull= no budget, as before), passed to each launched run as itsbudget_usd. (b)budget_usdused to count only the run's direct child sessions plus its owncall_llmmeter, so a grandchild (a child's owncall_agent) or a sub-run spent outside it. The gate, thebudget()builtin and the refusal message now all read the creation-subtree cost from the shared accounting rollup. (c) The gate ran only when a newagent()/call_llmopened, so a run parked behind a burning child was never stopped. The needs-step sweep now also wakes a suspended, budgeted run with an openagent()call once its subtree spend reaches the budget. The step force-resolves each open call through the #2440 exit path:AgentError(kind="timeout", bound="budget"), the child gets a cancel marker, and the script can catch it. Work already journaled is kept. BREAKING for update callers:WorkflowActionReplacenow REQUIRESbudget_usd(send explicitnull), the same asmax_outstanding_runs. -
Workflow-action triggers can cap their own outstanding runs (#2446, part d). New opt-in
WorkflowAction.max_outstanding_runs(int >= 1, defaultnull= uncapped, same as before). A trigger'srunning_sincelease clears whencreate_runreturns, not when the launched run finishes. So a slow or suspended run never stopped the next fire from stacking a second run on top of it. With the cap set, a fire where this trigger already has that manypending/running/suspendedruns launches nothing and recordsskipped(reasonoutstanding_runs_cap: ...). That is back-pressure, not an error:consecutive_failuresdoes not move, so a healthy capped lane is never auto-disabled. Runs now record their launching trigger (wf_runs.trigger_id, migration 0182, with a partial active-runs index). The count happens insidecreate_rununder the same per-account advisory lock as the launcher and account fan-out caps, so concurrent fires of one trigger cannot both pass. The field only restricts: it never lifts those caps. It is available on the HTTP API and on thetrigger_create/trigger_updatetools. BREAKING for update callers:WorkflowActionReplacenow REQUIRESmax_outstanding_runs(Replace semantics; send explicitnullfor uncapped). A PUT of a workflow action without the key now fails with 422. Affected callers: the CLIsessions triggers update,trigger_updatetool calls, and SDK builds generated before this change. Add"max_outstanding_runs": nullto keep the old uncapped behavior. -
Model calls now reserve the model's own output ceiling instead of inheriting a provider default that silently truncates replies (#2451). aios never set
max_tokens, and omitting it does NOT mean "unlimited" — on Anthropic-shaped routes it means 4096. Extended-thinking tokens are drawn from that same budget, so a hard turn could spend all 4096 reasoning and return EMPTY assistant content withfinish_reason: "length": full cost billed, recorded as a clean turn, no error raised — and the failure got more likely the harder the task.harness/completion.pynow defaultsmax_tokensfrom the model'smax_output_tokens, scoped to Anthropic-shaped routes (OpenAI's no-max_tokensbehaviour is already "as much as fits", and reserving there regresses into OpenAI context-window 400s and OpenRouter credit-affordance 402s). An agent-suppliedmax_tokens/max_completion_tokensstill wins verbatim, and an unknown ceiling omits the key rather than sendingmax_tokens: None. One resolver (completion.resolve_output_cap) decides THE cap for every consumer: the first positive-int value inmax_output_tokens > max_tokens > max_completion_tokensorder, else the model-ceiling default. The request then carries exactly that one cap key —max_tokenson Anthropic-shaped routes (LiteLLM passesmax_output_tokensthrough unrecognised and fills its ownmax_tokens, so a second spelling is a second, competing cap), the winning spelling verbatim elsewhere (notablyopenai/responses/*). Competing or invalid spellings are dropped and logged (explicit_output_cap_discarded), and context windowing reserves the same value the wire carries. -
Vision capability now treats a missing LiteLLM catalog entry or absent
supports_visionfield as unknown and lets image consumers attempt safe inline delivery by default. Explicit overrides and catalog booleans remain authoritative; image size, decoded-format, and resize limits are unchanged. -
Fix the #2294 schema-diet production incident: the dieted opaque arrays rendered as bare
{"type": "array"}with noitems, and litellm'stoken_counter→_format_typedereferencesprops['items']unconditionally, soprelude_overhead_localraisedKeyError: 'items'on every step of every workflow-capable agent (the fleet was rolled back). Opaque arrays now render as{"type": "array", "items": {}}— also required for OpenAI provider validity.sanitize_mcp_schemacloses the same defect class for untrusted third-party MCP schemas (missing/tuple-form/booleanitems, non-dict property values), and a registry-wide regression fence runs the realtoken_counterover every registered tool's rendered schema. -
get_workflow_script_contractnow returns the authoring contract as a plain-textToolResultinstead of a{"contract": …}dict, so the ~4KB multi-line manual reaches the model as real prose rather than a JSON-escaped single line (#2294, per the #2291 convention). -
Restore the context window's history floor:
window_minagain bounds RETAINED HISTORY only, so the per-request prelude (system prompt + tool schemas + reserves) is subtracted fromwindow_maxalone. Subtracting it from both bounds let a fat tool prelude satisfy the floor by itself, driving the effective floor to 0 so every snap emptied the window down to a single event — the agent then saw only the harness's own "history has scrolled out of view, search first" notice and looped onsearch_events. A floor the band cannot afford is now clamped to 75% of the events budget (keeping the snap chunk usable) and reported on theread_window_endspan plus awindow.floor_clampedwarning, rather than silently zeroed (#2289). -
search_eventsandmemory_searchnow return their formatted table as plain multi-line text (ToolResult) instead of a{"result": …}JSON envelope, so an inline or spilled result stays line-oriented forgrep/sed/wc -l/read(#2291). -
Cut the model-facing
create_workflow/update_workflow/call_workflowtool schemas from ~69KB to ~5.8KB combined by moving the script-authoring contract behind a new on-demandget_workflow_script_contractbuiltin and rendering the declared tool/MCP/HTTP surface as opaque arrays; pydantic validation and the HTTP/SDK schemas are unchanged (#2294). -
Distinguish a dead OAuth refresh token (RFC 6749
invalid_grant— revoked, expired, or otherwise unrecoverable) from a generic/transientOAuthRefreshErrorvia a newOAuthReauthRequiredErrorsubclass (error_type: "oauth_reauth_required"), so a caller can branch on "this connection needs to be reconnected" instead of retrying. Severity/eviction wiring (status_code 502, non-evicting on the MCP dispatch path, evicting via thehttp_requestbuilt-in's oauth2_refresh path) is unchanged (#192). -
Extend the #1975 diagnostic harness with timeout-scoped slow HTTP and streaming call graphs inside open transactions, a pre-saturated 16-slot pool, 13 queued waiters, and per-storm client/server recovery checks.
-
Rebuild the asyncpg cancellation investigation harness with synchronized acquire, post-query/pre-delivery, and release phase instrumentation, an asserted phase census, incident-shaped concurrency, and pool/server leak checks after every randomized storm (#1975).
-
Close PR #1979 adversarial-review findings: strict called-object pooled-connection linting and verified issue pragmas, cross-process OAuth refresh arbitration/race adoption with bounded local locks, and advisory-lock-safe threaded workspace deletion without holding a pooled connection.
-
Make production worker watchdog telemetry fail-open with reconnect/backoff and bounded query, stack, log, and forensic-file capture; correct workflow journal payloads and proxy task attribution; align dead-man session/workflow counters.
-
Make durable sandbox tarballs canonical across Docker cache pruning, add CAS publication, separate filesystem GC, and enforce persistent-store disk preflight and capacity pressure.