Added
- The workspace session list can report and filter by scope membership.
?include_scopes=truegives each session ascopeslist of the scopes it is in ([]for none), fetched in one query per page; without it the response is unchanged. Bodyscopelists only that scope's sessions (404 if the scope doesn't exist) andin_no_scope: truelists only sessions in no scope; both combine withfiltersand pagination, andtotalcounts the filtered result. Clients no longer need to list every scope's members to show which scopes a session is in (#1477) - The scope list can filter by name and count each scope's sessions. Body
filterstakes{"id": {"icontains": "sup"}}and the other list operators, matching the unprefixed scope name (never the internalscope.prefix), pluscreated_atandmetadata.?include_session_count=truegives each scope asession_countof its active member sessions, counted the same wayscopes/{id}/sessions/listlists them, in one query per page; without it the response is unchanged (#1478) POST /scopes/{scope_id}/sessions/removeremoves up to 100 sessions from a scope in one request, mirroring bulk add. The batch is all-or-nothing: if any named session doesn't exist it returns 404 and no membership changes. Sessions that aren't members are ignored, and only sessions whose membership actually ended get ascope_removalreconciliation task, so a stray ID can't clear the scope's dream notes. Memberships are locked in session order, so it can't deadlock against a concurrent bulk add of the same sessions (#1480)DIALECTIC_MAX_QUERY_LENGTH([dialectic] MAX_QUERY_LENGTH) sets the longest peer chatqueryaccepted, in characters, up to 200,000. It defaults to 10,000, the previous fixed limit, so nothing changes unless you raise it; workspace chat keeps its fixed 10,000-character limit. The dialectic's prefetch search now truncates a query over the embedding token limit instead of erroring and silently losing every prefetched conclusion (#1443)- Explicit conclusions cite the messages they came from. The deriver emits
source_indicesper conclusion, resolved against the batch and stored as edges in a newconclusion_source_messagestable (DDL-only migrationc4d8e2f6a1b3, no backfill: citations are per-derivation, so rows written earlier surface null). Conclusions responses carrysource_message_ids(public message ids, citation order; null for derived levels), andsource_message_idsjoins the documents filter allowlist with the samecontains/in/ bare-list grammar assource_ids, soPOST /conclusions/listanswers "which conclusions cite this message?". Cited messages may belong to any peer in the batch, not only the observed one. When a conclusion is derived again, exact or semantic deduplication merges the new citations into the kept conclusion, and copies made when a session joins a scope keep theirs. The batch-levelmessage_idsmetadata is unchanged (#935)
Changed
- Updating a workspace's
configurationreplaces the stored object instead of merging into it, so{}clears explicit settings and the workspace falls back to system defaults. Before, keys left out of the update were silently kept. Omittingconfiguration, sending null, or updating only metadata leaves it as it is; peer and session configuration updates still merge. To change one setting, send the full configuration (#1458) - Peer chat (the dialectic) answers as a single turn. Answers no longer end with an offer or a question the caller can't reply to ("If you want, I can…", "Which statement is correct?"); contradictions in memory are still reported. A query where a harness wraps the end user's message is no longer answered as if Honcho were the assistant in that conversation. Workspace chat is unchanged (#1451)
- Streamed peer and workspace chat return the answer the dialectic's tool loop already has, instead of discarding it and making a second, streamed LLM call over the same context. A streamed answer now usually arrives as a single chunk when the loop finishes rather than token by token; only an answer synthesized at the tool-call limit still streams as it is generated. Streaming saves that extra call, returns the same answer as non-streaming, and no longer comes back empty when the extra call returned only thinking (#1443)
- Groundwork for deleting workspaces, peers, sessions and messages. Migration
d8c4f6a2b901adds a nullabledeleted_atcolumn to each (no default, backfill or index), and workspace, peer, session and scope reads skip rows that are tombstoned or whose parent is. Nothing setsdeleted_atyet, so behavior is unchanged (#1429, #1450) - Langfuse Python SDK upgraded from 4.6.1 to 5.0.2, which sends traces on the Langfuse v4 ingestion path ahead of Langfuse Cloud's v4 cutover on 2026-11-16. Self-hosted Langfuse must be v4.59.0 or later; a v3 server no longer receives Honcho's traces. Observation metadata values are now JSON-encoded on the span (
"honcho"rather thanhoncho), so saved Langfuse metadata filters written against the old shape may need updating. Trace names, user ids and the trace tree are unchanged (#1452) - Summaries the provider withholds for their content (finish reason
content_filter,safety, …) are no longer reported to Sentry as errors, since nothing on Honcho's side can fix them. They are counted in thesummary_rejectionsmetric withreason="content_filtered"instead (#1457)
Fixed
- Removing a session from a scope and adding it back no longer loses the scope's memory of that session. The removal's cleanup task could run after the re-add (another deriver replica, a retry, a backlog) and soft-delete the session's scope conclusions, including ones derived after the re-add, plus the scope's dream notes and the session's backfill status. The cleanup now locks the membership and skips if the session is a member again. Session and workspace deletion now delete memberships before conclusions, in the order the cleanup takes them, so the two can't deadlock (#1479)
- Chat, the deriver, summaries and dreams no longer fail when an OpenAI-compatible model returns a usage total but omits the prompt and completion counts. The answer is kept and the missing counts are recorded as zero, instead of the request erroring and being sent again (#1460)
- Claude 5.x models work as configured models instead of failing over to the fallback. The Anthropic backend sends adaptive thinking with an
effortinstead of a thinking budget on Claude 4.6+ and 5.x, relaxes a forcedtool_choicetoautoon models that reject it (Opus and Sonnet 5.5), skips the assistant prefill where it's rejected, and stops replaying earlier thinking blocks once the tool loop has changed the tools or history (#1442) - Requests the API can't store or parse return 422 instead of 500: a path ID that fails the name pattern on a get-or-create route, a number outside Postgres's range, NUL or a lone surrogate in a JSON field, a
pageabove 2^31−1, and a malformed or empty PDF upload. NUL is stripped from workspace, peer and messageconfiguration, as it already was from metadata. A numeric metadata filter ({"gte": 0}) no longer fails when the key holds mixed value types; strings that aren't safely numeric drop out of ordered comparisons. Two requests racing to create the same workspace no longer turn the loser into a 409 (#1445) - Peer cards written by the agents'
update_peer_cardtool and session summaries no longer fail to save when the model output contains a NUL character, which Postgres rejected along with the whole write. NUL is stripped first, and a summary that is only NUL is retried like an empty one (#1455) - When a message is both a short and a long summary boundary (every long boundary under the default 20/60 cadence), both summaries are now kept. The two saves run concurrently, and each wrote back the whole summaries map it had read, possibly from cache, so the later one could drop the other or revert it to its previous version. Each save now merges only its own entry, on the server (#1493)
- A summary that fails to fetch or save at such a boundary is no longer dropped without a trace. Only a refused (empty) summary reached the queue; any other failure let the queue item complete with no log line, and that window of messages never reached a later summary. Every failure is now logged with its summary type and raised to the queue, preferring a transient error so the item is retried. On the retry, a summary that was already saved is skipped (#1493)
- A deriver queue item whose error message contains a NUL or a lone surrogate is now marked errored. Postgres rejected that error text, so the item was never marked and the worker ran it again straight away, LLM call included, over and over. Those characters are now stored escaped (
\x00,\ud83d). If marking an item errored fails for any other reason, the worker now releases the work unit and backs off instead of rerunning the item at once (#1492)
Security
- pypdf moves from 6.18.1 to 6.21.2 for three high-severity advisories where a crafted PDF makes parsing run for a long time (GHSA-gmh7-3g9r-mjh5, GHSA-jr96-gvgh-j3r4, GHSA-6c7f-9265-g8fw).
POST .../messages/uploadpasses any authenticated caller's PDF to the parser. Parsing also moves off the event loop, so a slow PDF holds up only its own upload instead of every request on that API worker (#1476)