v3.0.11 Release Candidate (#841)

* chore(docs): Release Candidate Changelog and Version Updates

* chore: fix basedpyright error
This commit is contained in:
Vineeth Voruganti 2026-06-24 12:44:13 -04:00 committed by GitHub
parent 810e25c487
commit 60a15e664d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 177 additions and 51 deletions

View File

@ -5,20 +5,30 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased]
## [3.0.11] - 2026-06-24
### Added
- `DERIVER_REPRESENTATION_BATCH_MAX_AGE_SECONDS` (default 1800s) lets sub-threshold representation work units flush once their oldest unprocessed queue item ages out. Set it to `0` to keep the legacy behavior where sub-threshold tails wait indefinitely unless `DERIVER_FLUSH_ENABLED=true`.
- `api_request_duration_seconds` Prometheus histogram tracking per-route request latency, labeled by method and endpoint (#837)
- LLM `provider_params` passthroughs (`extra_body` / `extra_headers` / `extra_query`) are now forwarded to the underlying provider transport across all backends, with shape validation that rejects non-mapping values (#821)
- `structured_output_mode` model-config option to use `json_object` mode for OpenAI-compatible providers that lack native Structured Outputs support (used by the deriver) (#820)
- OpenRouter app-attribution headers (`HTTP-Referer` / `X-Openrouter-Title`) are now sent on OpenAI-compatible clients when the configured base URL is OpenRouter, so requests are attributed to "Honcho" in OpenRouter's dashboard (#805)
- Langfuse traces are now tagged with user and session IDs for easier trace filtering (#814)
- `DERIVER_REPRESENTATION_BATCH_MAX_AGE_SECONDS` (default 1800s) lets sub-threshold representation work units flush once their oldest unprocessed queue item ages out. Set it to `0` to keep the legacy behavior where sub-threshold tails wait indefinitely unless `DERIVER_FLUSH_ENABLED=true` (#826)
### Changed
- Peer-scoped JWTs now get read-only access to the sessions their peer is an active member of (session context, summaries, peers, their own per-session config, search, and message reads). Session-scoped JWTs remain confined to their session and cannot reach peer routes.
- Peer-scoped JWTs now get read-only access to the sessions their peer is an active member of (session context, summaries, peers, their own per-session config, search, and message reads). Session-scoped JWTs remain confined to their session and cannot reach peer routes (#679)
- Compacted Honcho's log output, with guarded ms/s metric formatting that falls back to a plain string for non-numeric values (#836)
- Sentry now drops noisy infra/scrape transactions: the reconciler opens a transaction only once a batch has rows (idle cycles emit none), and a `traces_sampler` returns `0.0` for `/metrics`, `/health`, `/openapi.json`, `/docs`, `/redoc`, and the deriver metrics server. `SENTRY.TRACES_SAMPLE_RATE` still governs real traffic (#834)
### Fixed
- Peer- and session-scoped JWTs were effectively workspace-scoped: authorization walked the route's declared scope and fell through to a workspace match, so a `{w, p: alice}` token could act on any peer in the workspace. JWTs are now authorized by their narrowest claim and never widen to workspace access.
- The keys API now rejects creating a peer- or session-scoped key without a workspace. Such keys were minted successfully but failed verification on every request.
- Peer- and session-scoped JWTs were effectively workspace-scoped: authorization walked the route's declared scope and fell through to a workspace match, so a `{w, p: alice}` token could act on any peer in the workspace. JWTs are now authorized by their narrowest claim and never widen to workspace access (#679)
- The keys API now rejects creating a peer- or session-scoped key without a workspace. Such keys were minted successfully but failed verification on every request (#679)
- Agent-supplied observation IDs carrying the display-format `id:` prefix are now normalized (prefix and trailing whitespace stripped) before `source_ids` are stored and on `get_reasoning_chain` lookups, fixing corrupted provenance links and broken reasoning-chain traversal (#795)
- Fixed a `create_tree` keyword-argument mismatch in the Dreamer's surprisal tree construction (#749)
- Providers that omit output-token counts (observed with Gemini on tool-loop completions) returned `output_tokens=None`, which raised a Pydantic validation error that aborted the call and crashed the Dreamer's induction phase before inductive conclusions were persisted. `None` is now coerced to `0` so token accounting degrades gracefully (#809)
## [3.0.10] - 2026-06-15

View File

@ -30,7 +30,9 @@ This guide helps you match the right SDK version to your Honcho API version. New
| Honcho API Version | TypeScript SDK | Python SDK |
|-------------------|---------------|------------|
| v3.0.9 (Current) | v2.1.2 | v2.1.2 |
| v3.0.11 (Current) | v2.1.2 | v2.1.2 |
| v3.0.10 | v2.1.2 | v2.1.2 |
| v3.0.9 | v2.1.2 | v2.1.2 |
| v3.0.8 | v2.1.2 | v2.1.2 |
| v3.0.7 | v2.1.2 | v2.1.2 |
| v3.0.6 | v2.1.1 | v2.1.1 |

View File

@ -27,7 +27,32 @@ Welcome to the Honcho changelog! This section documents all notable changes to t
### Honcho API and SDK Changelogs
<Tabs>
<Tab title="Honcho API">
<Update label="v3.0.10 (Current)">
<Update label="v3.0.11 (Current)">
### Added
- `api_request_duration_seconds` Prometheus histogram tracking per-route request latency, labeled by method and endpoint (#837)
- LLM `provider_params` passthroughs (`extra_body` / `extra_headers` / `extra_query`) are now forwarded to the underlying provider transport across all backends, with shape validation that rejects non-mapping values (#821)
- `structured_output_mode` model-config option to use `json_object` mode for OpenAI-compatible providers that lack native Structured Outputs support (used by the deriver) (#820)
- OpenRouter app-attribution headers (`HTTP-Referer` / `X-Openrouter-Title`) are now sent on OpenAI-compatible clients when the configured base URL is OpenRouter, so requests are attributed to "Honcho" in OpenRouter's dashboard (#805)
- Langfuse traces are now tagged with user and session IDs for easier trace filtering (#814)
- `DERIVER_REPRESENTATION_BATCH_MAX_AGE_SECONDS` (default 1800s) lets sub-threshold representation work units flush once their oldest unprocessed queue item ages out. Set it to `0` to keep the legacy behavior where sub-threshold tails wait indefinitely unless `DERIVER_FLUSH_ENABLED=true` (#826)
### Changed
- Peer-scoped JWTs now get read-only access to the sessions their peer is an active member of (session context, summaries, peers, their own per-session config, search, and message reads). Session-scoped JWTs remain confined to their session and cannot reach peer routes (#679)
- Compacted Honcho's log output, with guarded ms/s metric formatting that falls back to a plain string for non-numeric values (#836)
- Sentry now drops noisy infra/scrape transactions: the reconciler opens a transaction only once a batch has rows (idle cycles emit none), and a `traces_sampler` returns `0.0` for `/metrics`, `/health`, `/openapi.json`, `/docs`, `/redoc`, and the deriver metrics server. `SENTRY.TRACES_SAMPLE_RATE` still governs real traffic (#834)
### Fixed
- Peer- and session-scoped JWTs were effectively workspace-scoped: authorization walked the route's declared scope and fell through to a workspace match, so a `{w, p: alice}` token could act on any peer in the workspace. JWTs are now authorized by their narrowest claim and never widen to workspace access (#679)
- The keys API now rejects creating a peer- or session-scoped key without a workspace. Such keys were minted successfully but failed verification on every request (#679)
- Agent-supplied observation IDs carrying the display-format `id:` prefix are now normalized (prefix and trailing whitespace stripped) before `source_ids` are stored and on `get_reasoning_chain` lookups, fixing corrupted provenance links and broken reasoning-chain traversal (#795)
- Fixed a `create_tree` keyword-argument mismatch in the Dreamer's surprisal tree construction (#749)
- Providers that omit output-token counts (observed with Gemini on tool-loop completions) returned `output_tokens=None`, which raised a Pydantic validation error that aborted the call and crashed the Dreamer's induction phase before inductive conclusions were persisted. `None` is now coerced to `0` so token accounting degrades gracefully (#809)
</Update>
<Update label="v3.0.10">
### Added
- Messages are now embedded via a background task rather than blocking API request
@ -671,7 +696,7 @@ Welcome to the Honcho changelog! This section documents all notable changes to t
<Tab title="Python SDK">
[Python SDK](https://pypi.org/project/honcho-ai/)
<Update label="v2.1.2 (Current)">
<Update label="v2.1.2">
### Added
- `page`, `size`, and `reverse` pagination parameters on `Honcho.workspaces()` and `HonchoAio.workspaces()`, closing the gap from 2.1.0 which added these to other list methods but not to `workspaces()`. Honoring `reverse` on the workspace/peer/session list routes also requires a Honcho server with the matching API fix; older servers silently ignore the parameter.
@ -823,7 +848,7 @@ Welcome to the Honcho changelog! This section documents all notable changes to t
<Tab title="TypeScript SDK">
[TypeScript SDK](https://www.npmjs.com/package/@honcho-ai/sdk)
<Update label="v2.1.2 (Current)">
<Update label="v2.1.2">
### Added
- `peers` option on `Honcho.session()` — attach peers to a session at creation time instead of needing a follow-up `session.addPeers()` call. Accepts the same `PeerAddition` shape as `session.addPeers()` (peer ID strings, `Peer` objects, arrays of either, or a record with per-peer `observe_me`/`observe_others` config).

View File

@ -24,7 +24,7 @@
"navigation": {
"versions": [
{
"version": "v3.0.10",
"version": "v3.0.11",
"api": {
"openapi": ["v3/openapi.json"]
},

View File

@ -1,6 +1,6 @@
[project]
name = "honcho"
version = "3.0.10"
version = "3.0.11"
description = "Honcho Server"
authors = [
{name = "Plastic Labs", email = "hello@plasticlabs.ai"},

View File

@ -6,6 +6,7 @@ This script helps update version numbers across the Honcho repository.
It handles the main API, Python SDK, and TypeScript SDK in a single operation.
"""
import argparse
import json
import os
import re
@ -17,11 +18,11 @@ from datetime import datetime
class VersionUpdater:
def __init__(self, base_path: str):
self.base_path = base_path
self.base_path: str = base_path
def get_current_versions(self) -> dict[str, str]:
"""Get current version numbers from the repository."""
versions = {}
versions: dict[str, str] = {}
# Main API version
with open(os.path.join(self.base_path, "pyproject.toml")) as f:
@ -89,7 +90,7 @@ TYPESCRIPT_VERSION=
os.unlink(temp_file)
# Extract all versions and changelogs
updates = {}
updates: dict[str, dict[str, str]] = {}
# Parse API version
api_match = re.search(r"^API_VERSION=(.*)$", content, re.MULTILINE)
@ -131,7 +132,7 @@ TYPESCRIPT_VERSION=
) -> str:
"""Extract changelog content between markers."""
lines = content.split("\n")
changelog_lines = []
changelog_lines: list[str] = []
in_section = False
for line in lines:
@ -158,8 +159,8 @@ TYPESCRIPT_VERSION=
"""Remove empty changelog sections."""
sections = ["Added", "Changed", "Fixed", "Deprecated", "Removed", "Security"]
lines = changelog.split("\n")
cleaned_lines = []
current_section = None
cleaned_lines: list[str] = []
current_section: str | None = None
section_has_content = False
section_start_idx = -1
for i, line in enumerate(lines):
@ -346,28 +347,30 @@ TYPESCRIPT_VERSION=
self._update_compatibility_guide("typescript", new_version)
def _update_docs_json(self, new_version: str):
"""Update docs.json - only update versions with same major version."""
"""Update docs.json version label(s) sharing the new version's major.
Uses a targeted regex replacement rather than a JSON round-trip so the
file's existing formatting (compact inline arrays) is preserved instead
of being reflowed.
"""
file_path = os.path.join(self.base_path, "docs/docs.json")
with open(file_path) as f:
data = json.load(f)
content = f.read()
# Get major version of new version
new_major = new_version.split(".")[0]
# Update only matching major versions
if "navigation" in data and "versions" in data["navigation"]:
for version_entry in data["navigation"]["versions"]:
if "version" in version_entry:
current_version = version_entry["version"].lstrip("v")
current_major = current_version.split(".")[0]
def _replace(match: re.Match[str]) -> str:
# Only update labels whose major version matches the new version's.
if match.group(1) == new_major:
return f'"version": "v{new_version}"'
return match.group(0)
if current_major == new_major:
version_entry["version"] = f"v{new_version}"
content = re.sub(r'"version": "v(\d+)\.\d+\.\d+"', _replace, content)
with open(file_path, "w") as f:
json.dump(data, f, indent=2)
f.write("\n")
f.write(content)
def _update_sdk_changelog(self, version: str, changelog: str, relative_path: str):
"""Update an SDK's CHANGELOG.md file."""
@ -405,12 +408,39 @@ TYPESCRIPT_VERSION=
f.write(new_content)
def _update_changelog_md(self, version: str, changelog: str):
"""Update the main CHANGELOG.md file."""
"""Update the main CHANGELOG.md file.
If an ``## [Unreleased]`` section is present, it is promoted to the new
version (its contents replaced by ``changelog``, which the caller is
expected to have already merged). Otherwise a new version entry is
prepended above the most recent release, preserving the legacy behavior.
"""
file_path = os.path.join(self.base_path, "CHANGELOG.md")
with open(file_path) as f:
content = f.read()
date = datetime.now().strftime("%Y-%m-%d")
# Ensure changelog content is properly formatted
if changelog.strip():
formatted_changelog = changelog.strip()
else:
formatted_changelog = "### Changed\n\n- Updated version"
# Promote an existing [Unreleased] section if one exists. Match from the
# "## [Unreleased]" header up to (but not including) the next release
# heading, and replace the whole block with the new version section.
unreleased_re = re.compile(
r"\n## \[Unreleased\][\s\S]*?(?=\n## \[)", re.IGNORECASE
)
if unreleased_re.search(content):
replacement = f"\n## [{version}] - {date}\n\n{formatted_changelog}\n"
new_content = unreleased_re.sub(replacement, content, count=1)
with open(file_path, "w") as f:
f.write(new_content)
return
# Find the position after the header
header_end = content.find("\n## [")
if header_end == -1:
@ -420,15 +450,6 @@ TYPESCRIPT_VERSION=
# No existing entries, add after title
header_end = content.find("\n", content.find("# Changelog"))
# Create new entry with proper formatting
date = datetime.now().strftime("%Y-%m-%d")
# Ensure changelog content is properly formatted
if changelog.strip():
formatted_changelog = changelog.strip()
else:
formatted_changelog = "### Changed\n\n- Updated version"
new_entry = f"\n\n## [{version}] - {date}\n\n{formatted_changelog}\n"
# Insert the new entry
@ -624,7 +645,68 @@ TYPESCRIPT_VERSION=
f.write(content)
def _resolve_changelog(value: str | None) -> str:
"""Resolve a changelog argument that is either inline text or a file path."""
if not value:
return ""
if os.path.isfile(value):
with open(value) as f:
return f.read().strip()
return value.strip()
def _updates_from_args(args: argparse.Namespace) -> dict[str, dict[str, str]]:
"""Build the updates dict from CLI flags (headless mode)."""
updates: dict[str, dict[str, str]] = {}
if args.api_version:
updates["api"] = {
"version": args.api_version,
"changelog": _resolve_changelog(args.api_changelog),
}
if args.python_version:
updates["python_sdk"] = {
"version": args.python_version,
"changelog": _resolve_changelog(args.python_changelog),
}
if args.typescript_version:
updates["typescript_sdk"] = {
"version": args.typescript_version,
"changelog": _resolve_changelog(args.typescript_changelog),
}
return updates
def main():
parser = argparse.ArgumentParser(
description=(
"Update Honcho version numbers and changelogs. With no version "
"flags, opens an interactive editor; pass one or more --*-version "
"flags to run headless (agent-friendly)."
)
)
parser.add_argument("--api-version", help="New Main API version.")
parser.add_argument("--python-version", help="New Python SDK version.")
parser.add_argument("--typescript-version", help="New TypeScript SDK version.")
parser.add_argument(
"--api-changelog",
help="API changelog markdown, or a path to a file containing it.",
)
parser.add_argument(
"--python-changelog",
help="Python SDK changelog markdown, or a path to a file containing it.",
)
parser.add_argument(
"--typescript-changelog",
help="TypeScript SDK changelog markdown, or a path to a file containing it.",
)
parser.add_argument(
"-y",
"--yes",
action="store_true",
help="Skip the confirmation prompt (implied in headless mode).",
)
args = parser.parse_args()
# Get the parent directory of the scripts folder (the project root)
script_dir = os.path.dirname(os.path.abspath(__file__))
base_path = os.path.dirname(script_dir)
@ -633,6 +715,8 @@ def main():
# Get current versions
current_versions = updater.get_current_versions()
headless = any([args.api_version, args.python_version, args.typescript_version])
print("Honcho Version Updater")
print("=" * 50)
print("\nCurrent versions:")
@ -640,12 +724,15 @@ def main():
print(f" Python SDK: {current_versions['python_sdk']}")
print(f" TypeScript SDK: {current_versions['typescript_sdk']}")
print()
print("Opening editor for version updates...")
print("Leave version fields blank to skip updating that component.")
print()
# Get all updates at once
updates = updater.get_all_versions_from_editor(current_versions)
if headless:
updates = _updates_from_args(args)
else:
print("Opening editor for version updates...")
print("Leave version fields blank to skip updating that component.")
print()
# Get all updates at once
updates = updater.get_all_versions_from_editor(current_versions)
if not updates:
print("No versions specified. Exiting...")
@ -661,11 +748,12 @@ def main():
}[component]
print(f" {component_name}: {current_versions[component]} → {info['version']}")
# Confirm
response = input("\nProceed with updates? (y/n): ").strip().lower()
if response != "y":
print("Cancelled.")
sys.exit(0)
# Confirm (skipped in headless mode or with --yes)
if not headless and not args.yes:
response = input("\nProceed with updates? (y/n): ").strip().lower()
if response != "y":
print("Cancelled.")
sys.exit(0)
# Apply all updates
updater.update_all(updates, current_versions)
@ -673,6 +761,7 @@ def main():
print("\nVersion updates complete!")
print("\nDon't forget to:")
print(" - Review the changes with `git diff`")
print(" - Run `uv lock` to refresh the lockfile")
print(" - Commit the changes")
print(" - Create git tags for the new versions")
print(" - Push the changes and tags")

View File

@ -8,7 +8,7 @@ resolution-markers = [
]
[options]
exclude-newer = "2026-06-10T19:45:16.447512Z"
exclude-newer = "2026-06-19T14:44:24.535479Z"
exclude-newer-span = "P5D"
[manifest]
@ -1159,7 +1159,7 @@ wheels = [
[[package]]
name = "honcho"
version = "3.0.10"
version = "3.0.11"
source = { virtual = "." }
dependencies = [
{ name = "alembic" },