Added
- Workspace, peer and session responses carry
uid, the immutable identifier Honcho assigns (the row's internal id).idis unchanged and stays the client-chosen name, so clients can start storinguidas a reference that won't move if names become renameable. Every route that returns these objects includes it: get-or-create, list, update, clone, session peers, peer sessions and scope sessions. Additive, no migration (#1394) - Named-scope API keys: a key bound to one named scope, for recall and for message ingestion (including creating new member sessions). Recall automatically uses the key's projection, session access requires active scope membership, and a restricted key cannot enroll unrelated sessions or use global resource routes. New keys carry a dedicated JWT audience, so an older server rejects them instead of ignoring the scope claim and granting workspace access. On self-hosted instances with authentication enabled, mint one with
scripts/generate_jwt.py; the scopes documentation lists the supported routes and limits. Managed (Groudon) keys need separate support, and the high-level Python SDK and CLI helpers still attempt workspace provisioning, so restricted callers use REST or the SDKs' HTTP transports (#1341) - Dreamer working memory, all opt-in and off by default so today's behaviour is unchanged: the dreamer and the dialectic show the agent its remaining tool budget (
DREAM_BUDGET_ENABLED/DIALECTIC_BUDGET_ENABLED) and a brief describing how memory is laid out, with conclusion counts by level and, for the dreamer, the conclusions created since the last dream (DREAM_BRIEF_ENABLED/DIALECTIC_BRIEF_ENABLED). The dreamer also reserves a write window at the end of each pass so planned writes are not lost when the iteration limit hits, and keeps capped working notes per representation from one dream to the next (DREAM_NOTES_MODE:off,shadoworon). Deduction and induction get their own iteration limits (DREAM_DEDUCTION_MAX_TOOL_ITERATIONS/DREAM_INDUCTION_MAX_TOOL_ITERATIONS);DREAM_MAX_TOOL_ITERATIONSnow applies to card refresh only. Telemetry gainshit_iteration_limit,write_window_usedand thenotes_*/new_conclusions_*fields (#1378) - Telemetry emits
workspace.created,peer.created,session.createdandscope.createdCloudEvents, once per row actually inserted, including resources created implicitly (posting messages into a new workspace, a workspace created throughPOST /peers). Repeat requests emit nothing. Still gated byTELEMETRY_ENABLED(defaultfalse), so self-hosted deployments without telemetry are unaffected (#1360)
Changed
- This release includes a migration. It adds a
deletion_generationcounter (INTEGER NOT NULL DEFAULT 0, a catalog-only change) to workspaces, sessions and collections, and five partial indexes over unprocessed queue rows so pending work can be found by the peer, session or scope peer it touches. Nothing reads the counter or the indexes yet; they prepare the deletion lifecycle work, and the previous release's code runs fine against the new schema. The indexes are builtCONCURRENTLY, so writes continue, but a build waits for open transactions that touchedqueueand for any older snapshot in the database, and after 60 minutes the migration fails and leaves an invalid index, which the next run drops and rebuilds. One index (ix_queue_pending_observers) needs thebtree_ginextension; where it cannot be created, the migration skips that index and logs the statement to create it later. On large or busy databases, build the indexes ahead of the upgrade with the statements in the troubleshooting guide, and the migration then has nothing to do (#1409) - Startup warns when an LLM API key is set for a transport no workload uses — across primary models, fallbacks, every dialectic level and embeddings — as provider migrations can leave populated credentials behind silently. The warning names only the setting, never the value, and does not block startup (#1344)
Fixed
provider_params.thinking_tool_choice_conflictnow applies on the OpenAI transport, where Anthropic models behind an OpenAI-compatible proxy used to ignore it. With thinking requested, it covers structured-output requests (the proxy turns the schema into a forced tool call) and forced tool choices.override_thinkingdrops thinking for the call;override_toolrelaxes the tool choice toautoand requests the schema asjson_object;throwraises. Unset leaves requests unchanged, but a config that already sets the key on the OpenAI transport now has it applied, including against OpenAI's own reasoning models, so set it only where the model is Anthropic. An unknown value now fails config load instead of failing requests with a 422 at runtime. Previously a proxy could leave the schema tool optional when thinking was on, so chats with aresponse_formatcould come back as prose and fail with a 500 (#1402)- The deriver accepts
PromptRepresentation.explicitas a bare string or a list of strings from OpenAI-compatible structured-output backends, coercing each non-blank string into an observation instead of failing validation and dropping the whole batch to the empty fallback. Blank strings are dropped; genuinely malformed shapes still raise (#538) - The deriver fails loudly on malformed or degraded structured output instead of silently discarding the batch as an honest empty: bad payloads raise inside the retried LLM call, so they are retried, reach the fallback model, and error the queue item if every attempt fails. A clean-stop
{"explicit": []}is still accepted. The summarizer gets the same treatment for blank responses cut off or filtered. Failure telemetry carries only the failure class, model and a payload digest, never the payload (#1373) - A chat request whose
response_formatanswer still fails its schema after retries returns 422 (The model's answer did not match the requested response_format) instead of an opaque 500 that the SDKs retry and Sentry drops (#1379) - Tool-loop answers that miss their
response_model— a plain-string refusal, orjson_objectmode's empty answer — are checked inside the retried call and raise, so they use the configured retries and fallback model before the request fails, instead of returning as a success and only then failing validation (#1393) - Dialectic chat no longer overflows the prompt on oversized tool output: message tools scope sessions in SQL instead of binding every session name as a parameter (which past 65,535 sessions hit the PostgreSQL parameter cap), tool error text is capped at
LLM.MAX_TOOL_OUTPUT_CHARSinstead of returning the expanded SQL statement to the model, and prompt truncation caps a single tool result at half the budget and drops middle units before the opening query (#1390) - Request bodies containing a lone surrogate escape (the half of a split emoji, or text decoded with the wrong codec) return the usual 422 validation body instead of a 500, so clients lose the single bad field's error rather than the whole batch (#1387)