From 60a15e664d7298eb790b788e95c6ca2e6bd30c80 Mon Sep 17 00:00:00 2001 From: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com> Date: Wed, 24 Jun 2026 12:44:13 -0400 Subject: [PATCH] v3.0.11 Release Candidate (#841) * chore(docs): Release Candidate Changelog and Version Updates * chore: fix basedpyright error --- CHANGELOG.md | 20 ++- docs/changelog/compatibility-guide.mdx | 4 +- docs/changelog/introduction.mdx | 31 ++++- docs/docs.json | 2 +- pyproject.toml | 2 +- scripts/update_version.py | 165 +++++++++++++++++++------ uv.lock | 4 +- 7 files changed, 177 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bc93dc8..474c198d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/changelog/compatibility-guide.mdx b/docs/changelog/compatibility-guide.mdx index d1e225b9..42b2f373 100644 --- a/docs/changelog/compatibility-guide.mdx +++ b/docs/changelog/compatibility-guide.mdx @@ -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 | diff --git a/docs/changelog/introduction.mdx b/docs/changelog/introduction.mdx index c5bc129d..a69d4567 100644 --- a/docs/changelog/introduction.mdx +++ b/docs/changelog/introduction.mdx @@ -27,7 +27,32 @@ Welcome to the Honcho changelog! This section documents all notable changes to t ### Honcho API and SDK Changelogs - + + ### 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) + + + ### 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 [Python SDK](https://pypi.org/project/honcho-ai/) - + ### 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 [TypeScript SDK](https://www.npmjs.com/package/@honcho-ai/sdk) - + ### 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). diff --git a/docs/docs.json b/docs/docs.json index 669faf96..889ea9e6 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -24,7 +24,7 @@ "navigation": { "versions": [ { - "version": "v3.0.10", + "version": "v3.0.11", "api": { "openapi": ["v3/openapi.json"] }, diff --git a/pyproject.toml b/pyproject.toml index 95a7789c..a79a3d85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"}, diff --git a/scripts/update_version.py b/scripts/update_version.py index 4d99020b..40416567 100755 --- a/scripts/update_version.py +++ b/scripts/update_version.py @@ -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") diff --git a/uv.lock b/uv.lock index a71283bb..2d0ce1d7 100644 --- a/uv.lock +++ b/uv.lock @@ -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" },