Compare commits

...

420 Commits
v2.4.2 ... main

Author SHA1 Message Date
ajspig 699c99368d
feat(harness-plugin-core): simplify telemetry headers and HOME-first config path (#1124)
* feat(harness-plugin-core): simplify telemetry headers and HOME-first config path

- Identity is three headers: X-Honcho-Host `name/version (platform)`,
  X-Honcho-Plugin `name/version`, X-Honcho-Agent-Model. X-Honcho-Runtime is
  dropped. TelemetryIdentity gains `plugin` and `platform`.
- configPath() resolves env.HOME (then USERPROFILE) before os.homedir(), since
  Bun's homedir() ignores in-process HOME changes and plugin tests were hitting
  the real ~/.honcho/config.json.
- Extensionless internal imports so consumers no longer need
  allowImportingTsExtensions to type-check against the source exports.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* chore: minor nits

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 17:19:15 -04:00
Eugene Eisenstein 9677f3d80c
fix(tests): repair unsatisfiable unified-test assertions and record why tests fail (#1123)
* fix(tests): repair unsatisfiable unified-test assertions and surface traces

Five of the eight persistent `unified-tests` failures assert things the code
cannot produce. None are regressions.

Raise the queue-drain timeout to 600s on the three large longmem fixtures.
They ingest 484-550 messages across ~50 sessions, then wait on the 60s
`WaitAction` default; the deriver is still working normally when the timer
fires. Matches the sibling 550-message case that already passes.

Raise `max_tokens` to 2500 in the two config-summary fixtures. Context
allocates 40% of the limit to the summary, so the previous 400 gave a
160-token budget while `SUMMARY.MAX_TOKENS_SHORT` is 1000 — no conforming
summary could ever fit, and the query returned `summary=None` even though the
summary was created.

Drop `session_id` from the dream test's `get_representation` step. A bare
session id becomes a one-element allowlist, and an allowlist narrows levels to
`ALLOWLIST_SAFE_LEVELS` (`explicit`), so the deductive and inductive
observations the step asserts on are excluded by design. The unscoped
representation is where the dreamer's conclusions are actually served.

Delete `WaitAction.flush`. Flush is process-wide — the harness starts the
deriver with `DERIVER_FLUSH_ENABLED=true` — and there is no per-request flush,
so the field never had an effect despite being set in 47 places. `TestStep`
now forbids extra fields so a dead knob cannot silently accumulate again.

Presign the reasoning traces alongside `results.json` and report both to the
Discord webhook and a GitHub job summary. The traces hold the full prompts and
model outputs and were already uploaded, but only `results.json` was surfaced.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(tests): record why a unified test failed, not just that it did

`results.json` carried only name, status and duration, so a red run said
which test failed and nothing about why. The reason existed solely in the job
log, where the secrets action's masking can render it unreadable — diagnosing
a failure meant re-reading GHA logs that had digits redacted out of them.

`execute` now returns the `StepFailure` that stopped the test (step index,
step type, and the exception message) instead of a bare bool. Assertion
failures already raised useful text, including the LLM judge's own reasoning;
that text now reaches `results.json`, the console output, the job summary and
the Discord message rather than being discarded at the call site.

`results` moves from a `(status, duration)` tuple to a `TestOutcome` with
named fields so the failure can ride along.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(tests): keep the Discord report inside the webhook size limit

The failure reasons added to the Discord message pushed it past Discord's
2000-character content limit, and the webhook answered 400 — run
33779689337 sent no notification at all. Six LLM-judge verdicts run to
~2760 characters; capping the count at ten did nothing because the length
was never the count.

Reasons are now clipped per line for Discord only; the job summary, the
console and results.json keep them whole. `send_discord_message` also clamps
the assembled content, so an over-long report loses its tail rather than the
entire notification.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(tests): keep the Discord report short and link to the Actions run

The Discord message restated every failure, which pushed it past Discord's
2000-character limit and returned a 400 — run 33779689337 sent no
notification at all.

The report is now the headline, the results link, an Actions run link, and
the traces S3 key. Per-test failure reasons stay in the job summary that the
Actions link points at, along with both presigned URLs, so nothing is lost by
not repeating them in chat.

Restating failures was not the only size risk. A presigned URL carries an
OIDC session token and can run past a thousand characters by itself, so two
of them exceeded the limit unaided — which is why the traces go in as their
S3 key, the `aws s3 cp` path, at ~90 characters instead of ~1500.

`clamp_lines` drops whole lines rather than characters, since half a
presigned URL is useless and renders as broken markdown, and drops the
longest line first so an overlong URL cannot evict the short Actions link
that leads to everything else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(api): say when a session summary is dropped for budget

`get_context` allocates 40% of the token limit to the summary, but that limit
is what remains *after* the peer representation and peer card are subtracted,
not the `tokens` the caller asked for. When nothing fits, the caller receives
`summary: null` — indistinguishable from a session that has no summary — and
the only trace was a debug line in a different module.

`_select_summary_for_context` now logs at info when summaries exist and none
was chosen, with the budget and the sizes that missed it.

The two `config_summary_control` fixtures go to 4000. Measured against CI run
33779689337, their 12 messages produce 12 explicit observations costing ~1176
tokens, so the original `max_tokens: 400` left a budget of -776: no summary of
any size could have been served, and the earlier reading of this failure — a
160-token budget against a 388-token summary — had the mechanism wrong. 2500
was also short, leaving 529 against a `SUMMARY.MAX_TOKENS_SHORT` of 1000; 3676
is the minimum that guarantees a conforming summary fits.

Tests cover the budget arithmetic at each of those limits, the new log line,
and that a stored summary is served through the route with and without an
observer — the retrieval path itself was never at fault and had no coverage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(api): report a dropped summary on the path get_context actually takes

The previous commit added this log to `_select_summary_for_context`, which
only runs when `get_context` is given a `peer_target`. The unified
`config_summary` fixtures set `observer_peer_id`, but the runner does not
forward it, so those requests take `summarizer.get_session_context` instead —
where the same outcome was reported at debug and stayed invisible.

That also retracts the representation-budget explanation for those fixtures.
Nothing is subtracted from the limit on this path: the summary gets 40% of the
requested tokens outright, so at `max_tokens: 4000` a 99-token summary has a
1600-token budget and fits comfortably. The reason it is still absent is not
the budget, and the log now says so on the right path.

Tests cover both paths, and record that the fixtures exercise the one without
a representation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(tests): drop the ignored observer from the config_summary fixtures

`observer_peer_id` has no effect on a `get_context` step — the runner does not
forward it — so it read as scoping a request that was never scoped. The step
description now records that these are unscoped reads and what naming an
observer would change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 16:12:02 -04:00
Rajat Ahuja 235900b9e5
docs for programmatically creating api key (#1133)
* docs for programmatically creating api key

* chore: adtl reference to the create-key endpoint

* fix: enhance docs

---------

Co-authored-by: ajspig <dragon@monstercode.com>
2026-09-03 16:01:54 -04:00
Eugene Eisenstein afbc517cbc
feat(mock-provider): deterministic OpenAI-compatible endpoint for local and CI use (#1094)
* feat(mock-provider): deterministic OpenAI-compatible endpoint for local and CI use

Adds src/mock_provider/, a standalone ASGI app that lets Honcho run with no
model provider, no API key, and no spend. It answers /v1/chat/completions and
/v1/embeddings with obviously-synthetic content derived from the request, so
the same request always produces the same response.

It runs as its own service from the standard Honcho image with a different
entrypoint, the way api and deriver already differ, so there is no second image
to build or keep in digest-sync. The app imports nothing from src.config or
src.db, so it boots even when the rest of the stack is misconfigured.

The chat endpoint generates from the JSON Schema it is sent rather than
answering with prose. That matters because a prose answer does not fail loudly:
repair_response_model_json swallows the parse error and returns an empty
PromptRepresentation, which reads as "the deriver found nothing" rather than
"the mock is wrong". Generation resolves $ref/$defs indirection, caps recursion
for reasoning-tree schemas, and covers json_object mode by recovering the
schema Honcho injects into the prompt. Embeddings are hash-derived, so
identical input yields an identical vector.

Tests drive the production OpenAIBackend and _EmbeddingClient against the app
over ASGI, including the strict json_schema transform that
chat.completions.parse() applies. Verified end to end against a real stack:
messages in, conclusions and 1536-dim embeddings written to pgvector, with no
calls to any real provider.

Mock embeddings carry no semantic similarity, so recall against this provider
must use lexical search. CONTRIBUTING notes that, and the load_dotenv(override=
True) behaviour that lets a stale repo .env win over exported environment
variables.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(mock-provider): validate requests with Pydantic models

Review feedback: hand-coercing the request bodies was defended on the grounds
that FastAPI answers a malformed body with a 422, and a 422 mid-deriver-run
reads as a Honcho bug. That argues against the default handler, not against the
models. Registering an exception handler fixes it — and the resulting behaviour
is more faithful, not less, because the real API answers a bad request with a
400 and an `error` envelope, which is now exactly what the mock returns.

Adds src/mock_provider/schemas.py with ChatCompletionRequest and
EmbeddingsRequest. Every model allows extra fields and every field is optional,
so validation fires on a wrong type rather than on a parameter the mock has not
heard of — a new upstream parameter must not turn a working setup into a hard
failure. dimensions is a StrictInt because bool is an int subclass and a JSON
`true` would otherwise mean a one-dimensional vector.

coerce.py stays, narrowed to serving schema_gen, which walks arbitrary
caller-supplied JSON Schema and is untyped by nature. response_format likewise
stays dict[str, Any]: only its envelope is worth typing.

Also records why schema_gen does not reuse src/utils/schema_conversion.py
despite the overlapping $ref/$defs handling — it builds a model class rather
than an instance, raises by contract where a mock must degrade, and rejects
both allOf and the recursive $ref that reasoning-tree schemas rely on.

Documents that LLM_OPENAI_API_KEY is only tested for truthiness; the previous
wording read as though the value had to be the literal string "sandbox".

Re-verified end to end after the refactor: 6 messages in, 4 conclusions and 6
1536-dim embeddings out, every real request answered 200, no calls to any real
provider.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(mock-provider): honour include_usage, generate prefixItems tuples

Three fidelity gaps where the mock answered a request differently from the
API it stands in for:

- The usage chunk was emitted on every stream. The real API sends it only
  when stream_options.include_usage is set, so a caller that did not opt in
  had to skip a trailing chunk with an empty choices array. stream_options
  is now a typed model, which also rejects a non-boolean include_usage
  instead of reading it as truthy.
- A fixed-length tuple is prefixItems with no items, which is what Pydantic
  emits for tuple[str, int]. Reading only items returned [], failing the
  minItems the same schema carries — the silent-empty failure schema_gen
  exists to avoid.
- A zero or negative dimensions was silently replaced with 1536, answering
  a bad request with a plausible-looking vector rather than a 400.

Three further deviations from JSON Schema are left in place and documented
where they occur: allOf merges properties first-wins, oneOf is treated as
anyOf, and string pattern is ignored. None is reachable from a Honcho
response model — no model emits prefixItems or oneOf, and the only pattern
constraints are on API request models — and each fix costs more than the
unreachable path is worth.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(mock-provider): strict request booleans, bounded recursion, multipleOf

Second CodeRabbit pass. All four findings reproduced first; none is reachable
from a Honcho response model, but two trace back to the previous commit.

- `include_usage` and `stream` were plain `bool`, which Pydantic coerces from
  "yes"/"on"/"true"/"1". The comment added last commit claimed a string had to
  fail here, and it did not — the test only passed because "definitely" is not
  a recognised bool literal. Both are StrictBool now, matching why `dimensions`
  is StrictInt, and the tests cover the truthy strings that actually coerced.
- `_generate_array` returned the prefix alone when `items` was absent, so
  prefixItems plus a larger minItems undershot its own schema. Absent `items`
  leaves those positions unconstrained rather than disallowed, so the shortfall
  is filled to minItems — a bare `{"type": "array"}` still generates nothing.
- A required, non-nullable recursive $ref hit RecursionError: MAX_DEPTH only
  terminates a cycle that offers a `default` or a nullable branch, and
  `_generate_object` keeps descending into required properties. HARD_MAX_DEPTH
  degrades to an empty container instead, since a mock must not turn its own
  defect into a 500. Bounded, not plumbed into an error response — the
  unreachable path does not justify touching the request path.
- `_bounded_int` ignored `multipleOf` while honouring minimum, maximum and both
  exclusive bounds; 9 of 12 sampled paths produced a non-multiple. Values now
  snap onto a multiple inside the bounds, and an unsatisfiable window keeps the
  bounds. A fractional `multipleOf` is still ignored, as documented.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(mock-provider): correct the reason fractional multipleOf is dropped

The docstring claimed honouring it would mean returning a non-integer from an
integer schema. That is wrong: 3 is an integer and a multiple of 1.5. The real
reason is that it needs exact-decimal arithmetic to keep float drift from
deciding validity, and no Honcho response model emits multipleOf at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 14:20:47 -04:00
Eugene Eisenstein 7cf865c969
docs(contributing): update CONTRIBUTING.md with PR response time
Added a note about responding to PRs within 7 days.
2026-09-03 13:52:03 -04:00
Aakash Kattelu 2ad56a4d71
feat(mcp): add stdio host for local clients (#1102)
* feat(mcp): add stdio host for local clients

* feat(mcp): add Streamable HTTP host and image

Long-lived HTTP entry for Docker and other process hosts, reusing
createServer(). Dedicated mcp/Dockerfile; compose service beside api.

* fix(mcp): stdio launcher cwd/silent and HTTP session bounds

Pin bun --cwd so bunfig loads. Silence bun run. Require Bearer on HTTP.
Idle-expire and cap in-memory MCP sessions.

* fix(mcp): re-check bearer on established HTTP sessions

Session lookup returned early without Authorization, so a missing or
wrong token still 200'd after initialize. Bind each session to the
init key and 401 on mismatch.

* fix: nit cleaning claude command

---------

Co-authored-by: ajspig <dragon@monstercode.com>
2026-09-02 17:56:51 -04:00
ajspig b573a84806
Harness core (#1110)
* chore: scaffold @honcho-ai/harness-core

* feat(harness-core): resolve shared root config

* feat(harness-core): send client identity headers on SDK requests

* feat(harness-core): drop cloud vs custom api header

* feat(harness-core): migrating v0 config to schema v1 on read

* chore(harness-core): clean up

* feat(config): describe oauth and host overrides in the v1 schema

* chore: rename to harness-plugin-core

* feat(harness-plugin-core): update telemetry headers on a live client.
2026-09-02 17:56:09 -04:00
steven-ji 55a0519bd2
feat(sdk): add per-call peer chat timeout (#1098)
Forward optional timeout overrides through sync and async Peer.chat while retaining client-wide defaults.

Refs #734
2026-09-02 17:31:46 -04:00
Eugene Eisenstein a5fa8c3962
fix(dialectic): make workspace chat search before it answers (#1120)
The workspace agent's prefetch is an orientation overview — scale, active
peers, their cards — not the corpus. `low` is the only reasoning level that
explicitly sets TOOL_CHOICE="auto", so the model was free to skip tools
entirely, and it did: every workspace_chat call in CI run 33662772219 made
zero tool calls. It answered when the overview happened to carry the fact and
otherwise wrote out the search it should have run, then asked the caller which
option to take — at an endpoint with no caller to answer.

Add a `_tool_choice` seam alongside `_select_tools` and override it on
WorkspaceDialecticAgent to require a tool call. `execute_tool_loop` already
relaxes "required"/"any" to "auto" after the first iteration, so this costs one
search round rather than pinning the loop, and the model can still stop and
synthesize. Any value a level configures other than None/"auto" passes through.
The pair agent is unaffected: it prefetches the observations for its query and
can legitimately answer from context alone.

Also tell the workspace prompt it is non-interactive. It had "Do not narrate
tool use" but never said the caller cannot reply, and three of the five traced
responses ended in a menu of lookups.

Unified subset goes 1/5 -> 5/5, and search_memory — the recall path that never
once ran — now fires on 6 of 7 workspace queries. workspace_chat_scope is the
notable one: its two not_contains assertions were passing vacuously because
nothing was ever retrieved, and it now recalls the in-scope fact while still
excluding the out-of-scope vault code.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 17:27:01 -04:00
steven-ji 7d5d6109f7
feat(docker): make API worker count configurable (#1088)
* feat(docker): make API worker count configurable

Add API_WORKERS with a single-worker default and document database pool sizing.

Refs #1063

* fix(docker): address API worker review feedback
2026-09-02 17:05:02 -04:00
Ulysse Pence 5d992bc65a
feat(api): Export deriver backlog as metrics from API endpoint (#1115) 2026-09-02 13:42:48 -04:00
Aakash Kattelu 997b4764b9
chore: add changelog and version updates (#1117)
API: 3.1.0 -> 3.1.1
Python/TS SDK: 2.4.0 -> 2.4.0 (unchanged)
CLI: 0.1.4 -> 0.1.4 (unchanged)
2026-09-02 12:37:17 -04:00
Vineeth Voruganti ced1514200
chore(docs): Add section about harness integrations and deepseek harn… (#1116)
* chore(docs): Add section about harness integrations and deepseek harness to docs

* chore: Add section about harness integrations and deepseek harness to docs
2026-09-02 11:19:42 -04:00
Aakash Kattelu ccdb8ba113
fix(deriver): fix create_documents deadlock (#1033)
* fix(deriver): eliminate create_documents deadlock and stop silently burning batches on transient errors

Two concurrent work units writing the same (workspace, observer, observed)
collection deadlocked on times_derived reinforcement UPDATEs issued in
batch order (DEV-1975, 682 events in 90 days). The deadlock was swallowed
per-document, the loop cascaded PendingRollbackErrors against the dead
session, the whole batch was lost, and the queue item was marked processed.

- serialize writers per collection with a transaction-scoped advisory lock
  (pg_advisory_xact_lock + SET LOCAL lock_timeout), skipped for insert-only
  batches; covers all three row-lock sites in one move
- hoist external-vector-store dup-candidate resolution ahead of the first
  DB statement so the lock's critical section contains no network calls
- abort the batch on SQLAlchemyError instead of continuing through an
  aborted transaction; per-document skip semantics kept for non-DB errors
- classify transient errors (new src/utils/retryable_errors.py) and retry
  them via a bounded in-process counter instead of marking items errored

* fix(deriver): replace create_documents advisory lock with id-ordered row locks

Advisory locks are database-scoped and would serialize every writer to a
collection, including across Groudon tenants that share names. Collect
reinforcement and replace ops during the loop, lock target rows with
SELECT ... ORDER BY id FOR UPDATE, then apply. populate_existing reloads
times_derived so a prefetched identity-map row cannot lose a concurrent
increment.

* fix(deriver): harden create_documents candidate hoist and test isolation

Skip empty embeddings on the external-store path, isolate per-document
resolve failures, and keep replacement times_derived in the in-batch
ledger. Patch get_external_vector_store in the hoist test and cover
in-loop SQLAlchemyError abort.

* fix(deriver): address CodeRabbit findings on create_documents deadlock fix

- Distinguish external resolve failure ([] skip) from pgvector fallback (None)
  so _semantic_dup_decision never re-enters external I/O under an open session
- Bound external candidate hoist concurrency with a semaphore
- Map in-loop IntegrityError to ValidationException for a uniform contract
- Persist transient retry attempts on the oldest unprocessed queue item so
  every deriver instance shares one MAX_RETRYABLE_ATTEMPTS budget
- Cover resolve-failure skip and multi-manager reclaim of the retry budget

* fix(deriver): harden retry metadata cleanup and stale reinforce fallback

- Strip _retry_attempts from payloads in the same transaction as
  mark_queue_items_as_processed / mark_queue_item_as_errored
- Clear shared retry metadata only after a successful terminal mark
- On reinforce, if the locked target is gone or soft-deleted, insert the
  incoming document instead of dropping it
- Skip pgvector semantic lookup when embedding is empty so query_documents
  cannot embed under an open session

* fix(deriver): address review on deadlock retry and row-lock apply

Strip _retry_attempts before payload validation so non-representation
tasks are not burned as extra_forbidden. Re-raise retryable observer
save errors after telemetry so the queue actually retries. Skip
same-batch reinforce fallbacks after a replace. Revert unordered
FOR UPDATE on mark processed/errored and drop post-commit retry
cleanup from the success path.

* fix: add test and simplify queue query

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-09-02 11:07:50 -04:00
Vineeth Voruganti a026bebdef
chore(docs): Add explanation on deleting data and cloud vs local differences (#1114) 2026-09-01 23:03:44 -04:00
Aakash Kattelu c300236c11
fix(deriver): reduce scope backfill memory usage (#1104)
* fix(deriver): chunk scope backfill so large sessions don't OOM the worker

_run_backfill embedded, wrote, and synced every planned copy at once, holding
one Python float list per document. A 14k-document session is ~580MB of
vectors alone, and several backfills run concurrently, which OOM-killed the
deriver at its 1000Mi limit and crash-looped it since the work units never
completed. Phases 2-4 now run per chunk of 500 specs and drop each chunk's
embeddings once synced.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(deriver): hydrate backfill embeddings per chunk

Phase 1 no longer materializes every source embedding into plans.
load_only skips the vector column on the plan queries, and each chunk
reloads only its source embeddings before embed/write/sync.

* fix(deriver): lock scope membership across backfill chunk writes

SELECT ... FOR UPDATE on the active SessionPeer row so a concurrent
leave cannot commit between the membership check and the copy inserts.
Adds a concurrency test that asserts the leave blocks until commit.

* fix: add test for memory bound

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-09-01 09:17:34 -04:00
Eugene Eisenstein 03253d7a08
fix(deriver): strip NUL bytes from model-generated observations (#1095)
* fix(deriver): strip NUL bytes from model-generated observations

Postgres rejects NUL (0x00) in text columns and in jsonb strings. API
ingress has always stripped it from user-supplied content, but the
deriver's own output did not go through any equivalent: a model can emit
a \u0000 escape in its tool-call arguments, which the JSON parser decodes
into a real NUL byte. Seen in production when models transcribe shell
output (`tr '\x00' '\n'`) or Windows paths (`c:\<NUL>users\amal`).

The NUL reached the exact-content dedup pre-fetch in create_documents as
a bind parameter, so the query raised DataError before any row was
written and the whole batch for that observer was dropped.

Strip in _normalized_observation and _normalized_observation_input --
the points that already normalize text for persistence and embedding --
so the embedded text matches the stored text. premises and sources are
covered too, since they ride along in internal_metadata. The emptiness
check now runs after normalization, because str.strip() does not remove
NUL and all-NUL content would otherwise be stored as an empty string.

DocumentCreate.content gets a mode="before" validator as a backstop for
callers that bypass those paths; running before the length constraint
makes all-NUL content fail min_length rather than silently empty out.

The NUL helpers move out of schemas/api.py into utils/sanitization.py as
a single recursive strip_nul, so ingress and internal paths share one
implementation. It is overloaded to keep str -> str for the callers that
chain .strip(), and passes None through so optional fields need no guard.

Fixes HONCHO-4XZ

* fix: broaden nul strip check

* chore: code simplification

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-08-31 13:32:43 -04:00
Eri Barrett 526461a642
Merge pull request #1071 from plastic-labs/eri/dev-2465
feat(docs): cookie consent on honcho.dev/docs
2026-08-31 11:40:48 -04:00
Eugene Eisenstein 82a92429b8
chore: harmonize Python version at 3.13 (#1090) 2026-08-27 18:54:44 -04:00
Vineeth Voruganti 4acd78d45f
chore(docs): Add Documentation for Scopes (#1086)
* chore(docs): Add Documentation for Scopes

* chore(docs): add scopes to API reference, architecture, and design patterns

- Add the seven /scopes routes and their schemas to openapi.json, plus the
  scope/kind fields on chat, representation, session-create, and peer-list
  schemas; generate the endpoint pages and register a scopes nav group
- Add a Scopes subsection and diagram node to the architecture data model
- Add scope guidance to design patterns: quick-reference rows, an isolation
  boundary comparison (workspace / scope / session allowlist), and common
  mistakes (scope-per-reader, scopes-as-access-control)
- Replace the "Underneath the Facade" section in scopes.mdx with behavioral
  guardrails and pointers to the implementation source

* chore(docs): tighten scopes doc to decision-level detail

- Drop the recall-resolution diagram (restated the Two Arms table)
- Replace the enumerated Rules table with prose; caps and error shapes
  now live in the API reference schema descriptions
- Trim backfill/removal internals to observable behavior and note that a
  backfilled scope deepens through subsequent dreams

* chore(docs): reserve "scope" for the scopes feature

Using it as a verb for session design, recall filters, and CLI targeting
collides with the named-session-set feature.

* chore(docs): clarify the scopes page and document create/status responses

The page now leads with projection rather than partition and points at the
scopes API; OpenAPI declares the 201/409/404 those routes actually return.

* chore(docs): fix broken anchor and core-concepts link

The rebase reintroduced a link to a renamed anchor in scopes.mdx, and
unified-memory-setup pointed at /core-concepts/, which has no index page.

* chore(docs): correct scope arms, listing, and read-surface pointers

The Accepts row mixed named-scope with the allowlist arm, kind=scope on
the peers list does not return facade ids, and chat/context/search never
mentioned scope=.

* chore(docs): drop the 1k-token session batching narrative

Reasoning no longer waits on a per-session token threshold, so product
docs should not tell people to size sessions around that gate.

* chore: minor fix
2026-08-27 15:21:10 -04:00
Erosika 86f8eb3e6d fix(docs): loader re-checks consent before init and captures SPA pageviews
If consent is withdrawn while array.js downloads, sync() runs before window.posthog exists and the opt-out is skipped. onload now re-checks granted() and resets loaded so a later re-grant retries.

Mintlify swaps pages without a reload, so capture_pageview: 'history_change' records navigation past the landing page.
2026-08-27 10:53:10 -04:00
Erosika f3db11ef3a chore(docs): undo array reformatting in docs.json
The integrations change is the only intended edit. The one-item arrays go back to their single-line form and the trailing newline returns.
2026-08-27 10:53:10 -04:00
Aakash Kattelu e1537216cf
fix: stop top_k=0 from reaching Turbopuffer on message search (#1084)
* fix: stop top_k=0 from reaching Turbopuffer on message search

HONCHO-19Q: dreamer search_messages passed LLM limit=0 through to
Turbopuffer (top_k must be 1..10000). #970 guarded documents; this
closes the message path and floors tool limits at 1.

* fix: preserve pgvector None sentinel on zero top_k

query_external_vector_document_ids must return None when on the
pgvector path before applying the top_k<=0 empty-list guard.
2026-08-26 17:07:41 -04:00
Rajat Ahuja 168185ae2b
fix(ci): stop skipping unified tests on merge to main (#1083)
The gate job only runs on `pull_request: labeled`, so it is skipped on
push. A skipped ancestor propagates down the needs chain unless a job
opts out, which `unified-tests` never did — so the suite has been
skipped on every merge to main while still burning a Fly machine.
2026-08-26 15:04:03 -04:00
ajspig 2ddd819a28
chore: bump honcho-cli to 0.1.4 (#1080)
* feat(cli): fix API key typing

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: bump honcho-cli to 0.1.4

Ship the masked --setup API key prompt plus the openai-compatible embedding base URL already on main.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(cli): check for newer version

* docs: nit

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-26 13:05:25 -04:00
Aakash Kattelu dea2917fa9
fix(openai): fix content normalization in openai backend history adapter (#1064)
* fix(llm): preserve null content on OpenAI tool-call turns

OpenAI-compatible providers can return assistant tool-call messages with
content=null. Coercing that to "" before history replay breaks providers
that bind opaque reasoning state to the exact assistant message shape.

Keep null only when the normalized response has tool calls; tool-less
null still becomes "", and content_override stays authoritative.

Fixes #1061

* test(live_llm): cover OpenAI null content tool-call replay

Add a live multi-turn tool replay that asserts provider content=null stays
null through normalize + OpenAIHistoryAdapter and that the continuation
still answers. Mark gpt_4/gpt_5 families as supports_tool_replay.

* docs(llm): note content_override None sentinel semantics

None means no override, not force-null content. Addresses review on #1064.
2026-08-26 12:14:32 -04:00
Aakash Kattelu 370232e139
fix(ci): exempt issue-gate writers via repo permission (#1081)
author_association on the webhook is CONTRIBUTOR when org membership is
private, so maintainers with write (e.g. ajspig) were labelled
needs-approved-issue. Skip on admin/maintain/write from
getCollaboratorPermissionLevel instead; 404 stays gated.
2026-08-26 12:14:23 -04:00
ajspig d04f622317
docs: adding honcho start (#1073)
* docs: make honcho start the documented local path

honcho-cli 0.1.3 can run a personal stack without cloning the repo; point the README, self-hosting, and CLI reference at that, and drop the community installer callouts.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: updating with new CLI language

* docs: drop compatibility-guide changes from this PR

Leave that file on main; CLI version cards are updated at release time.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: small language changes

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-26 11:39:58 -04:00
Erosika b5d1a1ae54 fix(docs): loader survives a failed fetch and honors withdrawal
Review findings on #1071. The cookie match now requires the exact
CookieConsent name boundary. A failed array.js request resets the
loaded flag so later consent events retry. And consent events now run
a full sync: withdrawal opts an already running instance out, and a
re-grant opts it back in — same behavior as the landing site's gate.
2026-08-26 10:25:26 -04:00
Rajat Ahuja 9380bf2753
fix(docker): ship pyproject.toml in the runtime image (#1074)
The runtime stage copies application code but not pyproject.toml, so
src/_version.py cannot find the file it reads the version from. The
image also installs dependencies with --no-install-project, so there is
no honcho distribution for the importlib.metadata fallback to find.

Both lookups fail, so the service falls back to reporting its version as
"unknown" in the OpenAPI schema and in telemetry events.

Copying the file into the runtime stage restores an accurate version.
The file is under 4 KB, so the image size is unchanged.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 17:35:04 -04:00
ajspig cccfa988f8
Abigail/embedding OpenAI base url (#1068)
* fix(cli): write embedding base url in --setup

* feat(cli): surface local stack on the welcome screen
2026-08-25 17:14:53 -04:00
Erosika 0b9ae00170 feat(docs): PostHog loads only with a granting consent answer
DEV-2465 open question 5, option d. Mintlify's built-in integration
loaded PostHog unconditionally on all 249 docs pages — a visitor who
declined on the homepage was tracked one click later in the docs. The
integration key comes out of docs.json; docs/posthog-consent.js loads
PostHog directly instead, only when the CookieConsent cookie grants
Statistics (or holds Cookiebot's -1 marker), and listens for the
consent events so a grant on the docs banner itself loads it too.

Trade recorded on the ticket: this bypasses the ph.mintlify.com proxy,
so ad blockers reduce docs PostHog volume. Verify after deploy that
Mintlify's page CSP allows us-assets.i.posthog.com; if it blocks,
fall back to option c.
2026-08-25 17:01:47 -04:00
Erosika b7bcb32738 feat(docs): load the GTM container on every docs page
DEV-2465 step 1. Mintlify injects gtm.js on all docs pages; the
container is audited to be inert on /docs before this merges, so the
snippet loads and nothing fires. Cookiebot and GA4 arrive later as
container publishes, consent first.

Merging this publishes the docs within minutes, so it stays unmerged
until Marc confirms the container audit.
2026-08-25 16:44:37 -04:00
Aakash Kattelu 9e60f73c7f
release: add changelog and version updates (#1069)
* chore: add changelog and version updates

API: 3.0.12 -> 3.1.0
Python/TS SDK: 2.3.0 -> 2.4.0
CLI: 0.1.3 -> 0.1.3 (updated docs)

* docs: add scopes to README architecture and fix changelog prefix

---------

Co-authored-by: ajspig <dragon@monstercode.com>
2026-08-25 16:25:31 -04:00
Eugene Eisenstein 2f7658577e
fix(scopes): scope observer sessions in SQL instead of a fetched name list (#1065)
`get_observation_context` resolved scope by fetching every session name the
observer has a membership record in, then expanding that list into
`session_name IN (...)` twice in one statement — once in the CTE and once in
the outer select. That puts psycopg's 65535-bind-parameter ceiling at roughly
32,765 sessions, and the count only ever grows: the loose membership
definition (`active_only=False`) counts sessions the peer has since left, so
leaving a session does not shrink the scope. A workspace with tens of
thousands of sessions for one peer produced a statement the driver could not
serialize at all.

Two new helpers in `crud.message` express the observer half as a correlated
EXISTS over `session_peers`. Scope now costs two bind parameters regardless of
membership size, and the membership query disappears (two round trips become
one). The `session_peers` primary key is `(workspace_name, session_name,
peer_name)`, so the correlated probe is an exact-match index hit.

The caller-supplied allowlist stays an IN clause — it is route-capped at 1000
entries and carries none of the unbounded-growth risk. `resolve_session_scope`
is left in place: three other callers still need the materialized list,
including `_search_messages_external`, which sends session names to the vector
store as a filter payload and cannot take SQL.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 13:24:31 -04:00
ajspig 2dbc25093d
chore: bump honcho-cli to 0.1.3 (#1067) 2026-08-25 12:53:12 -04:00
Rajat Ahuja 99a06baf29
perf(cache): hash-tag the cache namespace so an instance uses one shard (#1058)
On Redis Cluster a key's slot comes from the substring inside the first
{...}, when one is present. Untagged, one deployment's keys spread over
every slot, so its client opens and holds a connection to every node in
the cluster. Wrapping the namespace in braces puts them all on one slot,
and therefore one node, cutting each deployment's connection count to
the cluster by a factor of the shard count. Namespaces still hash
independently of each other, so keys stay spread across the cluster and
no shard becomes a hotspot.

The tag needs two spellings, because the two ways a key gets built treat
the string differently. cashews runs `prefix=` through format
substitution, so braces have to be doubled there to survive as literals;
keys built by concatenation need them single. A single brace passed to
cashews is read as an empty substitution field and the namespace is
dropped entirely, which would let two deployments collide on one key --
hence two clearly named helpers rather than one string, and a test that
the two paths produce identical bytes.

No key format change for a non-cluster backend, and no migration: the
old keys simply age out by TTL.
2026-08-25 12:34:42 -04:00
ajspig 5531ff0fee
Running Honcho Locally via Honcho CLI (#1029)
* feat(cli): add honcho start/stop/status for a local Docker stack

* feat(cli): fix status command

* feat(cli): improving how we pull docker images and writing a config,toml

* feat(cli): add honcho start --setup wizard for local stack config

* feat(cli): cleaning up unnecessary func, and error throwing

* feat(cli): minor clean up in stack.py

* feat(cli): read setup wizard defaults from the image config.toml

* feat(cli): cleaning up unused commands

* feat(cli): adding ignored docker-compose.yml

* feat(cli): forward host LLM env into honcho start

* feat(cli): share start/stop progress helpers via output.py and cleaning up language
2026-08-25 12:31:34 -04:00
Aakash Kattelu ac67017a18
fix(dialectic): revamp workspace and pair chat system prompts (#1066)
Teach both agents what Honcho, peers, and the harness are instead of comparing them to each other. Render only the tools the request actually offers, and drop the pair prompt's call to a write tool that is not in the loadout.
2026-08-25 12:30:06 -04:00
Aakash Kattelu 4492f66bca
fix(crud): preserve joined_at for active session peers (#1059)
* fix(crud): preserve joined_at for active session peers

Re-adding an already-active peer no longer advances the membership
window, so peer_perspective search keeps messages from the original
join. Genuine rejoins still start a new window.

* docs: document set_peers membership window and wrap test docstrings

* fix: preserve session observer limit

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-08-25 10:51:59 -04:00
Ken Weiner 5823f0fae9
fix: only classify genuine oversize input as a token-limit error (#791)
Callers wrapped every ValueError from the embedding client in a
"exceeds maximum token limit" message, so provider and configuration
failures (dimension mismatch, empty response, upstream error) surfaced
to users as though their input were too long.

Add EmbeddingTokenLimitError, raised only by the pre-flight token checks
in embed() and simple_batch_embed(), and narrow the remaps in search.py,
agent_tools.py, document.py and representation.py to catch it. It
subclasses ValueError so existing broad handlers keep working.

Both simple_batch_embed() remap sites pass on_oversize="truncate" and so
could never raise a token-limit error at all; their handlers only ever
mislabelled provider failures.

Fixes #568

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 09:45:01 -04:00
Phil cece51c079
Merge pull request #1055 from plastic-labs/phil/db-connection-metrics
feat(telemetry): add physical DB-connection metrics (visible under NullPool)
2026-08-25 07:46:01 -04:00
Vineeth Voruganti cf07068d95
Vineeth/dev 2418 (#1057)
* chore: update issue templates and docs

* chore: slim bug/quality forms and add integration template

Drop high-friction required fields from bug and quality issue forms.
Add an integration-request form (app stores, plugins, frameworks) labeled
integration, and point contributing docs at it.

* chore: route security mail to support@ and polish docs intake

Use support@honcho.dev for private vulnerability email. List the
documentation template in contributing guides, rename Media prove,
and add public-issue redaction/security redirects on the docs form.

* fix: address render issue in templates and add version field

* feat(docs): Initial draft of new contributing policies

* feat(ci): defer issue-gate closes to a scheduled sweeper

Addresses review feedback on #1041.

The gate now reads GitHub's resolved closing references
(closingIssuesReferences) instead of regex-parsing the pull request body,
so an issue linked through the sidebar Development panel counts, and a
bare `#123` mention no longer does.

It also no longer closes on the pull request event. It labels and
explains; pr-sweeper.yml re-checks every six hours and closes only what is
still failing 72 hours after the notice. That re-check is load-bearing:
linking an issue via the sidebar fires no webhook, so an event-only gate
could never observe a contributor complying that way. The sweeper also
closes drafts from outside the org after 30 days.

The shared check lives in .github/scripts/issue-gate.js so both workflows
run identical logic, with a dependency-free self-check wired into static
analysis. Its one regression guard: author_association CONTRIBUTOR stays
gated, since GitHub assigns it to anyone who has previously committed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(codeowners): drop the third reviewer from most areas

Discussed with @akattelu. Also reassigns SECURITY.md to @Rajat-Ahuja1997
and strips trailing whitespace from the deployment block.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(v2): port the issue gate policy into the v2 contributing guide

The v2 guide is still published (v2.5.1 in docs.json) but carried no
mention of the issue gate, so a contributor reading it would not learn
that a pull request needs an approved issue until the bot labelled theirs.

Ports the policy, both linking routes, and the gate's place among the
automated checks, keeping the v2 guide's own structure and unwrapped
prose rather than importing the v3 rewrite wholesale.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(ci): count only bot-authored gate notices, share the exemption list

Two review findings on the issue gate, with a common root cause.

MARKER is an invisible HTML comment, so anyone who can comment on a
public repository can paste it. findNotices accepted any comment
containing it, so a third party could post one on someone else's pull
request: runGate posts a notice only when none exists, so the author
would never be told, and runSweep would then measure the 72-hour grace
window from the stranger's timestamp and close them unwarned. Notices now
require bot authorship.

The stale-draft sweep re-listed the gate's exemptions and had lost the
bot case, so a bot's long-lived draft was closable despite checkGate
exempting bots. Both callers now share one exemptReason(pr) rather than
keeping parallel lists that drift.

Not changed: closingIssuesReferences(first: 20) truncation. It needs a
pull request with 21+ closing references where only a later one carries
the label, and the outcome would be a label plus the grace window, not a
close.

Coverage goes 11 -> 20 cases, including the stale-draft close path, which
had none. Both fixes were confirmed to fail their tests when reverted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Aakash Kattelu <aakash@plasticlabs.ai>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 17:27:02 -04:00
Aakash Kattelu da4b3ee435
chore: update issue templates and docs (#1032)
* chore: update issue templates and docs

* chore: slim bug/quality forms and add integration template

Drop high-friction required fields from bug and quality issue forms.
Add an integration-request form (app stores, plugins, frameworks) labeled
integration, and point contributing docs at it.

* chore: route security mail to support@ and polish docs intake

Use support@honcho.dev for private vulnerability email. List the
documentation template in contributing guides, rename Media prove,
and add public-issue redaction/security redirects on the docs form.

* fix: address render issue in templates and add version field

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-08-24 16:26:00 -04:00
adavyas c73f6a0b7a
feat: Add workspace-level chat (#931)
* Add workspace-level chat (DEV-1326)

POST /v3/workspaces/{workspace_id}/chat: agentic dialectic over the whole
workspace instead of a single (observer, observed) pair. Salvaged from
plastic-labs/honcho#373 and re-grown on today's DialecticAgent:

- WorkspaceDialecticAgent subclasses DialecticAgent via four new seams
  (_get_tools, _create_tool_executor, _prefetch_intro, _trace_name) instead
  of a base-class extraction; observer/observed use empty-string sentinels.
- Routing-accelerated prefetch: workspace stats + top-5 active peers with
  their self peer-cards (pure DB, ~7ms measured) so routing-obvious queries
  resolve without a discovery tool round.
- Observation search stays pair-scoped (matches per-pair vector namespaces;
  avoids workspace-flat top-k dilution): search_memory/get_peer_card take
  observer/observed as tool arguments, with pair attribution in results.
- workspace_chat / workspace_chat_stream orchestrators, WorkspaceChatOptions
  schema (scope param seam left for the #897 scopes facade), SSE streaming,
  structured output via response_format.
- crud: get_workspace_stats, get_active_peers; format_documents_with_attribution.
- SDKs: Python Honcho.chat/chat_stream + HonchoAio mirrors; TypeScript
  honcho.chat/chatStream.
- 46 tests (route, orchestrator preflight, tool handlers, executor routing,
  attribution formatting) + unified test cases + docs.

Co-Authored-By: doria <93405247+dr-frmr@users.noreply.github.com>
Co-Authored-By: Benjamin McCormick <docterformer@protonmail.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: type SSE stream wrapper as AsyncIterator (basedpyright)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: silence unused db_session fixture warnings (basedpyright failOnWarnings)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: drop docs changes from this PR (defer to follow-up)

Restores docs/v3/documentation/features/chat.mdx to main's version. This
also puts back the peer-chat Structured Outputs section (#896) that the
workspace-chat commit removed as a rebase artifact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: workspace message tools deny-all under rebased session scoping

The #882 rebase changed the unscoped-observer contract from falsy to
'observer is None': resolve_session_scope looked up the workspace
executor's observer='' sentinel as a real peer with no session
memberships and denied every workspace-flat message read (search, grep,
date-range, temporal, observation context) whenever no session was
pinned — the primary workspace-chat shape. Normalize the sentinel to
None at the five read-handler crud boundaries and add regression tests
that run the tools unpinned (verified to fail without the fix).

Also from review:
- wrap the workspace prefetch in the same degrade-to-None protection
  the base agent has (an overview query error no longer 500s the
  request or kills the SSE stream after headers)
- thread session_allowlist through create_workspace_tool_executor so
  the agent-level allowlist seam is honored end to end when scopes
  (#897) wire it up; allowlisted grep is covered by a test
- deterministic name tie-break in get_active_peers ordering

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* review: SDK response_format parity, shared query sanitizer, annotations

- TS SDK: WorkspaceChatParams gains response_format; _workspaceChat/
  _workspaceChatStream consume the shared interface instead of inline
  duplicates; chat/chatStream expose responseFormat.
- Consolidate the three identical sanitize_query validators into one
  NulStripped annotation.
- workspace_chat_stream: return annotation + full docstring.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* review: fold active peers into workspace stats; trace + query bounds

- Merge get_active_peers into get_workspace_stats (one discovery round
  instead of two); minimal loadout keeps a discovery tool via the merged
  stats tool. Fixed top-10 by recent activity; deeper discovery routes
  through search_messages.
- get_active_peers CRUD now aggregates over a trailing 90-day window so
  the chat-path prefetch never scans a workspace's full message history.
- Workspace agent inherits the "dialectic_chat" trace name; scope stays
  distinguished by agent_type/track_name (workspace name was already in
  telemetry context).
- Prefetch failure logs carry workspace + traceback; prompt no longer
  contrasts against a peer-level agent the model has no concept of;
  drop ticket identifiers from comments.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: add `scope` to workspace chat and exclude scope peers from stats

Workspace chat is peer-unanchored, so `scope` is always a session-union
allowlist (single name or list), fail-closed when empty. Stats and
active-peer prefetch drop scope-kind peers and honor the same allowlist.

* test: teach the unified runner `workspace_chat` and parse every case

QueryAction now accepts target=workspace_chat (SDK path, including
scope). A pytest over tests/unified/test_cases/*.json keeps the four
existing workspace-chat cases — and a new scoped one — from rotting
against the schema again.

* docs: tighten workspace-chat scope docs and judge prompt

Scoped workspace_chat uses the SDK, not raw HTTP. The scope fixture's
judge now requires the in-scope tea fact, not merely the absence of the
leak. format_sse_stream matches the peer-chat one-liner.

* fix(dialectic): restore the empty-memory fallback for workspace chat

`search_memory` auto-searches messages when a pair has no observations,
but the gate only admitted `agent_type == "dialectic"`. The workspace
executor passes `workspace_dialectic`, so workspace chat got a bare
"No observations found" and answered that it knew nothing rather than
falling through to message search.

Also fixes the two unified cases that never ran: `deriver` is not a
field on `WorkspaceConfiguration`, so both aborted at load with
`extra_forbidden`. `workspace_chat_scope` additionally enables reasoning,
since it asserts scope isolation and has no reason to depend on the
fallback path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(tests/unified): fail CI when unified tests fail

`runner.run()` tallied failures into `failed_count` and printed them, but
returned nothing, and both entrypoints ignored the result. The workflow
invokes `python -m tests.unified.run` bare, so the job has gone green on
failing and unrunnable cases since it was wired up in #291.

Return the count and exit non-zero on it. `INVALID SCHEMA` already counts
toward the tally, so a malformed case now fails the job instead of being
skipped silently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(unified): assert scope peers stay out of workspace chat answers

Scope peers are real peer rows, so a regression in the `scope_peer_clause`
exclusion would surface `scope.therapy` through workspace stats or the
routing prefetch. Nothing asserted against that.

Adds the check to the existing scoped query and a new unscoped one, since
the two exercise different `get_active_peers` branches. Verified by
removing the exclusion, which fails the unscoped query.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: Remove dead code references

---------

Co-authored-by: doria <93405247+dr-frmr@users.noreply.github.com>
Co-authored-by: Benjamin McCormick <docterformer@protonmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Aakash Kattelu <aakash@plasticlabs.ai>
Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-08-24 15:54:23 -04:00
Phil 3f0ba03d4e review: fix db_connections_open leak on the detach path; comment/doc polish
Bug (verified vs SQLAlchemy 2.0.49): on GC-cleanup of an abandoned async connection, _finalize_fairy routes through fairy.detach(), which nulls the record's dbapi_connection so NullPool's close is a no-op (the `close` event never fires) then emits `detach` with the record. Listening only to close/invalidate left the marker unpopped, so db_connections_open leaked upward and never reset until restart. Listen to `detach` too — it carries the ConnectionRecord and the marker dedupes, so exactly one decrement occurs.

Also: strip a bare PR-number provenance tag from the test docstring (plastic-labs comment-reconciliation rule); disambiguate db_connections_open from the existing db_pool_connections; note in initialize_bounded_metrics that DB-instrumentation metrics zero-init in their registrar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-24 14:37:20 -04:00
Phil fbb4a8ef0c feat(telemetry): add physical DB-connection metrics
Add db_connections_open (gauge) and db_connections_established (counter), driven by SQLAlchemy connection-lifecycle events so they report real connections under every pool class — including NullPool, where the pool-object collector (db_pool_connections) reads zero. Under NullPool the establishment rate approximates request rate.

DBConnectionTracker mirrors DBQueryInflightTracker: a ConnectionRecord.info marker makes each physical connection increment once and decrement at most once (no leak, no negative). Registered per-process in the API and deriver; zero-init via the pre-resolved labeled children, matching db_queries_in_flight_gauge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-24 14:35:51 -04:00
Serhii Zghama 3e73c6f287
fix(filter): make ne on jsonb metadata keys null-safe (#1036)
* fix(filter): make ne on jsonb metadata keys null-safe

* test(filter): cover null-safe ne on nested metadata keys

* test(filter): count actual rows for nested-metadata ne null-safety

Compile-only checks lock the operator map entry but don't catch wrong
row sets under three-valued logic. Adds a live messages/list case with
a message missing the key and one with empty metadata, following the
scalar-column pattern in test_negation_includes_conclusions_with_no_session.

* test(filter): type message_configs with a TypedDict

basedpyright couldn't narrow the heterogeneous metadata dict literals,
so indexing message_configs["content"] came back partially unknown and
broke the sorted() calls under type checking.
2026-08-24 09:33:08 -04:00
Aakash Kattelu ddbb90e36f
fix(embedding): truncate in batch embed and return results breakdown (#1019)
* fix(deriver): truncate oversize observations so one cannot drop the batch

simple_batch_embed raised ValueError when any input exceeded the per-input
token cap, which failed the entire deriver save when a single observation
was over-length. Add on_oversize="truncate": oversize inputs are embedded
from a token-capped prefix (re-encoded until it fits, with a warning),
preserving one vector per input. Default stays "raise" so existing callers
are unchanged. RepresentationManager opts into truncate.

Also add a live embedding test that fails on main (raise / missing kwarg)
and passes once a mixed short+oversize batch survives.

Refs #569

* fix(deriver): surface failure when all observer saves fail

When every observer's save_representation failed (e.g. embedding retries
exhausted under a sustained 429), the deriver logged the error and returned
normally, so the queue marked the work unit processed with zero documents
saved. Collect per-observer errors and, after telemetry is emitted, raise
RepresentationSaveError when no observer succeeded. Partial failures stay
processed (saved observers must not be discarded) and are recorded via an
additive failed_observer_count on RepresentationCompletedEvent.

Refs #728

* fix(embedding): guarantee truncation progress and truncate on re-embed

The retry slice in _truncate_to_token_limit always recomputed the same
keep count, so a slice whose re-encode grew past the cap could oscillate.
Decrement keep after each unsuccessful retry.

Document re-embed in the reconciler used the default on_oversize="raise",
so one oversize document failed every other document in the batch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore: drop ticket ids and shrink comments to one sentence

Comments and docstrings describe current behavior, not the PR that
introduced them. Ticket numbers stay in the commit/PR.

* chore: annotate RepresentationSaveError and assert truncate on re-embed

* fix(embedding): truncate on conclusion create paths and document BPE loop

Storage callers in create_observations (API + agent tools) now pass
on_oversize="truncate" so a single oversize item cannot drop the batch.
Docstring on _truncate_to_token_limit notes why decode/re-encode is load-bearing.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 11:42:38 -04:00
Daniel Peng 67f4dbf23f
fix(llm): preserve reasoning content across tool turns (#1034) 2026-08-20 11:12:47 -04:00
Phil 4797489281
telemetry: zero-initialize bounded-label metrics so an absent series means a broken scrape (#927)
* telemetry: materialize dropped-event counter children at 0

A labeled Prometheus counter exports no series until its first labels()
call, so telemetry_events_dropped stayed invisible until an event was
actually dropped — impossible to alert on or graph, and "no drops" was
indistinguishable from "metric missing / scrape broken".

Pre-create the (namespace, reason) children at 0 on emitter start, for
each reason the emitter can emit, so the metric is always present.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* telemetry: generalize counter zero-init to all bounded-label counters

Extends #927 (which zero-inited telemetry_events_dropped) to every counter
whose label domain is bounded and known at startup, so metrics are present in
Prometheus before their first event — a missing series then signals a broken
scrape rather than "nothing happened yet".

- add initialize_bounded_metrics(instance_type) on PrometheusMetrics; call it
  per-process from main.py (api) and deriver/__main__.py (deriver).
- extract a shared _touch() helper; refactor initialize_telemetry_dropped_metrics
  onto it (that one stays per-emitter in start() — it's prefix-dependent).
- explicit ALL_EVENT_TYPES / HIGH_VOLUME_EVENT_TYPES registry in telemetry.events,
  drift-guarded by tests that walk BaseEvent subclasses.
- only VALID (task_type, token_type, component) tuples for deriver_tokens (the
  cartesian product would fabricate impossible always-0 series); only high-volume
  event types for sampled_out; high-cardinality labels (endpoint, workspace_name)
  left open.
- gauges: zero-init embed_now_tasks_in_flight + telemetry_buffer_size; add a new
  message_embeddings_pending backlog gauge, set each reconciliation cycle and
  zero-inited at deriver startup (Rajat's pending/in-flight ask).
- backfills the tests #927 shipped without.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* review: task-aware deriver combos + fail-soft gauge zero-init

I1: _DERIVER_TOKEN_COMBOS was factored task-independently, materializing the
impossible (ingestion, input, previous_summary) series — previous_summary is
summary-only. Make combos task-aware (_DERIVER_TOKEN_COMBOS_BY_TASK) so no
always-0 impossible series is fabricated, matching the PR's own goal. Tests
tightened to assert the ingestion/previous_summary series is absent.

I2: the three gauge .set(0) zero-inits were bare while the counter inits go
through the fail-soft _touch. Add _set_gauge_zero() so a gauge init can't
propagate an exception into process startup either.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(telemetry): isolate zero-init namespaces, add deriver-to-api guard

Global-REGISTRY assertions used a fixed "test" namespace, which several
other suites also pin, so another test's materialized children could
satisfy a presence assertion or break an absence one. Each test now runs
under a unique namespace resolved from settings at read time.

Adds the inverse per-process isolation test: deriver-only init must not
materialize API-only series (dialectic tokens, embed_now).

Co-Authored-By: Claude <noreply@anthropic.com>

* review: per-replica backlog gauge, drop duplicated constants and .meta refs

Addresses Vineeth's review on #927.

Blocking:
- message_embeddings_pending is a DB-global count, so drive it from
  ReconcilerScheduler._scheduler_loop (runs on every replica, every
  interval) instead of run_vector_reconciliation_cycle (runs off the
  queue behind work-unit dedup, so one replica per cycle). Combined with
  the zero-init, the old placement made every replica that never won the
  work unit export a confident permanent 0. Help string now names the
  owner so dashboards don't reach for sum().
- guard initialize_telemetry_dropped_metrics on METRICS.ENABLED,
  matching its sibling initializer.
- drop the duplicate REASONING_LEVELS; import the one in src/config.

Non-blocking:
- walk BaseSpecialist recursively via a shared utils.types.walk_subclasses
  (replaces the direct-children-only __subclasses__() and the test's
  private copy of the same helper).
- derive the specialist assertion from the subclasses instead of
  hardcoding two names — the hardcoded pair kept passing after
  CardRefreshSpecialist landed, leaving it uncovered.
- inline the zero-init rationale and the multi-instance bucket taxonomy;
  removes both pointers to a .meta design doc that is not in the repo.

Tests: new tests/reconciler/test_pending_backlog_gauge.py pins both
halves of the relocation (verified it fails when reverted).

* review: fix inert test guard, stale comments, and the REASONING_LEVELS drift claim

Second review pass on the branch. Findings, most severe first:

- tests/reconciler/test_pending_backlog_gauge.py: the _try_enqueue_task stub
  was patched onto the class but declared without `self`, so calling it
  raised TypeError — which _scheduler_loop swallows. The guard was inert and
  the test passed for the wrong reason. Fixed the arity.

- metrics.py still commented that the backlog gauge is "set live each
  reconciliation cycle". That is the exact claim the previous commit
  overturned; it now contradicted the help string, the bucket-3 docstring
  and sync_vectors.py.

- metrics.py claimed REASONING_LEVELS is "derived from the config Literal so
  it never drifts", but config.py hand-listed it, so the earlier dedup had
  quietly traded away the guarantee the original get_args() call provided.
  Made it true instead: config.REASONING_LEVELS = list(get_args(...)), which
  keeps the dedup and restores the invariant.

- dropped _set_gauge_zero: all three gauges it zeroed already have identical
  fail-soft setters, so it was a second way to do one thing. Using the
  setters also makes _handle_metric_error name the actual gauge.

- record_pending_embeddings_backlog's docstring oversold the covering index
  as making the COUNT "negligible". The index makes cost proportional to the
  pending backlog, not to the table — which is worst precisely when the
  backlog matters. Stated honestly.

- _scheduler_loop's docstring said it only enqueues; it also refreshes the
  gauge, at a cadence set by the shortest task interval.

- comment reconciliation: stripped #927 / "the generalization" temporal
  anchoring, a CardRefreshSpecialist change-narration clause, and
  reviewer-directed phrasing from the test file; disambiguated the
  src/utils/summarizer.py path.

- CLAUDE.md had no Prometheus section at all, so the new "add a BaseEvent
  subclass -> update ALL_EVENT_TYPES" obligation and the never-sum() rule
  for non-additive gauges were undiscoverable from the architecture doc.

Verified: ruff + basedpyright clean (0 errors), tests/telemetry + reconciler
+ dialectic + llm 497 passed, full suite 1768 passed with only the 4
pre-existing test_document failures (OpenAI key required, reproduced on
clean origin/main). Re-confirmed the relocation guard fails when reverted.

* fix: silence the two basedpyright warnings inherited from main

CI runs `uv run basedpyright` bare, and basedpyright exits non-zero on any
warning — so these two have been failing the staticanalysis job on every
branch cut from current main, not just this one:

- src/vector_store/__init__.py:209 implicit string concatenation (#496)
- tests/test_cache_redaction.py:5 private import (#869)

Both predate this branch and are unrelated to the telemetry work; fixed
here only because they block this PR from going green. Verified: clean
origin/main also reports "0 errors, 2 warnings" and exits 1.

basedpyright now 0 errors, 0 warnings, exit 0.

* docs(telemetry): make the bucket-3 aggregation rule precise

The multi-instance taxonomy said a service-scoped non-additive metric has
"no aggregation correct once they disagree", then immediately mandated that
every instance refresh on its own timer. Those undercut each other: staggered
timers ALWAYS disagree slightly, so as written the rule reads as "ensure they
don't", which is unachievable, and it leaves the reader unsure whether max()
and avg() survived the fix.

The actual rule is bounded disagreement plus a scale-preserving aggregator.
Instances are N witnesses to one fact, not N parts of one whole, so sum() can
never be correct (it scales with replica count) while max()/avg()/quantiles
are correct precisely because the per-instance timer bounds the spread.

Wording only; no behavior change. The gauge help string already said
"max() or avg(), never sum()" — this makes the normative docstring agree
with it. Surfaced walking Vineeth's comment 3668208059 for comprehension.

* refactor(bench): import REASONING_LEVELS from config instead of re-listing

Third copy of the constant, missed when ee781c0/694e07f deduped the other
two. This one re-declared the ReasoningLevel Literal as well as the list,
so the type alias could diverge from config's with nothing to catch it —
and the list was hand-written, the variant that typechecks clean while
missing a member.

No import barrier justified it: this module already imports from src, as do
seven of its siblings in tests/bench. Concrete effect of the drift was that
a newly added sixth reasoning level would be rejected by the bench CLI's
argparse choices=.

src.config.REASONING_LEVELS is now the single definition repo-wide.

* test(telemetry): pin the METRICS.ENABLED guard on the per-emitter initializer

initialize_telemetry_dropped_metrics gained a METRICS.ENABLED guard in
ee781c0, addressing Vineeth's asymmetry comment, but nothing asserted it —
it had only the enabled half of the pair its sibling has. Deleting the guard
left the suite green, so the fix closed the asymmetry in the guards and
reproduced it one level up in the tests.

Mirrors test_init_noop_when_metrics_disabled. Verified live rather than
assumed: deleting the two guard lines turns this test red.

Uses a unique namespace, without which the absence assertion would be
satisfied by the enabled test's children rather than by the guard.

* docs(telemetry): fold zero-init why-prose behind # region ai markers

Comment/docstring-only pass over the changed files, per the groudon
comment-marker standard: the terse human-facing "what" stays visible, and
load-bearing "why" (the zero-init / absent-series-means-broken-scrape
rationale, gotchas, receipts) folds into # region ai / # ai: blocks.

Behavior-preserving: AST-identical modulo docstrings/comments vs the
pre-pass merge; ruff, ruff format --check, and basedpyright all clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-20 10:24:49 -04:00
Aakash Kattelu 7bafee5de1
chore: add pr template and pre-pre skill (#1031) 2026-08-19 16:18:47 -04:00
Serhii Zghama bd5fd4df62
fix: honor DERIVER_DEDUPLICATE in create_observations (#1018)
* fix: honor DERIVER_DEDUPLICATE in create_observations

The agent-tool path hardcoded deduplicate=True, so DERIVER_DEDUPLICATE=false
could not disable dedup for observations created through this path. Pass
settings.DERIVER.DEDUPLICATE, matching crud/representation.py.

* test: cover deduplicate setting is forwarded in create_observations
2026-08-19 13:57:49 -04:00
ZiYun Liu a915d298f2
fix: force LF line endings for shell scripts (#1017)
docker/entrypoint.sh runs under dash inside the container image. A Windows checkout with core.autocrlf=true rewrites it to CRLF, and dash aborts at startup because the carriage return becomes part of the -e flag argument.
2026-08-19 11:57:20 -04:00
Vineeth Voruganti c2d8cf3a72
Scopes SDK Changes (#1030)
* feat: scopes SDK surface and session allowlist on session context

Exposes the Scopes v1 facade in both SDKs, which until now was reachable
only by hand-rolled HTTP, and closes the Phase 1 gap where the session
allowlist never landed on the context route.

SDKs (DEV-2001, folds in DEV-1996)

  New Scope class in both SDKs — addSessions / removeSession / sessions /
  status — plus honcho.scope() and honcho.scopes() entry points, a
  `scopes` option on session creation, and `scope` + `sessions` read
  options on chat, chatStream, representation, and session.context.
  `scope` on workspace search. Python covers sync and .aio equally.

  `sessions` is sugar, not a new wire field: on the recall endpoints it
  goes out as the constrained `filters: {session_id: [...]}` body, never
  as a key of its own. Kept separate from the `filters` parameter on the
  list/search methods on purpose — that one is the full filter DSL,
  whereas the recall endpoints accept a single key and 422 on anything
  else, so one name for two grammars would be a trap.

Server (DEV-2357)

  `GET /sessions/{id}/context` accepts a `sessions` allowlist confining
  the target's representation. Two deliberate choices worth review:

  - Sent as a repeated query parameter rather than the `filters` body the
    issue specced. The route is a GET and `session_id` is the only
    supported key, so a JSON blob in a query string buys nothing.
  - The peer card is omitted under an allowlist. Cards key on
    (workspace, observer, observed) with no session dimension, so they
    cannot be narrowed; returning one would leak exactly what the
    allowlist exists to exclude. Same reasoning as ALLOWLIST_SAFE_LEVELS.
    `scope` needs no carve-out — it swaps the observer to the scope peer,
    so the card read is the scope's own.

  `extract_session_allowlist` now delegates to a shared
  `normalize_session_allowlist`, so the cap, id charset, and must_include
  rule have one implementation across both entry points. Existing error
  messages are unchanged.

Also in here

  - ConclusionScope renamed to ConclusionsView in both SDKs. "Scope" now
    means a named set of sessions, which that class is not — it is a view
    over one observer/observed pair. ConclusionScope kept as a deprecated
    alias; the package-level import path only.
  - The TS HTTP client comma-joined array query params, so any list-valued
    parameter arrived as one malformed entry. Fixed at buildURL rather
    than the call site.

Not in this PR: the "How Scopes Work" docs guide and the Groudon
dashboard tab (both DEV-2001), and CHANGELOG entries for Phases 2a-2c,
which are still merged-but-unrecorded.

Verified: ruff, basedpyright, tsc --noEmit, biome all clean. New unit
tests cover the SDK option translation and the Scope client, but the
context route's own behavior — the 422s, the 401 membership gate, the
dropped peer card — has no test yet; the analogous chat/representation
cases in tests/test_session_allowlist.py are the place for it.

Refs DEV-2001, DEV-1996, DEV-2357

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
  Exposes the Scopes v1 facade in both SDKs, which until now was reachable
  only by hand-rolled HTTP, and closes the Phase 1 gap where the session
  allowlist never landed on the context route.

  SDKs (DEV-2001, folds in DEV-1996)

    New Scope class in both SDKs — addSessions / removeSession / sessions /
    status — plus honcho.scope() and honcho.scopes() entry points, a
    `scopes` option on session creation, and `scope` + `sessions` read
    options on chat, chatStream, representation, and session.context.
    `scope` on workspace search. Python covers sync and .aio equally.

    `sessions` is sugar, not a new wire field: on the recall endpoints it
    goes out as the constrained `filters: {session_id: [...]}` body, never
    as a key of its own. Kept separate from the `filters` parameter on the
    list/search methods on purpose — that one is the full filter DSL,
    whereas the recall endpoints accept a single key and 422 on anything
    else, so one name for two grammars would be a trap.

  Server (DEV-2357)

    `GET /sessions/{id}/context` accepts a `sessions` allowlist confining
    the target's representation. Two deliberate choices worth review:

    - Sent as a repeated query parameter rather than the `filters` body the
      issue specced. The route is a GET and `session_id` is the only
      supported key, so a JSON blob in a query string buys nothing.
    - The peer card is omitted under an allowlist. Cards key on
      (workspace, observer, observed) with no session dimension, so they
      cannot be narrowed; returning one would leak exactly what the
      allowlist exists to exclude. Same reasoning as ALLOWLIST_SAFE_LEVELS.
      `scope` needs no carve-out — it swaps the observer to the scope peer,
      so the card read is the scope's own.

    `extract_session_allowlist` now delegates to a shared
    `normalize_session_allowlist`, so the cap, id charset, and must_include
    rule have one implementation across both entry points. Existing error
    messages are unchanged.

  Also in here

    - ConclusionScope renamed to ConclusionsView in both SDKs. "Scope" now
      means a named set of sessions, which that class is not — it is a view
      over one observer/observed pair. ConclusionScope kept as a deprecated
      alias; the package-level import path only.
    - The TS HTTP client comma-joined array query params, so any list-valued
      parameter arrived as one malformed entry. Fixed at buildURL rather
      than the call site

* fix(scopes): close peer-card leak under limit_to_session, harden SDK inputs

Addresses review findings on the scopes work. All four were verified by
reproducing them, not by reading.

Peer card no longer leaks under any allowlist

  The card was dropped when `sessions` was set but returned when
  `limit_to_session=true` produced the identical allowlist, so a control
  meant to fail closed was defeated by swapping one query parameter. It is
  now gated on the effective allowlist, computed once and shared by the
  representation call and the card read — the duplicated inline
  conditional is what let the two drift apart.

  `POST /peers/{id}/chat` still injects an unscoped card under an
  allowlist (src/dialectic/chat.py fetches it on peer_card.use alone, with
  no reference to session_allowlist). Left alone deliberately: that is a
  behavior change to the shipped dialectic and
Scope validation messages survive the option union

  ScopeOptionSchema is a union, and Zod collapses a failing union into one
  `invalid_union` / "Invalid input" issue, burying the branch errors. Every
  invalid scope on chat/representation reported "Invalid input" and told
  the caller nothing — including the reserved-prefix case the check order
  exists to surface. The rules are now a plain function applied after the
  union resolves, so the specific message reaches the caller for bad
  charset, reserved prefix, empty and over-cap lists alike.

Empty scope no longer fails open

  `session.context({scope: ''})` and `honcho.search(q, {scope: ''})` used
  truthiness checks, so an invalid scope was dropped and the call returned
  *unscoped* results. Both now test against undefined so the value reaches
  the schema.

Session IDs validated before reaching a URL path

  `scope.removeSession('valid-session?typo')` addressed `valid-session`
  with a stray query string: the wrong session removed, and reconciliation
  run against it. Both SDKs now validate the charset first. Python's
  `add_sessions` was unvalidated too — harmless in a JSON body, but
  leaving one path checked and its sibling unchecked is how this recurs.

Also

  - Corrected the `limit_to_session` description: it claimed "only used if
    search_query is provided", but the allowlist reaches
    _query_documents_recent unconditionally.
  - Corrected the documented 1,000-session cap on `sessions`, which is
    unreachable via repeated query params — the request line exceeds h11's
    16 KB and nginx's 8 KB defaults at a few hundred entries, giving an
    opaque 414/431 instead of a 422.
  - Removed a dead route builder.

Tests: 7 new TypeScript cases and 2 new Python classes covering all four
findings. The TypeScript unit suite passes 146/146. The context route
itself is still unexercised — the card gate and the 401 membership check
remain verified by reading only.

Refs DEV-2001, DEV-1996, DEV-2357, DEV-2201

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(sdk): align the conclusions-view error message across both SDKs

The ConclusionScope -> ConclusionsView rename updated the identifier but
not the prose inside the thrown message, which the rename pattern
(\bConclusionScope\b) does not match. TypeScript ended up throwing
"managed by this conclusions view" while Python still threw "managed by
this conclusion scope" — the same error, different text per SDK.

Three server-backed conclusions.test.ts cases assert that message by
regex and failed under `pytest -k typescript`. The four equivalent Python
assertions were passing, because they matched Python's unchanged string —
so fixing only the TypeScript tests would have made the suite green with
the divergence still in place.

Brings Python's message, comments and docstring in line with TypeScript,
and updates the assertions in both suites. `grep -ri 'conclusion scope'`
is now empty.

Verified: 64 passed across tests/sdk_typescript/, tests/sdk/test_conclusions.py
and tests/sdk/test_scope_options.py — the last of which had never been run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 10:48:29 -04:00
Joe-Kneeland 2163ab1aa3
fix(llm): forward provider_params.timeout to the OpenAI-compatible embedding client (#1024)
* fix(llm): forward provider_params.timeout to the OpenAI-compatible embedding client

#832 and #903 added a configurable request timeout for the LLM registry
and the Gemini embedding client respectively, but the OpenAI-compatible
embedding client (src/embedding_client.py) was never wired up. It
constructed AsyncOpenAI with no timeout at all, so a stalled socket
against a slow or contended OpenAI-compatible backend (e.g. a
self-hosted embedding model under load) wedges the deriver worker's
event loop indefinitely — the exact failure #785/#903 describe, just
via a code path #903 didn't cover.

EmbeddingModelConfig now carries provider_params through from
resolve_embedding_model_config, mirroring how resolve_model_config
already does it for ModelConfig, and the OpenAI branch of
_EmbeddingClient.__init__ extracts `timeout` via the existing
request_timeout_from_extra_params helper. Unset stays unset — no
existing behavior changes.

Reproduced and verified against a real self-hosted deployment (local
Ollama backend under load): before this fix, a single stuck embedding
call blocked all deriver queue processing for 20+ minutes with no
error logged, twice in one session.

* fix(embedding): use first-class timeout on embedding model config

provider_params is the LLM per-request escape hatch; embedding timeouts are
client-construction knobs and belong next to max_batch_size. Wire the field
for OpenAI and Gemini, omit the OpenAI kwarg when unset so the SDK default
stays, and keep Gemini's 10-minute floor when unset.

* test(embedding): live coverage for first-class embedding timeout

Exercise EmbeddingModelConfig.timeout on one representative OpenAI and
Gemini model: configured timeout lands on the SDK client, and a near-zero
timeout aborts before the provider answers.

---------

Co-authored-by: Aakash Kattelu <aakash@plasticlabs.ai>
2026-08-18 10:51:53 -04:00
Aakash Kattelu f88892b071
fix(deriver): update prompt to prevent example leakage (#1028)
* fix(deriver): delimit examples and forbid example leakage

Wrap the minimal deriver EXAMPLES block in <examples> tags and add an explicit negative instruction: the examples are fabricated format illustrations only, and every conclusion must be grounded in the <messages> block. Soft mitigation for example-content leakage; real fixes (sources_indices + SFT / structural enforcement) are follow-ups.

* test(deriver): drop low-value examples-delimiter unit test
2026-08-17 14:49:15 -04:00
ajspig 444897975c
docs: updating claude-codes with recent changes (#1021) 2026-08-14 17:39:14 -04:00
Ulysse Pence 16f490e345
fix(deriver): increase deriver polling backoff (#1015)
* fix(deriver): increase deriver polling backoff

* removes comment

* Removes config changes
2026-08-14 16:38:20 -04:00
Vineeth Voruganti 9379c634ed
feat: scope backfill-by-copy and removal reconciliation jobs (#904)
* fix: enforce explicit-document session purity in dedup/merge paths

Audit for DEV-2000 (Scopes RFC prerequisite): explicit-level documents must
stay session-pure so scope memory can be built by copying explicit documents
between collections. Two classes of violation were possible:

- Exact-content and semantic dedup in crud/document.py matched candidates
  with no level or session scoping, so an explicit document could be
  reinforced by — or soft-deleted in favor of — a same-content document from
  a different session or a different level (silently merging cross-session
  derivations into one row).
- The generic create_observations tool handler accepted level='explicit'
  from agents with no message context (dreamer/dialectic), which would mint
  session-less explicit documents.

Enforcement (refuse, never rewrite):
- create_documents refuses explicit documents with a null session_name
- exact dedup keys on (content, level, session-for-explicit); derived levels
  keep cross-session consolidation
- is_rejected_duplicate scopes candidate search to the same level, and the
  same session for explicit documents
- the create_observations tool rejects explicit-level input outside message
  ingestion (deriver) context

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: add card_refresh dream type for event-driven peer-card updates

Adds a lightweight dream variant (DEV-2000, Scopes RFC prerequisite) that
runs ONLY the peer-card update — for event-driven refreshes such as scope
membership changes and cold starts:

- DreamType.CARD_REFRESH alongside OMNI; dispatched by process_dream to a
  new run_card_refresh_dream orchestration
- CardRefreshSpecialist: restricted to get_recent_observations,
  search_memory, and update_peer_card (no observation-mutating tools), with
  a low tool-iteration cap of min(6, DREAM.MAX_TOOL_ITERATIONS)
- rebuild=True mode carried in the dream payload: the existing card is NOT
  injected into the prompt and the specialist rebuilds it solely from
  observations present in the collection (for use after removals)
- enqueue-able via the manual enqueue_dream path (bypasses volume gates);
  the work-unit key already embeds the dream type so a card refresh never
  collides with a pending omni dream. POST /v3/workspaces/{id}/schedule_dream
  accepts dream_type=card_refresh plus the rebuild flag
- card refreshes never advance the omni dream guard pair
  (last_dream_at / last_dream_document_count)
- shared PEER CARD prompt section extracted (verbatim) from
  DeductionSpecialist for reuse; CallPurpose gains dream.card_refresh

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: reserve scope__ peer namespace with kind flag and guardrails

Introduce the scope peer namespace (scope__<name>) and the authoritative
{"kind": "scope"} configuration flag, plus the server-side guardrails:

- src/utils/scopes.py: single source of truth for the prefix, kind flag,
  and name helpers (scope_peer_name / is_scope_peer_name /
  scope_name_from_peer / validate_no_scope_peer_names)
- reject reserved-prefix names on peer get-or-create (422)
- reject scope peers as message authors in crud.create_messages (422)
- reject scope peers as chat/representation targets (422); a scope peer
  as the path-level observer is deferred to Phase 2b
- reject scope peers on the generic session-peer add/set/remove routes
  and the session-create peers mapping (422, directing to scopes routes)
- peers.list excludes scope peers by default; new PeerGet.kind option
  ("scope" | "all") switches the view via a configuration JSONB filter
- schemas: Scope / ScopeCreate / ScopeSessions(Add) and
  SessionCreate.scopes (unprefixed scope names, validated)

Part of DEV-1997 (Scopes RFC DEV-1970).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: add scopes CRUD routes and session-create scopes wiring

New /v3/workspaces/{workspace_id}/scopes facade (workspace-level auth;
peer- and session-scoped keys are rejected):

  POST   ""                                   create-or-get (201/200)
  POST   /list                                 paginated scope list
  GET    /{scope_id}                           single scope
  POST   /{scope_id}/sessions                  add memberships
  DELETE /{scope_id}/sessions/{session_id}     remove membership
  GET    /{scope_id}/sessions                  list member session ids

- crud/scope.py: get_or_create_scopes stamps the backing peer with
  {"kind": "scope", "observe_me": false} and refuses to adopt a
  legacy peer occupying the reserved name without the flag (409)
- memberships are session_peers rows with observe_others=true /
  observe_me=false — identical to a hand-built observer peer
- SessionCreate.scopes: create-or-get each scope peer and add the
  membership at session creation (the no-backfill common path)
- crud/session.py: public upsert_session_peers wrapper so the facade
  bypasses the route-level guardrails without reaching into privates

Backfill of pre-existing documents and reconciliation on removal land in
DEV-1999; membership only affects messages ingested after the change.

Part of DEV-1997 (Scopes RFC DEV-1970).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: cover scopes facade, guardrails, and observer semantics

- create-or-get idempotency, list/get, name validation, legacy-collision
  rejection (409), auth scoping (workspace key ok, peer/session keys 401)
- reserved prefix rejected on peer create; peers.list kind filtering
- scope peers rejected as message authors, chat/representation targets,
  and on the generic session-peer routes
- membership add/list/remove with observe_others=true / observe_me=false
  row shape asserted via DB, and facade-less equivalence with a
  hand-built observer peer
- end-to-end litmus: after adding a session to a scope, the deriver
  enqueue fan-out includes the scope peer as an observer
- session creation with scopes: [a, b] creates both memberships

Part of DEV-1997 (Scopes RFC DEV-1970).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: scope backfill-by-copy and removal reconciliation jobs

Retroactive scope membership changes (DEV-1999):

- New queue task types scope_backfill / scope_removal with payloads,
  work-unit keys ({task}:{workspace}:{scope_peer}:{session}), deduped
  enqueue (mirrors enqueue_dream), and consumer dispatch.
- Backfill copies a session's explicit documents from each sender's
  global (P, P) collection into the scope's (scope_peer, P) collection
  with internal_metadata.copied_from as the idempotency marker;
  soft-deleted copies from an earlier removal are restored, so
  add -> remove -> re-add converges on exactly one live copy. Completion
  enqueues one manual omni dream per touched collection.
- Removal soft-deletes the session's explicit documents in the scope's
  collections and cascades (fail-closed, transitively) to derived
  documents whose source_ids intersect anything removed, deletes the
  vectors from the external store, then enqueues a card_refresh dream
  with rebuild=True plus a manual omni dream per touched collection.
- Zero LLM re-derivation: explicit documents are session-pure (DEV-2000
  invariant); the only external call is re-embedding rows whose
  embedding column is NULL (external-store deployments).
- Per-session job status lives in the scope peer's internal_metadata
  under backfill_status, written via single-statement JSONB merges
  (concurrent-writer safe) and surfaced at
  GET /v3/workspaces/{w}/scopes/{scope_id}/status.
- Enqueued from the scopes add-sessions route and SessionCreate.scopes
  handling, only when the session already has messages.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: add scope backfill/removal test coverage, fix backfill status JSONB bug

Adds tests/deriver/test_scope_backfill.py covering the DEV-1999 scope
backfill-by-copy and removal reconciliation jobs implemented in a prior
commit: explicit-doc copying, copied_from idempotency (including
add->remove->re-add), multi-peer collection routing, removal cascade to
dependent derived docs, dream enqueues (manual omni on backfill;
card_refresh rebuild + omni on removal), the status route, and add-sessions
route wiring (backfill enqueued only when the session already has messages).

Fixes a real production bug surfaced by these tests: update_scope_backfill_status
passed json.dumps()'d strings through SQLAlchemy cast(..., JSONB), which
double-encodes (psycopg re-serializes the already-JSON string), producing a
JSONB string scalar instead of an object. Postgres's `||` between two
non-array jsonb scalars doesn't merge — it silently wraps both into a
2-element array, corrupting backfill_status into a list. This crashed
clear_scope_backfill_status's `#-` path delete (called on every removal)
with "path element is not an integer" once a session had ever completed a
backfill. Fixed by passing raw Python dicts to cast() instead, mirroring the
working pattern already used in update_collection_internal_metadata.

Also adds src.deriver.scope_backfill.tracked_db to conftest's tracked_db
patch list — the module was missing from that per-import-site allowlist, so
its DB work ran against the real configured database instead of the
isolated per-test database.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(crud): preserve cache invalidation across get_or_create retry

`get_or_create_peers` and `get_or_create_scopes` mutate existing rows, then
insert new ones inside `db.begin_nested()`. When a concurrent writer creates
one of those rows first, the insert raises IntegrityError and the function
retries.

`begin_nested()` autoflushes the pending UPDATEs *before* opening the
savepoint, so the rollback neither undoes them nor expires the now-clean ORM
state. The retry then compared already-updated values, found no change, and
dropped those peers from `changed_peers` — skipping the cache purge while the
row change committed anyway, leaving entries stale until the 300s TTL.

Carry the mutated names into the retry via `_pending_invalidation` so the
purge cannot be lost.

The scopes facade mirrors `get_or_create_peers`, so both copies carried this.
The peer path is pre-existing and runs on every message ingest.

Also add the missing /v3 prefix to `_SCOPES_ROUTE_GUIDANCE`, which pointed
callers at a 404.

Adds tests/crud/test_get_or_create_retry_invalidation.py, which drives a real
racing session and fails without this change.

* fix(scopes): make scope identity unforgeable, unblock non-pattern peer names

Three coupled changes to the scopes facade.

1. `PeerCreate` no longer gates internal lookups. It exists to validate a new,
   user-supplied peer id at the API boundary, but crud used it as a DTO for
   names that already exist, so any name outside RESOURCE_NAME_PATTERN raised a
   raw pydantic ValidationError — which is not a HonchoException, so it fell
   through to the catch-all handler as an HTTP 500. Adds `PeerSpec` (same
   fields, no charset pattern) as `PeerCreate`'s base, widens
   `get_or_create_peers` to accept it, and changes `get_peer` to take a plain
   str. All 13 construction sites converted; the create route keeps full
   validation.

   This unbreaks the Dreamer: DreamScheduler passes `collection.observer`
   straight into the specialist preflight, and scope peers have
   `observe_others=true`, so every `(scope.x, peer)` dream died there — the
   feature scopes exist to enable. It also fixes a pre-existing bug unrelated
   to scopes: a peer named `alice.smith` (legal before d429de0e5338, which
   validated names by length alone) 500s on message create, session peer add,
   and peer update.

2. The `kind` flag moves from `configuration` to `internal_metadata`.
   `configuration` is user-writable — `PeerCreate`/`PeerUpdate` accept a
   free-form dict and `update_peer` replaces it wholesale — so a legitimate
   `{"observe_me": true}` update silently dropped the flag, and a forged
   `{"kind": "scope"}` injected an ordinary peer into `POST /scopes/list`.
   `internal_metadata` appears in no API schema. `observe_me: false` stays in
   `configuration`, where it belongs.

3. Scope identity requires prefix AND flag, via `is_scope_peer()` and
   `scope_peer_clause()`. Neither half is forgeable: the prefix sits outside
   RESOURCE_NAME_PATTERN, `internal_metadata` is unreachable. Usage-site guards
   become flag-based so a legacy peer merely occupying the namespace keeps
   working rather than 422-ing on its own traffic; peer create and update stay
   name-based, since those must stop new names entering the namespace.
   `update_peer` now returns 422 instead of 500.

Also swaps the reserved prefix from `scope__` to `scope.`: `_` is inside
RESOURCE_NAME_PATTERN, so any tenant could already own a `scope__x` peer.

No DB migration — `internal_metadata` already exists on `peers`, and no scope
peers exist in any deployment yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(scopes): validate peer names on create, close namespace squatting and upsert race

Addresses three review findings against 48047a6a.

1. `PeerSpec` let API callers create invalid and reserved-prefix peers.
   Widening `get_or_create_peers` to accept a pattern-free schema fixed the
   lookup 500s but also removed validation from the *insert* path, and
   request-controlled names reach it via message authors, session peer maps, and
   the chat observer path — none of which carry a charset pattern of their own.
   Confirmed: `POST /sessions/{id}/messages` with `peer_id: "scope.x"` returned
   201 and minted an unflagged squatter, after which `POST /scopes {id: x}` was
   permanently 409-blocked — namespace denial of service by any caller able to
   post a message. `peer_id: "not a valid name!@#"` was likewise created.

   Fixed by validating only names about to be INSERTed
   (`_validate_new_peer_names`), so already-existing names — legacy dotted
   names, scope peers — still resolve without a spurious 422. That keeps the
   Dreamer fix intact, since it reads through `get_peer`.

2. Existing reserved-prefix squatters could not be updated. The name-based guard
   on `PUT /peers/{peer_id}` refused every `scope.` name, contradicting the
   invariant that an unflagged squatter stays a normal peer. Now flag-based, so
   behavior is three-way: a real scope is refused, an existing unflagged peer
   updates, and a missing reserved-prefix name is refused by (1) rather than
   minted.

3. Scope checks raced with get-or-create and the membership upsert. The
   route-level guards run before peers are resolved, so a scope created
   concurrently in that window would be attached by the generic path with a
   default `SessionPeerConfig()`, clobbering its observer membership config.
   Adds `_reject_resolved_scope_peers`, which runs on the resolved rows in the
   same transaction as the upsert — no window, no extra query. The early checks
   stay for better error messages.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(scopes): guard scope membership config, move checks to the mutation point

Addresses a second review pass against 10655792.

1. Scope membership configuration was directly user-mutable.
   `PUT /sessions/{id}/peers/{peer_id}/config` had no scope guard at all, and
   `crud.set_peer_config` resolved the peer only to discard the row. Confirmed:
   posting `{"observe_others": false, "observe_me": true}` for a scope returned
   204 and persisted, which silently stops all fan-out into the scope and makes
   Honcho form a representation *of* a scope — neither of which is reachable
   through the facade. Deterministic, no race required. Now checked on the row
   `get_peer` already returns, so it costs nothing and cannot race.

2. Empty and over-long names were still 500s. Removing the charset pattern from
   `PeerSpec` fixed one trap but left its length bounds, and request-bound peer
   names carry no length limits of their own — so `peer_id: ""` or a 513-char
   name reached `PeerSpec(...)` and raised a raw pydantic ValidationError that
   the catch-all turned into a 500. `PeerSpec` now carries no constraints at all
   (matching its documented purpose) and every rule for a new name lives in
   `_validate_new_peer_names` on the insert path.

3. Resolved-row protection generalized. The previous pass applied it only to
   membership upserts, leaving check-then-use windows elsewhere: peer update
   could have a concurrently-created scope's configuration replaced wholesale
   (create-path validation does not fire for a peer that now exists), the chat
   observer get-or-create could resolve a fresh scope as its observer, and the
   generic session-peer removal could silently detach a scope from its sessions.
   Each now inspects the resolved peer immediately before acting; the redundant
   name-level guard on the update route is dropped in favor of the race-free one.

`remove_peers_from_session` grows an internal `_allow_scope_peers` flag because
the scopes facade ends membership through that same path and must not be blocked
by its own guard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(scopes): enumerate every peer-touching route against a scope policy

Three review passes each found the same class of defect: a route nobody had
checked, rather than logic that was subtly wrong. One of them — `PUT
/sessions/{id}/peers/{id}/config`, which let any caller set a scope to
`observe_others=false` and silently stop all fan-out into it — predated this work
entirely, because the guardrail set was assembled guardrail-by-guardrail instead
of derived from the route list. Sampling review cannot close that kind of gap;
enumeration can.

Derives every route through which a peer name can reach the system and requires
each to be classified as GUARDED or EXEMPT-with-a-reason, so a newly added
peer-touching route fails the suite until someone classifies it. Detection is the
union of path shape and a walk of the dependant tree (including sub-dependency
`Form(...)` params and nested request-body models), because neither signal alone
suffices: parameter names miss `POST /sessions/{id}/peers`, whose peer names are
dict keys, and path shape misses `messages/upload`, whose `peer_id` arrives as a
form field behind a parser dependency.

Both invariants are then asserted behaviorally, by calling the routes rather than
inspecting annotations — the guards deliberately live in crud, which is what makes
`messages/upload` guarded for free via `crud.create_messages`:

- a real scope is refused on all 11 guarded routes, and the rejection must name
  the scope, so an unrelated 422 (a malformed body) cannot pass the assertion;
- an *unflagged* peer merely occupying the reserved namespace is unaffected. That
  half regressed once already when `update_peer` used a name-based check.

Mutation-tested all three failure modes: disabling the `set_peer_config` guard
fails the guarded test naming that route; regressing `update_peer` to name-based
fails the squatter test; adding an unclassified peer route fails the enumeration.

Covers the HTTP surface only. Peer names also reach the system through the
deriver, dreamer, and queue, which have no route table to enumerate — noted in
the module docstring rather than implied to be covered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(scopes): refuse a scope in every observed position; enumerate per position

Addresses a fourth review pass against 62681e4d. The headline finding is that the
previous commit's enumeration test had the wrong *model*, not a missing entry.

1. Manual conclusions could create knowledge about a scope. `POST /conclusions`
   validated only that observer_id and observed_id exist, so a scope as
   `observed_id` persisted a conclusion about a peer carrying observe_me=false and
   created an (observer, scope) collection for it. Confirmed: 201, and it read
   back. `POST /schedule_dream` had the same hole via `observed`.

   The fix is positional, because the invariant is:

       A scope may be an OBSERVER. A scope may never be OBSERVED.

   A scope as `observer_id` is how scoped conclusions are stored and must keep
   working (verified still 201); as `observed_id` it is now refused. Same split
   applied to schedule_dream `observed`, the peer-card `target` (which also covers
   a scope's self-card, since target-omitted collapses observed to peer_id), and
   session-context `peer_target`.

2. Chat target and both representation roles kept check-to-use races. Only the
   chat path-level observer was re-checked on its resolved row; the target was
   checked by name and then resolved without inspecting scope identity. Both are
   now checked at the dialectic preflight, where observer and observed are already
   resolved — an absent name has already failed by then, and an existing squatter
   cannot retroactively become a scope.

3. Generic membership removal was still racy. The adjacent SELECT narrowed the
   window but could not close it under READ COMMITTED. The UPDATE now carries its
   own correlated NOT EXISTS against scope_peer_clause(), so Postgres evaluates
   the exclusion as part of the statement and a scope committed after the advisory
   check still cannot be detached.

4. New-name validation ran after the name reached Postgres. A NUL byte passed the
   request schemas and PeerSpec, then raised psycopg.DataError inside the lookup —
   a 500. Values that cannot correspond to a stored row by construction (NUL
   bytes, over-length names) are now refused before the query.
   (Over-length names already returned 422; only the wasted query was real there.)

The enumeration test is rekeyed from (method, path) to (method, path, position).
A binary per-route verdict cannot express finding 1 at all: `POST /conclusions` is
one route with two positions and opposite verdicts. Detection widens to observer /
observed / target / peer_target / peer_perspective, which surfaced four routes the
previous version never saw — conclusions, schedule_dream, queue/status, and
session context.

Also registers `src.routers.workspaces.tracked_db` in the conftest patch list; the
new guard there would otherwise have run against the real configured database
instead of the per-test one.

Mutation-tested: disabling the conclusions observed-guard fails the positional
test naming that position; adding an unclassified `observed_id` param fails
enumeration.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(scopes): refuse future scopes in observed positions, preserve scope membership

Addresses a fifth review pass against 14136e5b. All six findings reproduced
locally before fixing.

1. High — generic peer replacement removed scope memberships.
   `set_peers_for_session` soft-deleted every active SessionPeer row, and the
   request-level guard only inspected names *present* in the replacement map. A
   caller detached a scope by simply omitting it, never naming it — so no
   request-level guard could ever see it. Reproduced: scope sessions went
   ['<id>'] -> [] on a 200. The exclusion now lives in the UPDATE itself
   (correlated NOT EXISTS against scope_peer_clause), so replacement means
   "replace ordinary peers" regardless of request contents or concurrent creation.

2. High — peer cards could be pre-seeded for future scopes.
   `set_peer_card` resolves only the observer and writes a JSONB key derived from
   an unchecked observed name, and the route guard rejected only *existing*
   flagged scopes. Reproduced: PUT card with target=scope.<missing> returned 200,
   creating that scope then returned 201, and the card described the real scope.

3. High — dreams could be queued for future scopes.
   The route checked `observed` in a read-only session that closed before
   `enqueue_dream`, and a missing reserved name passes any is-it-a-scope check.
   Reproduced: 204 with observed=scope.<missing>.

   2 and 3 share a root cause, so they share a fix: a new `reject_scope_observed`
   that is stricter than `reject_scope_peers` in exactly one case — a *missing*
   reserved name is refused, because nothing on these paths creates the peer, so
   nothing else would ever catch it. Existing unflagged squatters still pass.
   Both guards moved to the mutation point: card validation into
   `crud.set_peer_card` (same transaction as the JSONB write, so Dreamer and
   agent-tool callers are covered), dream validation into `enqueue_dream` (same
   transaction as the queue insert). The redundant route-level checks are dropped
   rather than left as weaker duplicates.

4. Medium — prefixed NUL names still reached PostgreSQL.
   `reject_scope_peers` filtered for the reserved prefix and sent matches to a
   text comparison, so "scope.future\0name" raised psycopg.DataError — a 500.
   Both guards now share `_reserved_name_candidates`, which materializes the input
   once and rejects impossible values before any SQL. Materializing matters
   independently: the message-author path passes a generator, and validation
   iterates separately from the prefix filter, so a generator would be
   half-consumed. `_reject_impossible_peer_names` now takes a Collection so the
   type checker enforces that.

5. Medium — representation kept a check-to-use race.
   The previous commit claimed both representation roles were rechecked after
   resolution; that was wrong — only the dialectic preflight got that check, and
   the representation route never goes through it. It now opens one short
   read-only session *after* the embedding call, checks both positions, and passes
   that same session to `get_working_representation`, so no connection is held
   across external work and a scope committed later cannot have conclusions in the
   collection being read.

6. Low — policy coverage was not exhaustive. `sender_id` reaches CRUD as
   `observed` but was missing from the detected parameter set. ALLOW cases could
   also not carry builders, so the suite never proved the other half of the
   contract — that legitimate scope *observers* keep working, which a guard
   rejecting scopes everywhere would satisfy. Both fixed; observer positions on
   conclusions, dreams, cards, session context and queue status are now asserted
   behaviorally.

Deliberately not implemented: the scope-creation backstop scanning for
pre-existing card keys and queue items naming a future backing peer. Reasoning is
recorded in `get_or_create_scopes` — no new such state can be created now, any
pre-existing row is coincidental since `scope.` was never a meaningful namespace,
the consequence is inert, and detecting card keys means a full table scan per
scope creation.

Mutation-tested each new guard: removing the replacement exclusion fails both
membership-preservation tests; weakening either observed guard to existing-only
fails the pre-seeding tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(scopes): exclude scope memberships from the session observer limit

Scope memberships carry observe_others=true, so every scope counted against
SESSION_OBSERVERS_LIMIT (default 10) — capping scopes-per-session at the limit
minus the session's real observers, and reporting the failure as
`400 Cannot create session <name> with 11 observers. ... Observers are peers
with 'observe_others' set to true.` on a membership call. Wrong on three counts:
the ceiling is undocumented and contradicts RFC §5.1 ("sessions belong to any
number of scopes"), the message describes session creation, and it leaks the
word "observer" through a facade whose entire job is hiding observers (RFC
goal 5). The limit exists to bound per-observer deriver fan-out for real peers;
a scope costs document rows, not LLM calls (RFC §5.2), so it does not belong in
that budget.

Excluded from both halves of the check in `_get_or_add_peers_to_session`: the
incoming names via a flag-based lookup, existing memberships via a correlated
NOT EXISTS on `scope_peer_clause()` — the same pattern the replacement and
removal paths already use, so the exclusion holds regardless of concurrent
scope creation. The early `count_observers_in_config(session.peer_names)` check
in `get_or_create_session` is left alone: `peer_names` cannot contain a scope,
and `scopes` is a separate field.

`reject_scope_peers` is split into a `scope_peer_names()` query helper plus a
two-line raiser so the observer count reuses the authoritative name-AND-flag
predicate instead of growing a third copy of it. Still costs nothing on the
common path — no reserved-prefix name in the input means no query at all.

Also caps `SessionCreate.scopes` at 100, matching `ScopeSessionsAdd.session_ids`.
This belongs in the same commit: the observer limit was the only thing bounding
that list, so removing it turns an unbounded `scopes` array into a peer row and
a membership row per element, committed — the single-request path to the
cardinality anti-pattern RFC §8 warns about. Partly answers OQ6: no per-session
cap, 100 per request.

Tests: a session joins SESSION_OBSERVERS_LIMIT + 2 scopes through both the
facade and session creation; real observers over the limit still 400, so the
carve-out cannot quietly disable the limit; 101 scopes is a 422.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(scopes): skip semantic retrieval when the embedding precompute failed

Addresses three open review comments.

1. Major — the representation read could embed inside its DB session. The
   route's precompute is suppressed, and both
   `RepresentationManager.get_working_representation` and
   `crud.query_documents` fall back to embedding when a query arrives without
   one, so a failed precompute meant an external call inside the read session
   this branch opens for the scope re-check — the connection-holding rule the
   route's own comment claimed to satisfy. The innermost fallback also only
   catches ValueError, so a provider outage surfaced as a 500. The semantic
   query is now passed only when an embedding exists, degrading to
   derived+recent retrieval. (`crud.query_documents` embedding inside a caller's
   session predates this branch and is left alone.)

2. Minor — `test_resolved_scope_peer_rejected_at_membership_upsert` described a
   race it does not perform. It creates an already-flagged scope and calls crud
   directly; the unflagged → flagged transition is not simulated. Docstring now
   says what the test actually pins.

3. Minor — `test_empty_replacement_preserves_scope_membership` asserted only
   half its docstring. It passed if the empty PUT left every ordinary
   membership intact; now asserts the ordinary peer's left_at is set.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(scopes): close auth and observed-position gaps, paginate membership

Review response for #884.

Security:
- gate `SessionCreate.scopes` behind a workspace-level key; the session-create
  route is self-authorizing, so a peer- or session-scoped token could mint scope
  peers and join sessions to scopes it had no access to via `POST /scopes`
- refuse a reserved-but-nonexistent name in two observed positions that used the
  permissive guard: chat `target` and session-context `peer_target`. Both let a
  caller act on `scope.X` before it existed, then create the scope

Facade:
- exclude scope peers from `GET /sessions/{id}/peers` and refuse the membership
  -config read for a real scope, matching its write side
- replace `GET /scopes/{id}/sessions` with `POST /scopes/{id}/sessions/list`
  returning `Page[Session]`; the add route now returns 204. Membership was
  unbounded on both, while every other list surface paginates
- rename `crud.get_scope` to `get_scope_or_raise`

Tests:
- add a missing-name axis to the route-policy table (`Case.refuse_missing`), which
  is what surfaced the two guard gaps above
- delete 14 hand-written tests the table now enumerates; 52 -> 39 functions in
  test_scopes.py with more cases covered
- tighten the squatter assertion from `!= 422` to `< 400`, which was passing on 5xx
- assert the FastAPI-internals traversal still derives positions, so a framework
  upgrade can't silently empty the suite

Docs:
- drop internal ticket and RFC references from the published OpenAPI descriptions
  and surrounding comments; state the behavior instead
- move implementation reasoning out of the `PUT /peers/{id}` docstring, which
  FastAPI publishes, into a comment

* test(scopes): assert exact statuses for permissive missing-name cases

Follow-up review pass on #884.

- add `Case.missing_status` so a permissive missing-name position asserts the
  status it should actually get (404, or 200 for the no-op removal) instead of
  `!= 422`, which also passed on a 5xx — the same hole already closed in the
  squatter assertion
- require it whenever `refuse_missing` is False, and require its absence when
  True, so the policy table can't drift from the assertion
- repoint a stale allow-reason at POST /scopes/{scope_id}/sessions/list; the GET
  it named was removed
- document the membership list's ordering under `reverse`

* chore: clean up stale docstring language

* fix: don't backfill a session that left the scope

scope_backfill and scope_removal carry different work-unit keys, so
nothing orders them: a removal enqueued right after the add — or one
that lands while the backfill is embedding — sweeps the scope before
the copies exist, leaving a departed session's documents live in the
scope forever.

_run_backfill now re-checks SessionPeer membership inside the write
transaction and returns None; process_scope_backfill then skips both
the dream enqueues and the status write, so a skipped backfill can't
resurrect the status entry removal just cleared.

Adds coverage for the skip, the NULL-embedding re-embed path, and the
failed-status write. Handler-driven tests now stand up the membership
row the guard requires.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 15:08:43 -04:00
ajspig 226bd0d0fb
fix(mcp): OAuth session fixes, write scope, SDK bump (#1004) 2026-08-14 11:02:59 -04:00
Rajat Ahuja c594469f05
fix: add scheduled probe for mcp (#988)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-14 10:02:17 -04:00
ajspig e82d2ef497
feat: adds workspace headers to MCP calls and adds list/create workspace tools (#1020)
* feat: adds workspace headers to MCP calls and adds list/create workspace tools

* docs: clean

* fix(mcp): clarify missing-workspace and admin-key errors

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-13 16:48:31 -04:00
ajspig b7d4a2a3ad
feat(mcp): search conclusions via search tool (#974)
* feat(mcp): search conclusions via search tool

Port of plastic-labs/claude-honcho#113 to the standalone MCP worker.
The search tool only queried the message store, so saved conclusions
were unreachable through search (findable only by paging
list_conclusions or via query_conclusions with a known scope).

search now also queries conclusions in parallel with the message
search, returning {messages, conclusions}. Since the conclusions
query API requires an explicit (observer, observed) pair, the
conclusion leg runs only when peer_id is given (self-conclusions,
matching list_conclusions/query_conclusions defaults) and degrades
to [] on error so search never gets worse than before. Conclusion
results include IDs usable with delete_conclusion.

Also syncs mcp/bun.lock with package.json's @honcho-ai/sdk ^2.1.0
(the lock still recorded ^2.0.0) and points delete_conclusion's
description at query_conclusions/list_conclusions for ID discovery.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: add support for filtering to mcp

* chore: minor nits

* fix: adding better error handling

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 16:40:00 -04:00
Vineeth Voruganti c9f836c7f6
fix(filter): make the filter DSL reject bad input instead of 500ing, and fix negation over unset fields (#947)
* fix(filter): reject unknown operator dicts on scalar columns

An unrecognized operator dict on a non-JSONB column (e.g.
{"session_id": {"operator": "null"}}) fell through to `column == value`,
binding a dict to a VARCHAR parameter. That compiles, then fails in the
driver at execute time with "cannot adapt type 'dict'" — an unhandled
500 for what is invalid input.

Raise FilterError (422) instead. The guard lives in the shared
_build_field_condition, so every route through apply_filter is covered.
It keys on the actual column type rather than the JSONB_COLUMNS name
list, so dict equality still works on JSONB columns reachable through
Document's raw-key fallback (e.g. source_ids), where the driver adapts
dicts fine.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(filter): name psycopg explicitly in comment

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(filter): handle null operands and non-numeric columns in comparisons

Two defects in _build_comparison_conditions, both reachable from any
route that accepts filters:

1. A null operand hit float(None), raising TypeError where only
   ValueError was caught — an unhandled 500. A null operand is a null
   check, not a value comparison, so {"ne": null} now compiles to
   IS NOT NULL and the other operators reject null with a 422. Equality
   against null already produced IS NULL via _build_field_condition.

2. Numeric operators float()-cast on every column type, so a string
   inequality on a text column ({"session_id": {"ne": "abc"}}) was
   rejected as an invalid number. Coercion is now gated on the column
   actually being numeric; text columns compare as text. Numeric columns
   still validate, and TypeError is caught alongside ValueError.

Existing ne coverage only exercised the JSONB metadata path, which uses
_safe_numeric_cast and handles strings — the scalar column path was
untested. Adds cases for both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(filter): fail closed on unrecognized filter shapes

The filter body is arbitrary client JSON with no schema, so validation
was emergent: any shape the DSL didn't recognize surfaced as an
unhandled 500 from somewhere in SQLAlchemy or psycopg. Fixing individual
shapes doesn't converge — a fuzz over the DSL found five more families
beyond the three already fixed here:

  {"AND": [None]}                TypeError, non-dict in a logical list
  {"AND": [[]]}                  AttributeError on .items()
  {"session_id": {"gte": true}}  SQLAlchemy ArgumentError
  {"embedding": []}              NotImplementedError, no python_type
  {"session_id": {"ne": {...}}}  execute-time "cannot adapt type 'dict'"

Two generic guards instead:

1. Any operand bound to a non-JSONB column must be a scalar, checked
   element-wise for `in`. A dict or list bound to a scalar column
   compiles cleanly and only fails in the driver at execute time, so it
   has to be rejected during construction. JSONB columns are exempt —
   a dict there is a containment match.

2. apply_filter fails closed: FilterError propagates, anything else is
   logged with logger.exception (filter shape included) and re-raised as
   FilterError. Unknown filter failures become 422s while staying fully
   visible as errors rather than being swallowed.

Adds two invariant tests over a generated matrix of filter shapes: every
shape either compiles or raises FilterError, and no non-scalar is ever
bound to a scalar column. Both fail without the guards above. They cover
shapes nobody enumerated, so the next unimagined body fails in CI rather
than in production.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore: clear the two remaining basedpyright warnings

`uv run basedpyright tests/ src/` reported two warnings in files that
predate this change. The pre-commit hook is file-scoped, so neither was
visible unless the owning file was touched.

- src/vector_store/__init__.py: join the lancedb error message with
  explicit `+` instead of adjacent literals (reportImplicitStringConcatenation).
- tests/test_cache_redaction.py: the test covers a private helper
  deliberately, so annotate the import (reportPrivateUsage).

No behavior change; whole-tree check is now clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(filter): keep numeric operands exact instead of coercing to float

float() rounds any integer past 2**53 and flattens a Decimal, so
{"token_count": {"gt": 9007199254740993}} silently compared against
9007199254740992 — a different row set than the client asked for.

_coerce_numeric passes already-numeric operands through untouched and
only parses strings, trying int() before float() so "5" stays exact
while "5.5" still parses. bool narrows to int: it is an int subclass,
but binding it as a boolean against a numeric column produces SQL
Postgres has no operator for.

Not coerced to the column's own type: int(5.5) would turn
{"token_count": {"lt": 5.5}} into `lt 5`, changing which rows match.

Also fixes a vacuous assertion in test_dict_on_jsonb_column_still_works.
It checked for "internal_metadata" in the whole statement, but that name
is in the SELECT projection either way, so the test passed even when no
WHERE clause was applied. Now asserts on stmt.whereclause and that the
filter payload is actually bound.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(filter): bind boolean columns as booleans, numerics without a cast

Boolean columns were treated as numeric because bool subclasses int, so
`{"is_active": {"ne": true}}` coerced true to 1 and Postgres rejected
`boolean <> integer` at execute time. Confirmed against a live database:
every form except bare equality with a native boolean was a 500.

Boolean columns now get their own branch: native true/false bind as
booleans, and any other operand is a FilterError. SQLAlchemy types the
bind from the operand rather than the column, so "true" renders
`is_active = %(param)s::VARCHAR` and Postgres has no such operator — a
422 is the honest answer. String booleans have never worked, are absent
from the docs (every documented boolean is inside metadata, which is
JSONB containment and unaffected), and produced no Sentry events in 90
days, so nothing can depend on the current behavior.

Also corrects the previous commit. Coercing operands to exact ints made
SQLAlchemy render an ::INTEGER cast, so any value past int4 — not 2**53
— started failing with "integer out of range" where float() had silently
compared as a double. Decimal keeps the value exact and renders no cast,
matching what float() did. The `in` branch never went through coercion
at all, so {"token_count": {"in": [1, 2147483648]}} was a 500 before
this PR too; it now takes the same path.

Verified end to end against the live database, not just at compile time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(filter): coerce every operand against its column's type in one place

The DSL had two operand paths with different rules. Comparison operators
parsed datetimes and coerced numbers; bare equality bound whatever it was
handed. SQLAlchemy types a bind from the operand rather than the column
and the psycopg dialect renders that type as an explicit cast, so a
mismatch compiled into valid-looking SQL and failed at execute time:
"operator does not exist: timestamp with time zone = character varying".

A matrix of column type x operand type x operator against a live
database found 462 combinations, of which 55 built cleanly and then
failed. The most plausible was a filter someone would write first try:

    {"created_at": "2026-01-01"}          500
    {"created_at": {"gte": "2026-01-01"}} worked

_coerce_operand now handles every operand, whatever the operator, keyed
on the column's real type: JSONB takes an object, boolean takes only
true/false, datetime parses strings, numeric goes through _coerce_numeric,
text requires a string, and a column with no python_type (pgvector) is
not filterable. eq/ne/gt/in cannot drift apart because they share the
one call; `in` coerces element-wise, since a single element's type
decides the cast rendered for that parameter. The matrix is now clean.

This is a net deletion: the separate datetime, numeric, in-datetime and
boolean branches, plus _require_bindable_operand, all collapse into it.

Two more execute-time failures fixed on the way. `contains` was keyed on
column_name == "h_metadata", so Document's equally-JSONB
internal_metadata fell through to ILIKE and produced `jsonb ~~* text`;
it now keys on the column type. And {"source_ids": "abc"} was
`jsonb = character varying`.

Closed-set columns are validated against the Literal that defines them,
so declaring a new level or sync state updates filter validation with no
change here. {"level": "banana"} was silently matching nothing.

Empty IN is now always applied rather than skipped. Unifying the branches
inherited a guard that had only ever wrapped the datetime path, which
dropped the condition entirely and widened the query to every row —
fail-open on an empty allowlist, which session scoping relies on to fail
closed (see extract_session_allowlist). Caught by an existing test that
asserts returned rows; the fuzz and the type matrix only check for
errors, so neither would have seen it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(filter): make NOT and ne include rows where the field is unset

NOT (col = v) and col <> v are NULL when col is NULL, so negation
dropped rows whose column is unset — a conclusion with no session is
not "some other session", but was excluded anyway. IS NOT TRUE and
IS DISTINCT FROM behave identically when no NULL is involved.

Adds a row-count test, since this class of bug builds and executes
cleanly, and documents {"ne": null} for excluding unset fields.

* fix(filter): reject session ids that can't name a real session

extract_session_allowlist accepted any non-empty string, so "*" reached
the three consumers of the allowlist — direct IN, the filter DSL, and a
Python membership test — which disagree about it. The DSL reads "*" as
"drop the condition" and matches every session; the others treat it as a
literal name and match none. One /chat request could have some recall
sources unscoped and others scoped to nothing.

Entries are now validated against RESOURCE_NAME_PATTERN, the same pattern
the API requires of session ids, so no session could ever be named "*"
anyway. Wildcards were never part of this endpoint's documented contract
(an id, a list of ids, or {"in": [...]}), and a wildcard alongside a
top-level session_id already 422'd via must_include.

* fix(filter): treat a bare null operand as a null check

Routing every operand through _coerce_operand made bare `None` a type
error rather than a null check, so {"session_id": null} raised FilterError
where it previously built IS NULL: _build_field_condition used to end in
`column == value`, which SQLAlchemy renders as IS NULL. Confirmed 422 on
all five column families (text, numeric, boolean, datetime, JSONB).

Nothing caught it. The docs added in this branch promise
`{"session_id": None}` matches unset rows, the comment in
_build_comparison_conditions claimed the equality path already covered it,
and the DSL-wide invariant test accepts "compiles OR raises FilterError",
so a 422 passed. _coerce_operand's docstring already stated the contract
its caller wasn't honoring — "Callers handle None (a null check) and `*`
(a wildcard) before calling" — so the guard restores that rather than
adding a new rule.

The three null forms now agree: {"col": null} is IS NULL, {"col": {"ne":
null}} is IS NOT NULL, NOT [{"col": null}] is (IS NULL) IS NOT true.

Also from review of #947:

- Log filter keys, not the body. That log line is new in this branch and
  operands carry peer/session ids and free-text `contains` values; the
  traceback plus the entry shape is what locates a builder bug.
- Assert whereclause in the _where test helper, so a dropped condition
  fails instead of returning the whole statement to substring-match.
- Cover the raw-key JSONB path via source_ids, a JSONB column outside
  JSONB_COLUMNS reachable through Document's raw-key fallback.
- Drop the orphaned comment left above ENUM_COLUMN_VALUES when
  _coerce_operand replaced SCALAR_OPERAND_TYPES.
- Document that a JSONB column takes an object bare or under `contains`
  and nothing else. Bare {"metadata": X} is containment, so the `ne` this
  branch removed was never its inverse: a row with {"status":"done","x":1}
  satisfied both it and {"metadata": {"status":"done"}}. Per-key operators
  and NOT cover the real intents.
- Rewrite "Negation and Unset Fields" to lead with the operator rule and a
  truth table, forward-linking to Filtering Conclusions instead of using
  conclusions ~525 lines before they are introduced. A conclusion's
  session_id is the only nullable documented filterable field, verified
  across Message/Document/Session/Peer/Workspace.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 10:39:19 -04:00
Aakash Kattelu 252269e9b6
perf: lazy-load provider SDKs to cut idle memory per process (#1011)
Import anthropic/openai/google-genai only when a provider is first used
instead of at module import. CLIENTS is now populated lazily via
default_client(), which preserves the patch.dict test seam. The
embedding client defers its SDK imports the same way and dispatches on
transport instead of isinstance.

Cuts idle RSS by ~60MiB per process with all three providers configured
but unused at startup; a process that only ever calls one provider also
never pays for the other two.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 21:39:12 -04:00
Rajat Ahuja ed253bf8a2
fix: reduce Honcho runtime image size (#1014)
* Reapply "fix: slim honcho image"

This reverts commit a74f2b3a1c.

* docs: note LanceDB is excluded from the default Docker image

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(docker): share lancedb volume across compose services, clarify import error

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Aakash Kattelu <aakash@plasticlabs.ai>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-08-12 19:55:41 -04:00
Vineeth Voruganti be7ad9d919
Scopes Phase 2b: `scope` option on chat, representation, context, and search (#897)
* fix: apply session scoping to all working-representation query paths

session_name was only applied to the recent-documents query in
RepresentationManager; the semantic and most-derived paths ignored it,
so limit_to_session leaked cross-session conclusions into perspectives.

- Thread a session allowlist (session_names) uniformly through all
  three query paths; pushed down to pgvector and external vector stores
- Accept a list so the upcoming session-allowlist API reuses this path
- Fail closed on an empty allowlist (downstream stores drop empty IN
  clauses, which would silently widen scope)

Fixes DEV-1994

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: bare-list membership sugar in the filter DSL

{"session_id": ["s1", "s2"]} is now shorthand for
{"session_id": {"in": [...]}} on regular columns, generically
(peer_id, etc.). JSONB metadata columns are excluded — a bare list
there keeps JSONB containment semantics, unchanged.

Previously a bare list on a regular column compiled to a type-mismatched
equality that matched nothing, so this is strictly additive.

Also translates the same shape in the turbopuffer/lancedb filter
builders, and fixes lancedb dropping empty IN clauses (fail-open) —
an empty membership list now emits an always-false condition.

Groundwork for DEV-1995 (session allowlist via the existing filters
DSL, no new API params)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: session allowlist on dialectic and representation via filters

Adds a constrained 'filters' body to peer.chat and /representation —
the same DSL search and conclusions already accept, supporting only the
session_id key (a session id, a bare list, or {"in": [...]}).
Unsupported keys and shapes are rejected with 422, never silently
ignored. Composes with session_id (must be included in the allowlist
when both are given). Capped at 1,000 sessions per request.

Enforcement is uniform at every recall chokepoint, fail-closed:
- dialectic prefetch + search_memory: conclusion recall restricted to
  the allowlist; dream docs (session_name IS NULL) excluded
- message tools (search/grep/date-range/temporal/context/history):
  strict intersection of allowlist and observer session membership
- get_reasoning_chain: unavailable under an allowlist (chains traverse
  provenance across sessions and cannot be scoped without leaking)
- empty allowlist short-circuits to empty results everywhere

Auth: workspace keys pass the allowlist as-given; peer-scoped JWTs must
be a member of every allowlisted session (403 otherwise), mirroring the
existing single-session check.

Fixes DEV-1995

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: reserve scope__ peer namespace with kind flag and guardrails

Introduce the scope peer namespace (scope__<name>) and the authoritative
{"kind": "scope"} configuration flag, plus the server-side guardrails:

- src/utils/scopes.py: single source of truth for the prefix, kind flag,
  and name helpers (scope_peer_name / is_scope_peer_name /
  scope_name_from_peer / validate_no_scope_peer_names)
- reject reserved-prefix names on peer get-or-create (422)
- reject scope peers as message authors in crud.create_messages (422)
- reject scope peers as chat/representation targets (422); a scope peer
  as the path-level observer is deferred to Phase 2b
- reject scope peers on the generic session-peer add/set/remove routes
  and the session-create peers mapping (422, directing to scopes routes)
- peers.list excludes scope peers by default; new PeerGet.kind option
  ("scope" | "all") switches the view via a configuration JSONB filter
- schemas: Scope / ScopeCreate / ScopeSessions(Add) and
  SessionCreate.scopes (unprefixed scope names, validated)

Part of DEV-1997 (Scopes RFC DEV-1970).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: add scopes CRUD routes and session-create scopes wiring

New /v3/workspaces/{workspace_id}/scopes facade (workspace-level auth;
peer- and session-scoped keys are rejected):

  POST   ""                                   create-or-get (201/200)
  POST   /list                                 paginated scope list
  GET    /{scope_id}                           single scope
  POST   /{scope_id}/sessions                  add memberships
  DELETE /{scope_id}/sessions/{session_id}     remove membership
  GET    /{scope_id}/sessions                  list member session ids

- crud/scope.py: get_or_create_scopes stamps the backing peer with
  {"kind": "scope", "observe_me": false} and refuses to adopt a
  legacy peer occupying the reserved name without the flag (409)
- memberships are session_peers rows with observe_others=true /
  observe_me=false — identical to a hand-built observer peer
- SessionCreate.scopes: create-or-get each scope peer and add the
  membership at session creation (the no-backfill common path)
- crud/session.py: public upsert_session_peers wrapper so the facade
  bypasses the route-level guardrails without reaching into privates

Backfill of pre-existing documents and reconciliation on removal land in
DEV-1999; membership only affects messages ingested after the change.

Part of DEV-1997 (Scopes RFC DEV-1970).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: cover scopes facade, guardrails, and observer semantics

- create-or-get idempotency, list/get, name validation, legacy-collision
  rejection (409), auth scoping (workspace key ok, peer/session keys 401)
- reserved prefix rejected on peer create; peers.list kind filtering
- scope peers rejected as message authors, chat/representation targets,
  and on the generic session-peer routes
- membership add/list/remove with observe_others=true / observe_me=false
  row shape asserted via DB, and facade-less equivalence with a
  hand-built observer peer
- end-to-end litmus: after adding a session to a scope, the deriver
  enqueue fan-out includes the scope peer as an observer
- session creation with scopes: [a, b] creates both memberships

Part of DEV-1997 (Scopes RFC DEV-1970).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(scopes): scope resolution helper and read-route schemas

Add resolve_scope_peers (crud/scope.py) mapping unprefixed scope names to
their backing scope peers, 404 when missing and 422 when a non-scope peer
squats the reserved name. Add the `scope` option to DialecticOptions and
PeerRepresentationGet, and a WorkspaceMessageSearchOptions with `scope`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(scopes): wire the `scope` read option into the routes

Chat and representation: a single scope swaps the observer to the scope
peer (recall confined to the scoped collection and the scope's sessions by
existing observer semantics); a scope list resolves to the union of member
sessions and rides the DEV-1995 dynamic allowlist arm with the path peer as
observer. `scope` is mutually exclusive with `filters`/`session_id` (422)
and requires a workspace/admin key (403 for peer-scoped JWTs).

Session context: `scope` swaps the perspective source for both the working
representation and the peer-card fetch. Workspace search: `scope` injects
the scope's session set into the message-search filter (empty scope -> no
results). Close the carry-over guardrail gap: scope peers are rejected as
peer_target/peer_perspective in session context and as the peer/target in
GET /peers/{id}/context.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(scopes): cover the `scope` read option end-to-end

Validation (404/422/403), single-scope observer swap vs. union allowlist,
scoped representation and session-context (scoped collection + scoped card),
workspace search restricted to a scope's sessions, and guardrail closure for
scope peers on the peer/session context surfaces. Patch the workspaces
tracked_db import site so scope resolution reads the per-test database.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(crud): preserve cache invalidation across get_or_create retry

`get_or_create_peers` and `get_or_create_scopes` mutate existing rows, then
insert new ones inside `db.begin_nested()`. When a concurrent writer creates
one of those rows first, the insert raises IntegrityError and the function
retries.

`begin_nested()` autoflushes the pending UPDATEs *before* opening the
savepoint, so the rollback neither undoes them nor expires the now-clean ORM
state. The retry then compared already-updated values, found no change, and
dropped those peers from `changed_peers` — skipping the cache purge while the
row change committed anyway, leaving entries stale until the 300s TTL.

Carry the mutated names into the retry via `_pending_invalidation` so the
purge cannot be lost.

The scopes facade mirrors `get_or_create_peers`, so both copies carried this.
The peer path is pre-existing and runs on every message ingest.

Also add the missing /v3 prefix to `_SCOPES_ROUTE_GUIDANCE`, which pointed
callers at a 404.

Adds tests/crud/test_get_or_create_retry_invalidation.py, which drives a real
racing session and fails without this change.

* fix(scopes): make scope identity unforgeable, unblock non-pattern peer names

Three coupled changes to the scopes facade.

1. `PeerCreate` no longer gates internal lookups. It exists to validate a new,
   user-supplied peer id at the API boundary, but crud used it as a DTO for
   names that already exist, so any name outside RESOURCE_NAME_PATTERN raised a
   raw pydantic ValidationError — which is not a HonchoException, so it fell
   through to the catch-all handler as an HTTP 500. Adds `PeerSpec` (same
   fields, no charset pattern) as `PeerCreate`'s base, widens
   `get_or_create_peers` to accept it, and changes `get_peer` to take a plain
   str. All 13 construction sites converted; the create route keeps full
   validation.

   This unbreaks the Dreamer: DreamScheduler passes `collection.observer`
   straight into the specialist preflight, and scope peers have
   `observe_others=true`, so every `(scope.x, peer)` dream died there — the
   feature scopes exist to enable. It also fixes a pre-existing bug unrelated
   to scopes: a peer named `alice.smith` (legal before d429de0e5338, which
   validated names by length alone) 500s on message create, session peer add,
   and peer update.

2. The `kind` flag moves from `configuration` to `internal_metadata`.
   `configuration` is user-writable — `PeerCreate`/`PeerUpdate` accept a
   free-form dict and `update_peer` replaces it wholesale — so a legitimate
   `{"observe_me": true}` update silently dropped the flag, and a forged
   `{"kind": "scope"}` injected an ordinary peer into `POST /scopes/list`.
   `internal_metadata` appears in no API schema. `observe_me: false` stays in
   `configuration`, where it belongs.

3. Scope identity requires prefix AND flag, via `is_scope_peer()` and
   `scope_peer_clause()`. Neither half is forgeable: the prefix sits outside
   RESOURCE_NAME_PATTERN, `internal_metadata` is unreachable. Usage-site guards
   become flag-based so a legacy peer merely occupying the namespace keeps
   working rather than 422-ing on its own traffic; peer create and update stay
   name-based, since those must stop new names entering the namespace.
   `update_peer` now returns 422 instead of 500.

Also swaps the reserved prefix from `scope__` to `scope.`: `_` is inside
RESOURCE_NAME_PATTERN, so any tenant could already own a `scope__x` peer.

No DB migration — `internal_metadata` already exists on `peers`, and no scope
peers exist in any deployment yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(scopes): validate peer names on create, close namespace squatting and upsert race

Addresses three review findings against 48047a6a.

1. `PeerSpec` let API callers create invalid and reserved-prefix peers.
   Widening `get_or_create_peers` to accept a pattern-free schema fixed the
   lookup 500s but also removed validation from the *insert* path, and
   request-controlled names reach it via message authors, session peer maps, and
   the chat observer path — none of which carry a charset pattern of their own.
   Confirmed: `POST /sessions/{id}/messages` with `peer_id: "scope.x"` returned
   201 and minted an unflagged squatter, after which `POST /scopes {id: x}` was
   permanently 409-blocked — namespace denial of service by any caller able to
   post a message. `peer_id: "not a valid name!@#"` was likewise created.

   Fixed by validating only names about to be INSERTed
   (`_validate_new_peer_names`), so already-existing names — legacy dotted
   names, scope peers — still resolve without a spurious 422. That keeps the
   Dreamer fix intact, since it reads through `get_peer`.

2. Existing reserved-prefix squatters could not be updated. The name-based guard
   on `PUT /peers/{peer_id}` refused every `scope.` name, contradicting the
   invariant that an unflagged squatter stays a normal peer. Now flag-based, so
   behavior is three-way: a real scope is refused, an existing unflagged peer
   updates, and a missing reserved-prefix name is refused by (1) rather than
   minted.

3. Scope checks raced with get-or-create and the membership upsert. The
   route-level guards run before peers are resolved, so a scope created
   concurrently in that window would be attached by the generic path with a
   default `SessionPeerConfig()`, clobbering its observer membership config.
   Adds `_reject_resolved_scope_peers`, which runs on the resolved rows in the
   same transaction as the upsert — no window, no extra query. The early checks
   stay for better error messages.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(scopes): guard scope membership config, move checks to the mutation point

Addresses a second review pass against 10655792.

1. Scope membership configuration was directly user-mutable.
   `PUT /sessions/{id}/peers/{peer_id}/config` had no scope guard at all, and
   `crud.set_peer_config` resolved the peer only to discard the row. Confirmed:
   posting `{"observe_others": false, "observe_me": true}` for a scope returned
   204 and persisted, which silently stops all fan-out into the scope and makes
   Honcho form a representation *of* a scope — neither of which is reachable
   through the facade. Deterministic, no race required. Now checked on the row
   `get_peer` already returns, so it costs nothing and cannot race.

2. Empty and over-long names were still 500s. Removing the charset pattern from
   `PeerSpec` fixed one trap but left its length bounds, and request-bound peer
   names carry no length limits of their own — so `peer_id: ""` or a 513-char
   name reached `PeerSpec(...)` and raised a raw pydantic ValidationError that
   the catch-all turned into a 500. `PeerSpec` now carries no constraints at all
   (matching its documented purpose) and every rule for a new name lives in
   `_validate_new_peer_names` on the insert path.

3. Resolved-row protection generalized. The previous pass applied it only to
   membership upserts, leaving check-then-use windows elsewhere: peer update
   could have a concurrently-created scope's configuration replaced wholesale
   (create-path validation does not fire for a peer that now exists), the chat
   observer get-or-create could resolve a fresh scope as its observer, and the
   generic session-peer removal could silently detach a scope from its sessions.
   Each now inspects the resolved peer immediately before acting; the redundant
   name-level guard on the update route is dropped in favor of the race-free one.

`remove_peers_from_session` grows an internal `_allow_scope_peers` flag because
the scopes facade ends membership through that same path and must not be blocked
by its own guard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(scopes): enumerate every peer-touching route against a scope policy

Three review passes each found the same class of defect: a route nobody had
checked, rather than logic that was subtly wrong. One of them — `PUT
/sessions/{id}/peers/{id}/config`, which let any caller set a scope to
`observe_others=false` and silently stop all fan-out into it — predated this work
entirely, because the guardrail set was assembled guardrail-by-guardrail instead
of derived from the route list. Sampling review cannot close that kind of gap;
enumeration can.

Derives every route through which a peer name can reach the system and requires
each to be classified as GUARDED or EXEMPT-with-a-reason, so a newly added
peer-touching route fails the suite until someone classifies it. Detection is the
union of path shape and a walk of the dependant tree (including sub-dependency
`Form(...)` params and nested request-body models), because neither signal alone
suffices: parameter names miss `POST /sessions/{id}/peers`, whose peer names are
dict keys, and path shape misses `messages/upload`, whose `peer_id` arrives as a
form field behind a parser dependency.

Both invariants are then asserted behaviorally, by calling the routes rather than
inspecting annotations — the guards deliberately live in crud, which is what makes
`messages/upload` guarded for free via `crud.create_messages`:

- a real scope is refused on all 11 guarded routes, and the rejection must name
  the scope, so an unrelated 422 (a malformed body) cannot pass the assertion;
- an *unflagged* peer merely occupying the reserved namespace is unaffected. That
  half regressed once already when `update_peer` used a name-based check.

Mutation-tested all three failure modes: disabling the `set_peer_config` guard
fails the guarded test naming that route; regressing `update_peer` to name-based
fails the squatter test; adding an unclassified peer route fails the enumeration.

Covers the HTTP surface only. Peer names also reach the system through the
deriver, dreamer, and queue, which have no route table to enumerate — noted in
the module docstring rather than implied to be covered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(scopes): refuse a scope in every observed position; enumerate per position

Addresses a fourth review pass against 62681e4d. The headline finding is that the
previous commit's enumeration test had the wrong *model*, not a missing entry.

1. Manual conclusions could create knowledge about a scope. `POST /conclusions`
   validated only that observer_id and observed_id exist, so a scope as
   `observed_id` persisted a conclusion about a peer carrying observe_me=false and
   created an (observer, scope) collection for it. Confirmed: 201, and it read
   back. `POST /schedule_dream` had the same hole via `observed`.

   The fix is positional, because the invariant is:

       A scope may be an OBSERVER. A scope may never be OBSERVED.

   A scope as `observer_id` is how scoped conclusions are stored and must keep
   working (verified still 201); as `observed_id` it is now refused. Same split
   applied to schedule_dream `observed`, the peer-card `target` (which also covers
   a scope's self-card, since target-omitted collapses observed to peer_id), and
   session-context `peer_target`.

2. Chat target and both representation roles kept check-to-use races. Only the
   chat path-level observer was re-checked on its resolved row; the target was
   checked by name and then resolved without inspecting scope identity. Both are
   now checked at the dialectic preflight, where observer and observed are already
   resolved — an absent name has already failed by then, and an existing squatter
   cannot retroactively become a scope.

3. Generic membership removal was still racy. The adjacent SELECT narrowed the
   window but could not close it under READ COMMITTED. The UPDATE now carries its
   own correlated NOT EXISTS against scope_peer_clause(), so Postgres evaluates
   the exclusion as part of the statement and a scope committed after the advisory
   check still cannot be detached.

4. New-name validation ran after the name reached Postgres. A NUL byte passed the
   request schemas and PeerSpec, then raised psycopg.DataError inside the lookup —
   a 500. Values that cannot correspond to a stored row by construction (NUL
   bytes, over-length names) are now refused before the query.
   (Over-length names already returned 422; only the wasted query was real there.)

The enumeration test is rekeyed from (method, path) to (method, path, position).
A binary per-route verdict cannot express finding 1 at all: `POST /conclusions` is
one route with two positions and opposite verdicts. Detection widens to observer /
observed / target / peer_target / peer_perspective, which surfaced four routes the
previous version never saw — conclusions, schedule_dream, queue/status, and
session context.

Also registers `src.routers.workspaces.tracked_db` in the conftest patch list; the
new guard there would otherwise have run against the real configured database
instead of the per-test one.

Mutation-tested: disabling the conclusions observed-guard fails the positional
test naming that position; adding an unclassified `observed_id` param fails
enumeration.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(scopes): refuse future scopes in observed positions, preserve scope membership

Addresses a fifth review pass against 14136e5b. All six findings reproduced
locally before fixing.

1. High — generic peer replacement removed scope memberships.
   `set_peers_for_session` soft-deleted every active SessionPeer row, and the
   request-level guard only inspected names *present* in the replacement map. A
   caller detached a scope by simply omitting it, never naming it — so no
   request-level guard could ever see it. Reproduced: scope sessions went
   ['<id>'] -> [] on a 200. The exclusion now lives in the UPDATE itself
   (correlated NOT EXISTS against scope_peer_clause), so replacement means
   "replace ordinary peers" regardless of request contents or concurrent creation.

2. High — peer cards could be pre-seeded for future scopes.
   `set_peer_card` resolves only the observer and writes a JSONB key derived from
   an unchecked observed name, and the route guard rejected only *existing*
   flagged scopes. Reproduced: PUT card with target=scope.<missing> returned 200,
   creating that scope then returned 201, and the card described the real scope.

3. High — dreams could be queued for future scopes.
   The route checked `observed` in a read-only session that closed before
   `enqueue_dream`, and a missing reserved name passes any is-it-a-scope check.
   Reproduced: 204 with observed=scope.<missing>.

   2 and 3 share a root cause, so they share a fix: a new `reject_scope_observed`
   that is stricter than `reject_scope_peers` in exactly one case — a *missing*
   reserved name is refused, because nothing on these paths creates the peer, so
   nothing else would ever catch it. Existing unflagged squatters still pass.
   Both guards moved to the mutation point: card validation into
   `crud.set_peer_card` (same transaction as the JSONB write, so Dreamer and
   agent-tool callers are covered), dream validation into `enqueue_dream` (same
   transaction as the queue insert). The redundant route-level checks are dropped
   rather than left as weaker duplicates.

4. Medium — prefixed NUL names still reached PostgreSQL.
   `reject_scope_peers` filtered for the reserved prefix and sent matches to a
   text comparison, so "scope.future\0name" raised psycopg.DataError — a 500.
   Both guards now share `_reserved_name_candidates`, which materializes the input
   once and rejects impossible values before any SQL. Materializing matters
   independently: the message-author path passes a generator, and validation
   iterates separately from the prefix filter, so a generator would be
   half-consumed. `_reject_impossible_peer_names` now takes a Collection so the
   type checker enforces that.

5. Medium — representation kept a check-to-use race.
   The previous commit claimed both representation roles were rechecked after
   resolution; that was wrong — only the dialectic preflight got that check, and
   the representation route never goes through it. It now opens one short
   read-only session *after* the embedding call, checks both positions, and passes
   that same session to `get_working_representation`, so no connection is held
   across external work and a scope committed later cannot have conclusions in the
   collection being read.

6. Low — policy coverage was not exhaustive. `sender_id` reaches CRUD as
   `observed` but was missing from the detected parameter set. ALLOW cases could
   also not carry builders, so the suite never proved the other half of the
   contract — that legitimate scope *observers* keep working, which a guard
   rejecting scopes everywhere would satisfy. Both fixed; observer positions on
   conclusions, dreams, cards, session context and queue status are now asserted
   behaviorally.

Deliberately not implemented: the scope-creation backstop scanning for
pre-existing card keys and queue items naming a future backing peer. Reasoning is
recorded in `get_or_create_scopes` — no new such state can be created now, any
pre-existing row is coincidental since `scope.` was never a meaningful namespace,
the consequence is inert, and detecting card keys means a full table scan per
scope creation.

Mutation-tested each new guard: removing the replacement exclusion fails both
membership-preservation tests; weakening either observed guard to existing-only
fails the pre-seeding tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(scopes): exclude scope memberships from the session observer limit

Scope memberships carry observe_others=true, so every scope counted against
SESSION_OBSERVERS_LIMIT (default 10) — capping scopes-per-session at the limit
minus the session's real observers, and reporting the failure as
`400 Cannot create session <name> with 11 observers. ... Observers are peers
with 'observe_others' set to true.` on a membership call. Wrong on three counts:
the ceiling is undocumented and contradicts RFC §5.1 ("sessions belong to any
number of scopes"), the message describes session creation, and it leaks the
word "observer" through a facade whose entire job is hiding observers (RFC
goal 5). The limit exists to bound per-observer deriver fan-out for real peers;
a scope costs document rows, not LLM calls (RFC §5.2), so it does not belong in
that budget.

Excluded from both halves of the check in `_get_or_add_peers_to_session`: the
incoming names via a flag-based lookup, existing memberships via a correlated
NOT EXISTS on `scope_peer_clause()` — the same pattern the replacement and
removal paths already use, so the exclusion holds regardless of concurrent
scope creation. The early `count_observers_in_config(session.peer_names)` check
in `get_or_create_session` is left alone: `peer_names` cannot contain a scope,
and `scopes` is a separate field.

`reject_scope_peers` is split into a `scope_peer_names()` query helper plus a
two-line raiser so the observer count reuses the authoritative name-AND-flag
predicate instead of growing a third copy of it. Still costs nothing on the
common path — no reserved-prefix name in the input means no query at all.

Also caps `SessionCreate.scopes` at 100, matching `ScopeSessionsAdd.session_ids`.
This belongs in the same commit: the observer limit was the only thing bounding
that list, so removing it turns an unbounded `scopes` array into a peer row and
a membership row per element, committed — the single-request path to the
cardinality anti-pattern RFC §8 warns about. Partly answers OQ6: no per-session
cap, 100 per request.

Tests: a session joins SESSION_OBSERVERS_LIMIT + 2 scopes through both the
facade and session creation; real observers over the limit still 400, so the
carve-out cannot quietly disable the limit; 101 scopes is a 422.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(scopes): skip semantic retrieval when the embedding precompute failed

Addresses three open review comments.

1. Major — the representation read could embed inside its DB session. The
   route's precompute is suppressed, and both
   `RepresentationManager.get_working_representation` and
   `crud.query_documents` fall back to embedding when a query arrives without
   one, so a failed precompute meant an external call inside the read session
   this branch opens for the scope re-check — the connection-holding rule the
   route's own comment claimed to satisfy. The innermost fallback also only
   catches ValueError, so a provider outage surfaced as a 500. The semantic
   query is now passed only when an embedding exists, degrading to
   derived+recent retrieval. (`crud.query_documents` embedding inside a caller's
   session predates this branch and is left alone.)

2. Minor — `test_resolved_scope_peer_rejected_at_membership_upsert` described a
   race it does not perform. It creates an already-flagged scope and calls crud
   directly; the unflagged → flagged transition is not simulated. Docstring now
   says what the test actually pins.

3. Minor — `test_empty_replacement_preserves_scope_membership` asserted only
   half its docstring. It passed if the empty PUT left every ordinary
   membership intact; now asserts the ordinary peer's left_at is set.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(scopes): close auth and observed-position gaps, paginate membership

Review response for #884.

Security:
- gate `SessionCreate.scopes` behind a workspace-level key; the session-create
  route is self-authorizing, so a peer- or session-scoped token could mint scope
  peers and join sessions to scopes it had no access to via `POST /scopes`
- refuse a reserved-but-nonexistent name in two observed positions that used the
  permissive guard: chat `target` and session-context `peer_target`. Both let a
  caller act on `scope.X` before it existed, then create the scope

Facade:
- exclude scope peers from `GET /sessions/{id}/peers` and refuse the membership
  -config read for a real scope, matching its write side
- replace `GET /scopes/{id}/sessions` with `POST /scopes/{id}/sessions/list`
  returning `Page[Session]`; the add route now returns 204. Membership was
  unbounded on both, while every other list surface paginates
- rename `crud.get_scope` to `get_scope_or_raise`

Tests:
- add a missing-name axis to the route-policy table (`Case.refuse_missing`), which
  is what surfaced the two guard gaps above
- delete 14 hand-written tests the table now enumerates; 52 -> 39 functions in
  test_scopes.py with more cases covered
- tighten the squatter assertion from `!= 422` to `< 400`, which was passing on 5xx
- assert the FastAPI-internals traversal still derives positions, so a framework
  upgrade can't silently empty the suite

Docs:
- drop internal ticket and RFC references from the published OpenAPI descriptions
  and surrounding comments; state the behavior instead
- move implementation reasoning out of the `PUT /peers/{id}` docstring, which
  FastAPI publishes, into a comment

* test(scopes): assert exact statuses for permissive missing-name cases

Follow-up review pass on #884.

- add `Case.missing_status` so a permissive missing-name position asserts the
  status it should actually get (404, or 200 for the no-op removal) instead of
  `!= 422`, which also passed on a 5xx — the same hole already closed in the
  squatter assertion
- require it whenever `refuse_missing` is False, and require its absence when
  True, so the policy table can't drift from the assertion
- repoint a stale allow-reason at POST /scopes/{scope_id}/sessions/list; the GET
  it named was removed
- document the membership list's ordering under `reverse`

* fix: Address Coderabbit Comments

* fix: Address more coderabbit comments

* chore: remove ai artifacting

* fix: remove unused fakeredis client in lieu of in-memory taskless cache for test suite

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 18:34:55 -04:00
Vineeth Voruganti 57ae7ef4d8
August Changelog Docs Sync (#1009)
* chore(docs): Sync Docs with latest Changelog features

* chore: nit on sentence style

* chore: simplifying language.

---------

Co-authored-by: ajspig <dragon@monstercode.com>
2026-08-12 17:35:51 -04:00
Vineeth Voruganti 81d8409b8c
Scopes Phase 2a: scope-kind peers, guardrails, and scopes CRUD routes (#884)
* feat: reserve scope__ peer namespace with kind flag and guardrails

Introduce the scope peer namespace (scope__<name>) and the authoritative
{"kind": "scope"} configuration flag, plus the server-side guardrails:

- src/utils/scopes.py: single source of truth for the prefix, kind flag,
  and name helpers (scope_peer_name / is_scope_peer_name /
  scope_name_from_peer / validate_no_scope_peer_names)
- reject reserved-prefix names on peer get-or-create (422)
- reject scope peers as message authors in crud.create_messages (422)
- reject scope peers as chat/representation targets (422); a scope peer
  as the path-level observer is deferred to Phase 2b
- reject scope peers on the generic session-peer add/set/remove routes
  and the session-create peers mapping (422, directing to scopes routes)
- peers.list excludes scope peers by default; new PeerGet.kind option
  ("scope" | "all") switches the view via a configuration JSONB filter
- schemas: Scope / ScopeCreate / ScopeSessions(Add) and
  SessionCreate.scopes (unprefixed scope names, validated)

Part of DEV-1997 (Scopes RFC DEV-1970).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: add scopes CRUD routes and session-create scopes wiring

New /v3/workspaces/{workspace_id}/scopes facade (workspace-level auth;
peer- and session-scoped keys are rejected):

  POST   ""                                   create-or-get (201/200)
  POST   /list                                 paginated scope list
  GET    /{scope_id}                           single scope
  POST   /{scope_id}/sessions                  add memberships
  DELETE /{scope_id}/sessions/{session_id}     remove membership
  GET    /{scope_id}/sessions                  list member session ids

- crud/scope.py: get_or_create_scopes stamps the backing peer with
  {"kind": "scope", "observe_me": false} and refuses to adopt a
  legacy peer occupying the reserved name without the flag (409)
- memberships are session_peers rows with observe_others=true /
  observe_me=false — identical to a hand-built observer peer
- SessionCreate.scopes: create-or-get each scope peer and add the
  membership at session creation (the no-backfill common path)
- crud/session.py: public upsert_session_peers wrapper so the facade
  bypasses the route-level guardrails without reaching into privates

Backfill of pre-existing documents and reconciliation on removal land in
DEV-1999; membership only affects messages ingested after the change.

Part of DEV-1997 (Scopes RFC DEV-1970).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: cover scopes facade, guardrails, and observer semantics

- create-or-get idempotency, list/get, name validation, legacy-collision
  rejection (409), auth scoping (workspace key ok, peer/session keys 401)
- reserved prefix rejected on peer create; peers.list kind filtering
- scope peers rejected as message authors, chat/representation targets,
  and on the generic session-peer routes
- membership add/list/remove with observe_others=true / observe_me=false
  row shape asserted via DB, and facade-less equivalence with a
  hand-built observer peer
- end-to-end litmus: after adding a session to a scope, the deriver
  enqueue fan-out includes the scope peer as an observer
- session creation with scopes: [a, b] creates both memberships

Part of DEV-1997 (Scopes RFC DEV-1970).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(crud): preserve cache invalidation across get_or_create retry

`get_or_create_peers` and `get_or_create_scopes` mutate existing rows, then
insert new ones inside `db.begin_nested()`. When a concurrent writer creates
one of those rows first, the insert raises IntegrityError and the function
retries.

`begin_nested()` autoflushes the pending UPDATEs *before* opening the
savepoint, so the rollback neither undoes them nor expires the now-clean ORM
state. The retry then compared already-updated values, found no change, and
dropped those peers from `changed_peers` — skipping the cache purge while the
row change committed anyway, leaving entries stale until the 300s TTL.

Carry the mutated names into the retry via `_pending_invalidation` so the
purge cannot be lost.

The scopes facade mirrors `get_or_create_peers`, so both copies carried this.
The peer path is pre-existing and runs on every message ingest.

Also add the missing /v3 prefix to `_SCOPES_ROUTE_GUIDANCE`, which pointed
callers at a 404.

Adds tests/crud/test_get_or_create_retry_invalidation.py, which drives a real
racing session and fails without this change.

* fix(scopes): make scope identity unforgeable, unblock non-pattern peer names

Three coupled changes to the scopes facade.

1. `PeerCreate` no longer gates internal lookups. It exists to validate a new,
   user-supplied peer id at the API boundary, but crud used it as a DTO for
   names that already exist, so any name outside RESOURCE_NAME_PATTERN raised a
   raw pydantic ValidationError — which is not a HonchoException, so it fell
   through to the catch-all handler as an HTTP 500. Adds `PeerSpec` (same
   fields, no charset pattern) as `PeerCreate`'s base, widens
   `get_or_create_peers` to accept it, and changes `get_peer` to take a plain
   str. All 13 construction sites converted; the create route keeps full
   validation.

   This unbreaks the Dreamer: DreamScheduler passes `collection.observer`
   straight into the specialist preflight, and scope peers have
   `observe_others=true`, so every `(scope.x, peer)` dream died there — the
   feature scopes exist to enable. It also fixes a pre-existing bug unrelated
   to scopes: a peer named `alice.smith` (legal before d429de0e5338, which
   validated names by length alone) 500s on message create, session peer add,
   and peer update.

2. The `kind` flag moves from `configuration` to `internal_metadata`.
   `configuration` is user-writable — `PeerCreate`/`PeerUpdate` accept a
   free-form dict and `update_peer` replaces it wholesale — so a legitimate
   `{"observe_me": true}` update silently dropped the flag, and a forged
   `{"kind": "scope"}` injected an ordinary peer into `POST /scopes/list`.
   `internal_metadata` appears in no API schema. `observe_me: false` stays in
   `configuration`, where it belongs.

3. Scope identity requires prefix AND flag, via `is_scope_peer()` and
   `scope_peer_clause()`. Neither half is forgeable: the prefix sits outside
   RESOURCE_NAME_PATTERN, `internal_metadata` is unreachable. Usage-site guards
   become flag-based so a legacy peer merely occupying the namespace keeps
   working rather than 422-ing on its own traffic; peer create and update stay
   name-based, since those must stop new names entering the namespace.
   `update_peer` now returns 422 instead of 500.

Also swaps the reserved prefix from `scope__` to `scope.`: `_` is inside
RESOURCE_NAME_PATTERN, so any tenant could already own a `scope__x` peer.

No DB migration — `internal_metadata` already exists on `peers`, and no scope
peers exist in any deployment yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(scopes): validate peer names on create, close namespace squatting and upsert race

Addresses three review findings against 48047a6a.

1. `PeerSpec` let API callers create invalid and reserved-prefix peers.
   Widening `get_or_create_peers` to accept a pattern-free schema fixed the
   lookup 500s but also removed validation from the *insert* path, and
   request-controlled names reach it via message authors, session peer maps, and
   the chat observer path — none of which carry a charset pattern of their own.
   Confirmed: `POST /sessions/{id}/messages` with `peer_id: "scope.x"` returned
   201 and minted an unflagged squatter, after which `POST /scopes {id: x}` was
   permanently 409-blocked — namespace denial of service by any caller able to
   post a message. `peer_id: "not a valid name!@#"` was likewise created.

   Fixed by validating only names about to be INSERTed
   (`_validate_new_peer_names`), so already-existing names — legacy dotted
   names, scope peers — still resolve without a spurious 422. That keeps the
   Dreamer fix intact, since it reads through `get_peer`.

2. Existing reserved-prefix squatters could not be updated. The name-based guard
   on `PUT /peers/{peer_id}` refused every `scope.` name, contradicting the
   invariant that an unflagged squatter stays a normal peer. Now flag-based, so
   behavior is three-way: a real scope is refused, an existing unflagged peer
   updates, and a missing reserved-prefix name is refused by (1) rather than
   minted.

3. Scope checks raced with get-or-create and the membership upsert. The
   route-level guards run before peers are resolved, so a scope created
   concurrently in that window would be attached by the generic path with a
   default `SessionPeerConfig()`, clobbering its observer membership config.
   Adds `_reject_resolved_scope_peers`, which runs on the resolved rows in the
   same transaction as the upsert — no window, no extra query. The early checks
   stay for better error messages.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(scopes): guard scope membership config, move checks to the mutation point

Addresses a second review pass against 10655792.

1. Scope membership configuration was directly user-mutable.
   `PUT /sessions/{id}/peers/{peer_id}/config` had no scope guard at all, and
   `crud.set_peer_config` resolved the peer only to discard the row. Confirmed:
   posting `{"observe_others": false, "observe_me": true}` for a scope returned
   204 and persisted, which silently stops all fan-out into the scope and makes
   Honcho form a representation *of* a scope — neither of which is reachable
   through the facade. Deterministic, no race required. Now checked on the row
   `get_peer` already returns, so it costs nothing and cannot race.

2. Empty and over-long names were still 500s. Removing the charset pattern from
   `PeerSpec` fixed one trap but left its length bounds, and request-bound peer
   names carry no length limits of their own — so `peer_id: ""` or a 513-char
   name reached `PeerSpec(...)` and raised a raw pydantic ValidationError that
   the catch-all turned into a 500. `PeerSpec` now carries no constraints at all
   (matching its documented purpose) and every rule for a new name lives in
   `_validate_new_peer_names` on the insert path.

3. Resolved-row protection generalized. The previous pass applied it only to
   membership upserts, leaving check-then-use windows elsewhere: peer update
   could have a concurrently-created scope's configuration replaced wholesale
   (create-path validation does not fire for a peer that now exists), the chat
   observer get-or-create could resolve a fresh scope as its observer, and the
   generic session-peer removal could silently detach a scope from its sessions.
   Each now inspects the resolved peer immediately before acting; the redundant
   name-level guard on the update route is dropped in favor of the race-free one.

`remove_peers_from_session` grows an internal `_allow_scope_peers` flag because
the scopes facade ends membership through that same path and must not be blocked
by its own guard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(scopes): enumerate every peer-touching route against a scope policy

Three review passes each found the same class of defect: a route nobody had
checked, rather than logic that was subtly wrong. One of them — `PUT
/sessions/{id}/peers/{id}/config`, which let any caller set a scope to
`observe_others=false` and silently stop all fan-out into it — predated this work
entirely, because the guardrail set was assembled guardrail-by-guardrail instead
of derived from the route list. Sampling review cannot close that kind of gap;
enumeration can.

Derives every route through which a peer name can reach the system and requires
each to be classified as GUARDED or EXEMPT-with-a-reason, so a newly added
peer-touching route fails the suite until someone classifies it. Detection is the
union of path shape and a walk of the dependant tree (including sub-dependency
`Form(...)` params and nested request-body models), because neither signal alone
suffices: parameter names miss `POST /sessions/{id}/peers`, whose peer names are
dict keys, and path shape misses `messages/upload`, whose `peer_id` arrives as a
form field behind a parser dependency.

Both invariants are then asserted behaviorally, by calling the routes rather than
inspecting annotations — the guards deliberately live in crud, which is what makes
`messages/upload` guarded for free via `crud.create_messages`:

- a real scope is refused on all 11 guarded routes, and the rejection must name
  the scope, so an unrelated 422 (a malformed body) cannot pass the assertion;
- an *unflagged* peer merely occupying the reserved namespace is unaffected. That
  half regressed once already when `update_peer` used a name-based check.

Mutation-tested all three failure modes: disabling the `set_peer_config` guard
fails the guarded test naming that route; regressing `update_peer` to name-based
fails the squatter test; adding an unclassified peer route fails the enumeration.

Covers the HTTP surface only. Peer names also reach the system through the
deriver, dreamer, and queue, which have no route table to enumerate — noted in
the module docstring rather than implied to be covered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(scopes): refuse a scope in every observed position; enumerate per position

Addresses a fourth review pass against 62681e4d. The headline finding is that the
previous commit's enumeration test had the wrong *model*, not a missing entry.

1. Manual conclusions could create knowledge about a scope. `POST /conclusions`
   validated only that observer_id and observed_id exist, so a scope as
   `observed_id` persisted a conclusion about a peer carrying observe_me=false and
   created an (observer, scope) collection for it. Confirmed: 201, and it read
   back. `POST /schedule_dream` had the same hole via `observed`.

   The fix is positional, because the invariant is:

       A scope may be an OBSERVER. A scope may never be OBSERVED.

   A scope as `observer_id` is how scoped conclusions are stored and must keep
   working (verified still 201); as `observed_id` it is now refused. Same split
   applied to schedule_dream `observed`, the peer-card `target` (which also covers
   a scope's self-card, since target-omitted collapses observed to peer_id), and
   session-context `peer_target`.

2. Chat target and both representation roles kept check-to-use races. Only the
   chat path-level observer was re-checked on its resolved row; the target was
   checked by name and then resolved without inspecting scope identity. Both are
   now checked at the dialectic preflight, where observer and observed are already
   resolved — an absent name has already failed by then, and an existing squatter
   cannot retroactively become a scope.

3. Generic membership removal was still racy. The adjacent SELECT narrowed the
   window but could not close it under READ COMMITTED. The UPDATE now carries its
   own correlated NOT EXISTS against scope_peer_clause(), so Postgres evaluates
   the exclusion as part of the statement and a scope committed after the advisory
   check still cannot be detached.

4. New-name validation ran after the name reached Postgres. A NUL byte passed the
   request schemas and PeerSpec, then raised psycopg.DataError inside the lookup —
   a 500. Values that cannot correspond to a stored row by construction (NUL
   bytes, over-length names) are now refused before the query.
   (Over-length names already returned 422; only the wasted query was real there.)

The enumeration test is rekeyed from (method, path) to (method, path, position).
A binary per-route verdict cannot express finding 1 at all: `POST /conclusions` is
one route with two positions and opposite verdicts. Detection widens to observer /
observed / target / peer_target / peer_perspective, which surfaced four routes the
previous version never saw — conclusions, schedule_dream, queue/status, and
session context.

Also registers `src.routers.workspaces.tracked_db` in the conftest patch list; the
new guard there would otherwise have run against the real configured database
instead of the per-test one.

Mutation-tested: disabling the conclusions observed-guard fails the positional
test naming that position; adding an unclassified `observed_id` param fails
enumeration.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(scopes): refuse future scopes in observed positions, preserve scope membership

Addresses a fifth review pass against 14136e5b. All six findings reproduced
locally before fixing.

1. High — generic peer replacement removed scope memberships.
   `set_peers_for_session` soft-deleted every active SessionPeer row, and the
   request-level guard only inspected names *present* in the replacement map. A
   caller detached a scope by simply omitting it, never naming it — so no
   request-level guard could ever see it. Reproduced: scope sessions went
   ['<id>'] -> [] on a 200. The exclusion now lives in the UPDATE itself
   (correlated NOT EXISTS against scope_peer_clause), so replacement means
   "replace ordinary peers" regardless of request contents or concurrent creation.

2. High — peer cards could be pre-seeded for future scopes.
   `set_peer_card` resolves only the observer and writes a JSONB key derived from
   an unchecked observed name, and the route guard rejected only *existing*
   flagged scopes. Reproduced: PUT card with target=scope.<missing> returned 200,
   creating that scope then returned 201, and the card described the real scope.

3. High — dreams could be queued for future scopes.
   The route checked `observed` in a read-only session that closed before
   `enqueue_dream`, and a missing reserved name passes any is-it-a-scope check.
   Reproduced: 204 with observed=scope.<missing>.

   2 and 3 share a root cause, so they share a fix: a new `reject_scope_observed`
   that is stricter than `reject_scope_peers` in exactly one case — a *missing*
   reserved name is refused, because nothing on these paths creates the peer, so
   nothing else would ever catch it. Existing unflagged squatters still pass.
   Both guards moved to the mutation point: card validation into
   `crud.set_peer_card` (same transaction as the JSONB write, so Dreamer and
   agent-tool callers are covered), dream validation into `enqueue_dream` (same
   transaction as the queue insert). The redundant route-level checks are dropped
   rather than left as weaker duplicates.

4. Medium — prefixed NUL names still reached PostgreSQL.
   `reject_scope_peers` filtered for the reserved prefix and sent matches to a
   text comparison, so "scope.future\0name" raised psycopg.DataError — a 500.
   Both guards now share `_reserved_name_candidates`, which materializes the input
   once and rejects impossible values before any SQL. Materializing matters
   independently: the message-author path passes a generator, and validation
   iterates separately from the prefix filter, so a generator would be
   half-consumed. `_reject_impossible_peer_names` now takes a Collection so the
   type checker enforces that.

5. Medium — representation kept a check-to-use race.
   The previous commit claimed both representation roles were rechecked after
   resolution; that was wrong — only the dialectic preflight got that check, and
   the representation route never goes through it. It now opens one short
   read-only session *after* the embedding call, checks both positions, and passes
   that same session to `get_working_representation`, so no connection is held
   across external work and a scope committed later cannot have conclusions in the
   collection being read.

6. Low — policy coverage was not exhaustive. `sender_id` reaches CRUD as
   `observed` but was missing from the detected parameter set. ALLOW cases could
   also not carry builders, so the suite never proved the other half of the
   contract — that legitimate scope *observers* keep working, which a guard
   rejecting scopes everywhere would satisfy. Both fixed; observer positions on
   conclusions, dreams, cards, session context and queue status are now asserted
   behaviorally.

Deliberately not implemented: the scope-creation backstop scanning for
pre-existing card keys and queue items naming a future backing peer. Reasoning is
recorded in `get_or_create_scopes` — no new such state can be created now, any
pre-existing row is coincidental since `scope.` was never a meaningful namespace,
the consequence is inert, and detecting card keys means a full table scan per
scope creation.

Mutation-tested each new guard: removing the replacement exclusion fails both
membership-preservation tests; weakening either observed guard to existing-only
fails the pre-seeding tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(scopes): exclude scope memberships from the session observer limit

Scope memberships carry observe_others=true, so every scope counted against
SESSION_OBSERVERS_LIMIT (default 10) — capping scopes-per-session at the limit
minus the session's real observers, and reporting the failure as
`400 Cannot create session <name> with 11 observers. ... Observers are peers
with 'observe_others' set to true.` on a membership call. Wrong on three counts:
the ceiling is undocumented and contradicts RFC §5.1 ("sessions belong to any
number of scopes"), the message describes session creation, and it leaks the
word "observer" through a facade whose entire job is hiding observers (RFC
goal 5). The limit exists to bound per-observer deriver fan-out for real peers;
a scope costs document rows, not LLM calls (RFC §5.2), so it does not belong in
that budget.

Excluded from both halves of the check in `_get_or_add_peers_to_session`: the
incoming names via a flag-based lookup, existing memberships via a correlated
NOT EXISTS on `scope_peer_clause()` — the same pattern the replacement and
removal paths already use, so the exclusion holds regardless of concurrent
scope creation. The early `count_observers_in_config(session.peer_names)` check
in `get_or_create_session` is left alone: `peer_names` cannot contain a scope,
and `scopes` is a separate field.

`reject_scope_peers` is split into a `scope_peer_names()` query helper plus a
two-line raiser so the observer count reuses the authoritative name-AND-flag
predicate instead of growing a third copy of it. Still costs nothing on the
common path — no reserved-prefix name in the input means no query at all.

Also caps `SessionCreate.scopes` at 100, matching `ScopeSessionsAdd.session_ids`.
This belongs in the same commit: the observer limit was the only thing bounding
that list, so removing it turns an unbounded `scopes` array into a peer row and
a membership row per element, committed — the single-request path to the
cardinality anti-pattern RFC §8 warns about. Partly answers OQ6: no per-session
cap, 100 per request.

Tests: a session joins SESSION_OBSERVERS_LIMIT + 2 scopes through both the
facade and session creation; real observers over the limit still 400, so the
carve-out cannot quietly disable the limit; 101 scopes is a 422.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(scopes): skip semantic retrieval when the embedding precompute failed

Addresses three open review comments.

1. Major — the representation read could embed inside its DB session. The
   route's precompute is suppressed, and both
   `RepresentationManager.get_working_representation` and
   `crud.query_documents` fall back to embedding when a query arrives without
   one, so a failed precompute meant an external call inside the read session
   this branch opens for the scope re-check — the connection-holding rule the
   route's own comment claimed to satisfy. The innermost fallback also only
   catches ValueError, so a provider outage surfaced as a 500. The semantic
   query is now passed only when an embedding exists, degrading to
   derived+recent retrieval. (`crud.query_documents` embedding inside a caller's
   session predates this branch and is left alone.)

2. Minor — `test_resolved_scope_peer_rejected_at_membership_upsert` described a
   race it does not perform. It creates an already-flagged scope and calls crud
   directly; the unflagged → flagged transition is not simulated. Docstring now
   says what the test actually pins.

3. Minor — `test_empty_replacement_preserves_scope_membership` asserted only
   half its docstring. It passed if the empty PUT left every ordinary
   membership intact; now asserts the ordinary peer's left_at is set.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(scopes): close auth and observed-position gaps, paginate membership

Review response for #884.

Security:
- gate `SessionCreate.scopes` behind a workspace-level key; the session-create
  route is self-authorizing, so a peer- or session-scoped token could mint scope
  peers and join sessions to scopes it had no access to via `POST /scopes`
- refuse a reserved-but-nonexistent name in two observed positions that used the
  permissive guard: chat `target` and session-context `peer_target`. Both let a
  caller act on `scope.X` before it existed, then create the scope

Facade:
- exclude scope peers from `GET /sessions/{id}/peers` and refuse the membership
  -config read for a real scope, matching its write side
- replace `GET /scopes/{id}/sessions` with `POST /scopes/{id}/sessions/list`
  returning `Page[Session]`; the add route now returns 204. Membership was
  unbounded on both, while every other list surface paginates
- rename `crud.get_scope` to `get_scope_or_raise`

Tests:
- add a missing-name axis to the route-policy table (`Case.refuse_missing`), which
  is what surfaced the two guard gaps above
- delete 14 hand-written tests the table now enumerates; 52 -> 39 functions in
  test_scopes.py with more cases covered
- tighten the squatter assertion from `!= 422` to `< 400`, which was passing on 5xx
- assert the FastAPI-internals traversal still derives positions, so a framework
  upgrade can't silently empty the suite

Docs:
- drop internal ticket and RFC references from the published OpenAPI descriptions
  and surrounding comments; state the behavior instead
- move implementation reasoning out of the `PUT /peers/{id}` docstring, which
  FastAPI publishes, into a comment

* test(scopes): assert exact statuses for permissive missing-name cases

Follow-up review pass on #884.

- add `Case.missing_status` so a permissive missing-name position asserts the
  status it should actually get (404, or 200 for the no-op removal) instead of
  `!= 422`, which also passed on a 5xx — the same hole already closed in the
  squatter assertion
- require it whenever `refuse_missing` is False, and require its absence when
  True, so the policy table can't drift from the assertion
- repoint a stale allow-reason at POST /scopes/{scope_id}/sessions/list; the GET
  it named was removed
- document the membership list's ordering under `reverse`

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 16:21:40 -04:00
Vansh Sharma 7b8c2917f9
fix(embedding): request float encoding_format on openai embedding calls (#938)
* test(embedding): add reproducer for missing encoding_format on openai paths

The openai SDK defaults encoding_format to base64 when it is not passed. OpenAI-compatible providers that don't support base64 embeddings (e.g. OpenRouter with nvidia/nemotron-3-embed-1b:free) return HTTP 200 with empty data, and every embedding call fails with 'No embedding data received'.

* fix(embedding): request float encoding_format on openai embedding calls

The openai SDK defaults encoding_format to base64 when the caller does not pass one. OpenAI-compatible providers that don't support base64 embeddings (e.g. OpenRouter hosting nvidia/nemotron-3-embed-1b:free) answer HTTP 200 with empty embedding data, and every embedding call fails with 'No embedding data received', breaking conclusions, semantic search, and the deriver. Pass encoding_format='float' explicitly on both the single-query and batch call paths.

* test(embedding): cover openai-compatible providers in the live embedding matrix

The existing openai family runs against real OpenAI, which serves base64
embeddings happily, so the matrix passes with or without the #932 fix. Adds an
`openai_compatible_embedding` family (openai transport, third-party base_url)
so the matrix can reach a provider that rejects base64. Empty default_models
keeps it skipped unless LIVE_EMBEDDING_OPENAI_COMPATIBLE_MODELS is set.

Also adds test_live_openai_float_encoding_matches_base64, which pins the other
direction: switching the wire format must not move vectors on real OpenAI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(embedding): keep an explicit embedding-count check on the openai paths

Passing `encoding_format` disables the openai SDK's own empty-data guard, so a
provider answering 200 with missing embeddings surfaced as `IndexError: list
index out of range` on the single path and `zip() argument 2 is shorter than
argument 1` on the batch path. The latter is also #745's signature, which would
have left it with two unrelated causes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(live-llm): correct the openai-compatible embedding matrix env docs

The documented default dimensions said 2048 after the family moved to 3072, and
LIVE_EMBEDDING_OPENAI_COMPATIBLE_SEND_DIMENSIONS was missing entirely. Also
points the example and the coverage note at a model that is actually reachable,
and records that OpenRouter load-balances, so the base64 failure is per-attempt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(embedding): resolve openai encoding_format by mode instead of pinning float

Requesting float unconditionally costs ~3.6x the response bytes of base64 and up
to +83% latency on a 500-item batch, which the default deployment on real OpenAI
pays for nothing: only third-party OpenAI-compatible providers reject base64.

Adds EMBEDDING_MODEL_CONFIG__ENCODING_FORMAT_MODE, mirroring dimensions_mode.
`auto` keeps base64 when no base_url override is set or it points at
api.openai.com, and picks float elsewhere. The format is still always sent
explicitly, since the SDK otherwise injects base64 on its own.

Also corrects the _validate_embedding_count docstring, which said "fewer" where
the guard is an inequality.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(embedding): request base64 embeddings by omission, not by name

The openai SDK decodes a base64 response only when it injected the default
itself; naming any format makes it skip the decode and hand back the raw string,
which then fails the dimension check with "Expected 1536, got 8192". base64 mode
therefore has to omit the kwarg rather than pass it.

The unit fake returned float lists whatever was asked for, so it could not catch
this. It now mirrors the SDK and returns a base64 string for a named base64
request, which fails against the previous commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Aakash Kattelu <aakash@plasticlabs.ai>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 13:24:38 -04:00
ajspig e6f436e69d
chore: adding server schema (#1012)
* chore: adding server schema

* chore: updating documentation

* fix: updating copy
2026-08-12 13:05:43 -04:00
Rajat Ahuja a74f2b3a1c Revert "fix: slim honcho image"
This reverts commit 6e2ca1bdc3.
2026-08-12 12:22:04 -04:00
Rajat Ahuja 6e2ca1bdc3 fix: slim honcho image 2026-08-12 12:19:14 -04:00
Niyaz Almufti a92fb1e078
Fix Gemini batch embedding for gemini-embedding-2* models (#745)
* Fix Gemini batch embedding for gemini-embedding-2* models

The google-genai SDK treats embed_content(contents=[list_of_strings]) as a
single multi-part document for gemini-embedding-2* models, silently returning
exactly 1 embedding regardless of input count. This caused a zip(...,
strict=True) ValueError in _process_batch.

Wrap each text in genai_types.Content(parts=[genai_types.Part(text=...)])
so the SDK treats each string as a separate content item. This matches the
workaround used by pydantic-ai (#4873) and graphiti (#1474).

Upstream SDK issue: googleapis/python-genai#2523
Fixes plastic-labs/honcho#744

* test(embedding): live embedding coverage for every Gemini and OpenAI model

Adds tests/live_llm/test_live_embeddings.py plus an env-driven embedding
matrix alongside the existing LLM one. Covers single embed, batched embed,
batch-vs-single alignment, chunk-to-id mapping, and the batch-split path.

Only a live call catches the gemini-embedding-2* collapse: the SDK folds a
list of bare strings into one document and returns a single embedding.
Reverting the Content wrapping fails all four batch tests for
gemini-embedding-2-preview and gemini-embedding-2 with the reported
`zip() argument 2 is shorter than argument 1`, while gemini-embedding-001
and text-embedding-3-small stay green.

Also makes the concatenation in the conclusions semantic-search validation
message explicit, so the repo-wide basedpyright pre-push hook passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(embedding): drop the preview twin from the default embedding matrix

gemini-embedding-2 is the GA release of gemini-embedding-2-preview and
behaves identically, so running both by default doubles the Gemini cost for
no extra coverage. The preview stays reachable through
LIVE_EMBEDDING_GEMINI_MODELS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Aakash Kattelu <aakash@plasticlabs.ai>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 17:50:40 -04:00
bbasketballer75 2d174a5fb2
fix(api): make the conclusions semantic-search validation error actionable (#960)
The error raised when observer/observed are missing from a semantic-search
query states the requirement but not where the values go, so callers can't
tell whether they belong at the top level or inside `filters`. Point at the
`filters` object explicitly, show a minimal well-formed payload, and note
that both the bare and `_id`-suffixed key spellings are accepted (the code
already reads either).

Co-authored-by: hermes <hermes@local>
2026-08-11 17:12:00 -04:00
Aakash Kattelu 0d57df430e
feat(cli): add `honcho session view` transcript command (#1006)
* feat(cli): add `honcho session view` transcript command

Adds a read-only transcript view for a session, with three paging modes:
a tail window (`--last N`, the default), server pages (`--page N --size M`),
and the whole conversation (`--all`). `--reverse` selects newest-first in
every mode, `--ids` exposes message IDs, and `-p` scopes to one peer.
JSON mode emits the same shape as `message list`.

The renderer is deliberately literal: content and identifiers go through
`rich.text.Text` rather than Markdown or console markup, so newlines, tag
delimiters like `<thinking>`, and bracketed text survive intact — this is a
debugging surface, so it has to show what was actually stored. Timestamps
are converted to UTC (not just stripped of their offset) and keep
millisecond precision. Nothing is truncated with an ellipsis: a displayed
message ID is always usable with `honcho message get`.

Flags are validated before the client is built, and the session is
constructed directly instead of via the get-or-create `client.session()`,
so an invalid or mistyped invocation never reaches — or creates — anything
server-side. `--size` is bounded locally to the server's 100-item ceiling
rather than surfacing a raw 422, and the "more:" hint echoes back the size
and ordering actually in use so following it lands on the adjacent window.

Also fixes `honcho message list --last N`, which stopped at the first page
of 50: both commands now share the page-walking helper, so the same flag
returns the same window either way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(cli): regenerate command reference for `session view`

Adds the generated `session view` accordion to the docs snippet and points
the session-debugging workflows at it. Trims the docstring to plain prose —
the RST double-backticks were rendering literally in `--help`, where every
other command uses unmarked flag names — and stops the generator emitting a
trailing blank line that tripped end-of-file-fixer on every regeneration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(cli): carry invocation scope into the next-page hint

Addresses CodeRabbit review on #1006.

The "more:" hint echoed only `--page`, `--size`, and `--reverse`, so copying
it off a scoped invocation dropped `-w`, `-p`, and `--ids` — landing on a
different workspace or an unfiltered transcript. Hint construction moves into
`_next_page_command` in the command module, which knows the invocation; the
renderer now just prints the string it's handed and no longer needs to know
CLI flag syntax. Only flags passed explicitly are echoed, since anything from
the environment or config resolves the same way on the next run.

Also rejects non-positive `--last` on `honcho message list`, which slice
semantics turned into a silently empty result. `session view` already errored
on it; the two now agree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(cli): read next-page hint scope from effective flag overrides

Addresses the second CodeRabbit pass on #1006.

`-w`/`-p` parse at group and top level as well as command level, all landing
in `_global_overrides`, so reading the command-level params dropped the scope
from `honcho session -w ws2 view ...`. The hint now reads the effective
overrides via a new `get_flag_overrides()`, which deliberately excludes
environment and config values since those resolve the same way on the next run.

Also shell-quotes the hint's identifiers with `shlex.join`. Note this is
hardening rather than a live injection fix: the API constrains IDs to
`^[a-zA-Z0-9_-]+$`, so an ID carrying a space or metacharacter fails the fetch
before any hint is printed. `validate_resource_id` is looser than the server
though, so quoting is the cheaper invariant to hold locally.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 10:12:16 -04:00
ajspig bd6163d9e3
docs: adding honcho-memory skill (#784)
* docs: adding honcho-memory skill

* fix: skills to point at llm friendly content

* docs: split honcho-mcp skill out of honcho-memory; address PR review

Restructure honcho-memory into a concepts/strategy hub that routes to
per-connection path skills, and add a dedicated honcho-mcp skill holding
the MCP-tool mechanics that previously lived inline.

Addresses review feedback on #784:
- honcho-memory step 2 now leads with fast context reads, with chat as
  the slower escalation
- honcho-mcp adds a "Speed: reads vs reasoning" section, describes what
  each context call returns, and a reasoning-levels table
- get_representation framed as a contextualized snapshot insertable into
  a system prompt
- drop schedule_dream from the tool table (manual escape hatch, not
  routine guidance)
- prune queue-status references from honcho-cli; document honcho-mcp in
  vibecoding skill registry

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(skills): move skills to canonical top-level skills/ with .claude symlink

Establish a single source of truth for agent skills. The real files now live in
the top-level skills/ directory (the publishing convention used by Vercel,
Supabase, and Cloudflare, and the tree Honcho's `npx skills add` distributes).
.claude/skills becomes a symlink to ../skills so Claude Code discovery keeps
working off the one tree — eliminating the parallel-copy sync burden.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: splitting context into references & verifying content is consistent.

* docs: fixing core language

* docs: fixing core language

* fix: language about observe_others

* chore: adding .agents folder for codex

* fix: add instructions.md into the mcp server & delete mcp skill in favor of including it in honcho-memory.

* chore: remove migrate docs (can be found on older versions)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-10 15:06:07 -04:00
Vineeth Voruganti 93dcf59c4a
chore(docs): Update changelogs (#1007) 2026-08-10 14:56:07 -04:00
Rajat Ahuja 618c331b2a
chore: update gcp/fly deployment yamls (#1005)
* chore: update gcp/fly deployment yamls

* fix: coderabbit comments
2026-08-10 13:11:59 -04:00
Aakash Kattelu d191c107e5
docs: document pgvector preinstall for least-privilege DB roles (#984)
* docs: document pgvector preinstall for least-privilege DB roles

Honcho issues CREATE EXTENSION IF NOT EXISTS vector before migrations
and again at server startup, both using the DB_CONNECTION_URI role. On
deployments where that role deliberately cannot create extensions
(managed Postgres, Kubernetes operators, NixOS), both statements fail
with a privilege error — IF NOT EXISTS does not save you, because
Postgres checks the privilege before checking for the extension.

Document preinstalling pgvector as a privileged role as the supported
path, and add a troubleshooting entry keyed to the exact error string.
Note that docker compose is unaffected, since the bundled database
service creates the extension via an initdb script.

Refs #614

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: clarify why docker compose avoids the pgvector privilege error

The previous wording pinned the claim entirely on database/init.sql,
which only runs on first boot of an empty data volume. The load-bearing
reason is that the bundled stack connects as the postgres superuser, so
it can create the extension regardless of volume state. Name that first
and keep init.sql as the secondary reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:23:43 -04:00
Aakash Kattelu 4f4ca5faa5
fix(deriver): update extraction examples (#985)
* fix(deriver): stop extraction examples teaching fabricated inferences

The EXAMPLES block in the minimal deriver prompt demonstrated two
inferences that its own output schema forbids:

- "I just had my 25th birthday last Saturday" -> "alice's birthday is
  June 21st". A vague relative reference cannot yield a specific date;
  this demonstrates inventing one.
- "I took my dog for a walk in NYC" -> "alice lives in NYC". Visiting a
  place is not living there.

A third example invited "+ general knowledge" inference to produce a
deductive conclusion. The deriver has no channel for that output --
PromptRepresentation carries only `explicit`, described as "direct
quotes or clear paraphrases only, no interpretation or inference", and
deductive conclusions are produced by the Dreamer's DeductionSpecialist.

Replace all three with examples that stay inside the schema's contract.
The dog/NYC message is kept and shown extracting correctly, and a third
example shows that "lives in NYC" is valid when actually stated, so the
examples teach the boundary rather than just avoiding it.

Closes #626

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(deriver): keep the stated duration in the NYC residence example

The example dropped "six years" from "I've lived in NYC for six years",
which contradicts the prompt's own rule to extract all observations and
to contextualize each one. Emit both the residence fact and the duration.

Keeping "alice lives in NYC" alongside it is deliberate: that output is
the point of the example, contrasting with the preceding one where the
same conclusion is *not* supported by a single visit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:23:18 -04:00
Chris Caldwell 0bbeb3bc43
fix(docker): copy uv build inputs for podman compatibility (#878) 2026-08-05 17:15:00 -04:00
JUNZE 00d6d36728
fix: make embedding batch size configurable (#983)
* fix: resolve tiktoken encoding without constructing the embedding client

EmbeddingClient.encoding forced full client construction, which raises
'OpenAI API key is required' even though tiktoken needs no credentials.
The document dedup tie-break (src/crud/document.py) only needs .encoding
for token counting, so any test hitting that path fails in environments
without embedding keys — notably CI for pull requests from forks, where
repo secrets are unavailable (e.g. #908's test-python job failing on
tests/crud/test_document.py::test_duplicate_rejection_reinforces_existing).

Resolve the encoding from the configured model directly, falling back to
cl100k_base, and only reuse the underlying client's encoding when it has
already been constructed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: make embedding batch size configurable

Add optional max_batch_size to the embedding model config
(EMBEDDING_MODEL_CONFIG__MAX_BATCH_SIZE) to cap texts per request for
OpenAI-compatible providers with smaller limits than OpenAI's, such as
DashScope text-embedding-v4 (10) and Alibaba Bailian
qwen3.7-text-embedding (20). When unset, native provider defaults are
preserved (OpenAI 2048, Gemini 100).

Fixes #687.

* test(embedding): cover Gemini batching and config fallbacks per review

- Gemini transport now tested for configured batch splitting and the 100
  default fallback
- OpenAI unset default (2048, single request) explicitly covered
- env-parsing test now asserts the value survives resolve_embedding_model_config
- docs: 100 is the client's conservative Gemini default, not a native limit

* test(embedding): assert provider batch-size defaults

---------

Co-authored-by: adavyas <adavyasharma@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 15:43:32 -04:00
adavyas ca32f50797
fix: resolve tiktoken encoding without constructing the embedding client (#955)
EmbeddingClient.encoding forced full client construction, which raises
'OpenAI API key is required' even though tiktoken needs no credentials.
The document dedup tie-break (src/crud/document.py) only needs .encoding
for token counting, so any test hitting that path fails in environments
without embedding keys — notably CI for pull requests from forks, where
repo secrets are unavailable (e.g. #908's test-python job failing on
tests/crud/test_document.py::test_duplicate_rejection_reinforces_existing).

Resolve the encoding from the configured model directly, falling back to
cl100k_base, and only reuse the underlying client's encoding when it has
already been constructed.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 15:28:39 -04:00
Aakash Kattelu e6d4d78ba1
fix(ci): clear basedpyright warnings from #903 tests; run static analysis on PRs (#975) 2026-08-04 17:03:10 -04:00
PK 5c32bd10ec
fix(llm): set HTTP timeout on Gemini clients (#903)
* fix(llm): set HTTP timeout on Gemini clients (#785)

* fix(embedding): set HTTP timeout on Gemini embedding client (#785)

Same wedge-class failure as the LLM client: a stalled Gemini embedding
socket hangs the in-process reconciler, which shares the deriver worker's
uvloop event loop. Apply the same 10-minute timeout here, in lockstep
with src/llm/registry.py's _build_gemini_http_options.

* style(test): drop extra blank line in test_registry imports
2026-08-04 16:29:32 -04:00
Alexei Vedernikov d815c8b8dc
fix(llm): support per-request provider timeouts (#832)
* fix(llm): support per-request provider timeouts

* fix(llm): convert Gemini timeout to milliseconds

* fix(llm): validate Gemini HTTP options

* test(llm): type Anthropic stream context args

* test(llm): live per-request timeout coverage for all providers

Two live checks per provider: a generous timeout asserted at the SDK
call boundary, and a tight timeout that must abort well under the 600s
client default. Gemini's async transport can be aiohttp, so its tight
timeout surfaces as asyncio.TimeoutError rather than httpx.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* style(tests): drop extra blank line in anthropic backend test

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(llm): validate provider_params.timeout at config load

Move the timeout coercion into src.config as coerce_provider_timeout and
run it from a field validator on ModelOverrideSettings.provider_params, so
a bad value in config.toml/env fails at startup with the exact config path
instead of surfacing per-request as a retried 500. Good values normalize
to float seconds at load. The per-request guard in src.llm.backend now
delegates to the same coercion (wrapping ValueError in ValidationException)
and continues to cover extra_params passed programmatically.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: document provider_params.timeout load-time validation and gotchas

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(llm): address review nits on timeout plumbing

Apply eisene's review feedback:
- Rename PROVIDER_TIMEOUT_ERROR → PROVIDER_TIMEOUT_ERROR_TEXT
- Move request_timeout_from_extra_params from backend.py (pure
  dataclasses) to request_builder.py (request assembly)
- Add comment explaining Gemini's ms timeout conversion
- Generalize _normalize_extra_params with _strip_none_params helper

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Aakash Kattelu <aakash@plasticlabs.ai>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 12:43:00 -04:00
Aakash Kattelu 2d2da75e75
docs: add verify skill for testing (#972)
* docs: update verify/SKILL.md

* docs: fold verification-run feedback into verify skill

Stale-compose-stack check + host-process path for branch code, worktree
.env/bun-install setup, concrete config.toml injection recipe, live-LLM
model-var requirement, and telemetry log-noise hint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 11:03:18 -04:00
Rajat Ahuja 148f646796
Fix: vector query top k zero (#970)
* fix: guard against top_k=0 reaching the vector store

Turbopuffer rejects top_k=0 with a 400 ('top_k must be between 1 and
10000'). The fix returns [] for a non-positive top_k, before the embedding call), and
floor the semantic budget at 1 so an explicitly requested search isn't
silently allocated zero.

* fix: comments
2026-08-03 12:31:03 -04:00
Vineeth Voruganti 4d3ab1c36b
fix: increase throughput of unit tests by changing behavior db teardown (#949)
* fix: increase throughput of unit tests by changing behavior db teardown

* fix: address review comments
2026-07-29 11:19:41 -04:00
Vineeth Voruganti e7cbcc8432
feat: session allowlist on dialectic and representation via filters (#882)
* fix: apply session scoping to all working-representation query paths

session_name was only applied to the recent-documents query in
RepresentationManager; the semantic and most-derived paths ignored it,
so limit_to_session leaked cross-session conclusions into perspectives.

- Thread a session allowlist (session_names) uniformly through all
  three query paths; pushed down to pgvector and external vector stores
- Accept a list so the upcoming session-allowlist API reuses this path
- Fail closed on an empty allowlist (downstream stores drop empty IN
  clauses, which would silently widen scope)

Fixes DEV-1994

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: bare-list membership sugar in the filter DSL

{"session_id": ["s1", "s2"]} is now shorthand for
{"session_id": {"in": [...]}} on regular columns, generically
(peer_id, etc.). JSONB metadata columns are excluded — a bare list
there keeps JSONB containment semantics, unchanged.

Previously a bare list on a regular column compiled to a type-mismatched
equality that matched nothing, so this is strictly additive.

Also translates the same shape in the turbopuffer/lancedb filter
builders, and fixes lancedb dropping empty IN clauses (fail-open) —
an empty membership list now emits an always-false condition.

Groundwork for DEV-1995 (session allowlist via the existing filters
DSL, no new API params)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: session allowlist on dialectic and representation via filters

Adds a constrained 'filters' body to peer.chat and /representation —
the same DSL search and conclusions already accept, supporting only the
session_id key (a session id, a bare list, or {"in": [...]}).
Unsupported keys and shapes are rejected with 422, never silently
ignored. Composes with session_id (must be included in the allowlist
when both are given). Capped at 1,000 sessions per request.

Enforcement is uniform at every recall chokepoint, fail-closed:
- dialectic prefetch + search_memory: conclusion recall restricted to
  the allowlist; dream docs (session_name IS NULL) excluded
- message tools (search/grep/date-range/temporal/context/history):
  strict intersection of allowlist and observer session membership
- get_reasoning_chain: unavailable under an allowlist (chains traverse
  provenance across sessions and cannot be scoped without leaking)
- empty allowlist short-circuits to empty results everywhere

Auth: workspace keys pass the allowlist as-given; peer-scoped JWTs must
be a member of every allowlisted session (403 otherwise), mirroring the
existing single-session check.

Fixes DEV-1995

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: fail closed on empty session allowlist across all filter builders

Empty session allowlists relied solely on the early-return guard in
_get_working_representation_internal. The layers below it were
inconsistent, so a future direct caller (the DEV-1995 allowlist API)
would silently widen scope instead of failing closed:

- _build_filter_conditions used a truthiness check; an empty list was
  treated like None and dropped the filter. Now uses `is not None`,
  matching the recent/most-derived SQL paths.
- turbopuffer emitted a bare `In []` with undocumented (possibly
  fail-open) semantics. Now emits an explicit always-false predicate,
  mirroring lancedb's `1 = 0`.

Also extract the duplicated JSONB column tuple in filter.py to a
JSONB_COLUMNS constant.

Tests exercise each fail-closed guarantee at the layer it lives, rather
than masking it behind the early-return guard.

* fix: address tests

* fix(crud): fail closed when session_name is outside the allowlist

search/grep/history helpers scoped to a single session_name ignored the
session_names allowlist entirely — a caller could read a session the
allowlist forbids. The API routes guarded this with a 422, but the
dialectic tools call these CRUD functions directly and bypassed it.

Enforce it at the boundary: return [] when session_name is set and not in
the allowlist, across _semantic_search_messages (covers search_messages +
search_messages_temporal), grep_messages, get_messages_by_date_range,
get_recent_history, and get_observation_context.

Also rename the public param allowed_sessions -> session_names for
consistency with representation.py / chat.py / peers.py; the resolved
intersection keeps its distinct name allowed_session_names.

* fix: test

* fix(scopes): tighten and consolidate session allowlist per review

Addresses review feedback on the session allowlist (DEV-1995).

Behavior changes:

- Auth gate on peers.chat now uses active membership (left_at IS NULL)
  via get_peer_session_names(active_only=True), matching the adjacent
  is_peer_in_session check on options.session_id. Previously a peer that
  had left a session was denied when naming it directly but permitted
  when naming it in filters.session_id.
- Scoped conclusion recall is restricted to level == "explicit"
  (ALLOWLIST_SAFE_LEVELS). Dream-derived conclusions are stamped with a
  single session_name but synthesized across all sessions, so that stamp
  can't be scoped on. Applied at all four recall paths. Unscoped recall
  is unchanged. Follow-up to give conclusions an authoritative
  source-session set is tracked in DEV-2201.
- The allowlist gate checks `is not None` rather than truthiness, so
  filters={"session_id": []} reaches it instead of being skipped.

Refactors:

- New crud.message.resolve_session_scope replaces four near-identical
  copies of the allowlist-membership intersection. Returns
  (allowlist, deny) and never returns an empty list, so the None vs []
  distinction that external stores fail open on lives in one tested
  place. Takes db=None and opens its own short-lived session only when
  distinction that external stores fail open on lives in one tested
  place. Takes db=None and opens its own short-lived session only when
  an observer lookup is needed, preserving external-lookup-first
  ordering on the vector-store path.
- extract_session_allowlist takes must_include, collapsing the
  session_id-in-allowlist check duplicated across both peer routes.
- DialecticAgent._select_tools dedupes the two toolset-selection blocks
  and drops get_reasoning_chain under an allowlist, rather than paying
  for the schema plus a wasted turn to return a refusal.
- Rename session_names -> session_allowlist across crud, agent tools,
  dialectic and routes, to remove the one-character ambiguity with
  session_name. Internal only; the public filters.session_id surface is
  unchanged.

Docs:

- session_allowlist documented across all message and recall entry
  points, including the None / [] / populated contract.
- session_name marked deprecated for scoping. Not removed and not
  aliased: it also pins the query to one session, bypasses observer
  scoping, and drives session-history injection into the dialectic
  prompt, so it has no drop-in replacement.
- Note at the Document branch in utils/filter.py that the raw-key
  fallback is load-bearing for session scoping.

Tests: 20 -> 39 in tests/test_session_allowlist.py, covering the
peer-scoped JWT gate (member, non-member, left-session, workspace-key
bypass, empty allowlist), the resolve_session_scope tri-state including
the no-DB-checkout path, must_include, and the level narrowing.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 10:55:02 -04:00
Vineeth Voruganti 3ee890fa6f
Vineeth/sentry filter consolidation (#934)
* fix: centralize sentry before_send filter config

* chore: comply with linter

* fix: default sentry filters
2026-07-24 15:42:41 -04:00
Leonardo Baray d7b64116ac
fix: redact Redis password from cache connection logs (#869)
* fix: redact Redis password from cache connection logs

The cache client logged the full Redis URL — including the password —
at INFO and WARNING levels on every connection attempt and failure.
This exposed the live Redis credential in stdout/container logs and any
downstream log aggregation.

Add _redact_cache_url() to mask the password component before logging.
URLs without a password are returned unchanged.

Closes #866

* fix: handle malformed URLs and IPv6 in _redact_cache_url

Address review feedback from VVoruganti and CodeRabbit:

- Wrap urlparse/urlunparse in try/except so malformed URLs (e.g. invalid
  port) don't raise ValueError inside except blocks, which would crash
  startup instead of degrading gracefully
- Preserve IPv6 brackets (e.g. [::1]) in reconstructed URLs
- Add Google-style Args/Returns docstring sections
- Add unit tests for password masking, no-password URLs, IPv6,
  malformed inputs, and the invalid-port regression

* test: use real secrets in redaction test fixtures

Three fixtures were weakened by copy-paste mangling: literal '***'
placeholders instead of real passwords (assertions trivially true),
an unescaped '#' that truncated netloc parsing via the URL fragment,
and a no-password case that actually contained userinfo. Restore
inputs that genuinely exercise the masking paths.

* fix: never leak password through malformed-URL fallback

The catch-all fallback returned the original URL when parsing failed,
so a Redis URL with a password and an invalid port (typo, out-of-range)
was logged in clear text - the exact leak #866 exists to fix. Narrow
the handling: .port access gets its own try/except (invalid port is
omitted from the output; userinfo/hostname masking never raises), and
the outer fallback now returns a generic placeholder instead of the
raw input. Tightened the invalid-port test to assert the password is
absent and added out-of-range-port and unparseable-URL cases.

* fix: redact secrets in query params and scheme-less URLs

_redact_cache_url only masked userinfo, but a credential can reach the
URL through two other real configuration paths: redis-py accepts
?password= (all querystring options become client kwargs) and cashews
accepts ?secret= (its HMAC signing key) - and honcho's own default
CACHE.URL already uses a query param (?suppress=true), so this is the
expected configuration style. Separately, a URL missing its scheme
(':pass@host:6379/0') parses with an empty netloc, making the password
invisible to .password and echoing it back verbatim.

Mask sensitive query values in place on the raw query string (no
decode/re-encode, so non-secret params are preserved byte-for-byte)
and return the generic placeholder for @-carrying strings with no
parseable authority. Verified with a 20k-case randomized fuzz run in
addition to the unit tests: no functional credential reaches the
output.
2026-07-24 12:54:31 -04:00
papesy384 24f7a2cbd2
fix(dev): skip lancedb on macOS Intel and guard optional import (#496)
Add a PEP 508 marker so lancedb is not installed on darwin/x86_64, wrap the
LanceDB vector store import in try/except for a clear config error, and
regenerate uv.lock.

Branch rebased onto upstream/main; prior src/utils/clients.py CI tweak is
obsolete because LLM wiring moved under src/llm/.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 12:36:55 -04:00
Vineeth Voruganti 672f4c6637
fix: apply session scoping to all working-representation query paths (#881)
* fix: apply session scoping to all working-representation query paths

session_name was only applied to the recent-documents query in
RepresentationManager; the semantic and most-derived paths ignored it,
so limit_to_session leaked cross-session conclusions into perspectives.

- Thread a session allowlist (session_names) uniformly through all
  three query paths; pushed down to pgvector and external vector stores
- Accept a list so the upcoming session-allowlist API reuses this path
- Fail closed on an empty allowlist (downstream stores drop empty IN
  clauses, which would silently widen scope)

Fixes DEV-1994

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: bare-list membership sugar in the filter DSL

{"session_id": ["s1", "s2"]} is now shorthand for
{"session_id": {"in": [...]}} on regular columns, generically
(peer_id, etc.). JSONB metadata columns are excluded — a bare list
there keeps JSONB containment semantics, unchanged.

Previously a bare list on a regular column compiled to a type-mismatched
equality that matched nothing, so this is strictly additive.

Also translates the same shape in the turbopuffer/lancedb filter
builders, and fixes lancedb dropping empty IN clauses (fail-open) —
an empty membership list now emits an always-false condition.

Groundwork for DEV-1995 (session allowlist via the existing filters
DSL, no new API params)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: fail closed on empty session allowlist across all filter builders

Empty session allowlists relied solely on the early-return guard in
_get_working_representation_internal. The layers below it were
inconsistent, so a future direct caller (the DEV-1995 allowlist API)
would silently widen scope instead of failing closed:

- _build_filter_conditions used a truthiness check; an empty list was
  treated like None and dropped the filter. Now uses `is not None`,
  matching the recent/most-derived SQL paths.
- turbopuffer emitted a bare `In []` with undocumented (possibly
  fail-open) semantics. Now emits an explicit always-false predicate,
  mirroring lancedb's `1 = 0`.

Also extract the duplicated JSONB column tuple in filter.py to a
JSONB_COLUMNS constant.

Tests exercise each fail-closed guarantee at the layer it lives, rather
than masking it behind the early-return guard.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 11:41:34 -04:00
Vineeth Voruganti a15c782985
Session-purity invariant + card_refresh dream type (DEV-2000) (#883)
* fix: enforce explicit-document session purity in dedup/merge paths

Audit for DEV-2000 (Scopes RFC prerequisite): explicit-level documents must
stay session-pure so scope memory can be built by copying explicit documents
between collections. Two classes of violation were possible:

- Exact-content and semantic dedup in crud/document.py matched candidates
  with no level or session scoping, so an explicit document could be
  reinforced by — or soft-deleted in favor of — a same-content document from
  a different session or a different level (silently merging cross-session
  derivations into one row).
- The generic create_observations tool handler accepted level='explicit'
  from agents with no message context (dreamer/dialectic), which would mint
  session-less explicit documents.

Enforcement (refuse, never rewrite):
- create_documents refuses explicit documents with a null session_name
- exact dedup keys on (content, level, session-for-explicit); derived levels
  keep cross-session consolidation
- is_rejected_duplicate scopes candidate search to the same level, and the
  same session for explicit documents
- the create_observations tool rejects explicit-level input outside message
  ingestion (deriver) context

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: add card_refresh dream type for event-driven peer-card updates

Adds a lightweight dream variant (DEV-2000, Scopes RFC prerequisite) that
runs ONLY the peer-card update — for event-driven refreshes such as scope
membership changes and cold starts:

- DreamType.CARD_REFRESH alongside OMNI; dispatched by process_dream to a
  new run_card_refresh_dream orchestration
- CardRefreshSpecialist: restricted to get_recent_observations,
  search_memory, and update_peer_card (no observation-mutating tools), with
  a low tool-iteration cap of min(6, DREAM.MAX_TOOL_ITERATIONS)
- rebuild=True mode carried in the dream payload: the existing card is NOT
  injected into the prompt and the specialist rebuilds it solely from
  observations present in the collection (for use after removals)
- enqueue-able via the manual enqueue_dream path (bypasses volume gates);
  the work-unit key already embeds the dream type so a card refresh never
  collides with a pending omni dream. POST /v3/workspaces/{id}/schedule_dream
  accepts dream_type=card_refresh plus the rebuild flag
- card refreshes never advance the omni dream guard pair
  (last_dream_at / last_dream_document_count)
- shared PEER CARD prompt section extracted (verbatim) from
  DeductionSpecialist for reuse; CallPurpose gains dream.card_refresh

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: fix tests

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 14:22:07 -04:00
ajspig 4f9a41360a
feat: support OAuth for MCP clients. (#923)
* feat: support OAuth for MCP clients.

* fix: scheme is case-insensitive.

* fix: expose WWW-Authenticate header for cross-origin clients.
2026-07-21 15:31:09 -04:00
Eugene Eisenstein 063aaa97a6
feat(dialectic): optional structured outputs with limited schema for Dialectic calls (#896)
* Structured outputs for dialectic

* cleanup

* rename json_schema_to_pydantic to clarify it's not a general schema converter

* clean up schema DoS guards

* simplification and cleanup of schema conversion

* chore: ruff and pyproject toml

* chore: basedpyright cleanup in test

* fix: some needed unrelated test failures

* test(schema_conversion-and-anthropic-backend): expand test coverage

include table tests

* fix(llm): support combined tool calling and structured output across backends

- OpenAI: parse() 500s on non-strict function tools; route tool-carrying
  structured requests through create() with an explicit json_schema
  response_format (mirrors the streaming path)
- Anthropic: skip the '{' JSON prefill when tools are present so tool_use
  blocks stay reachable; make the schema instruction conditional and rely
  on parse + repair
- Gemini: native response_schema + function calling is rejected before
  Gemini 3; with tools present, inject a schema instruction into the final
  turn instead and rely on parse + repair
- All backends: tool-call turns carry no consumable content, so skip
  structured-output parsing on them

Extracted from the dialectic structured-output branch (DEV-1652) so the
transport layer can land independently.

DEV-2035

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(live_llm): exercise combined tools + structured output per provider

Two-turn live flow per backend: a forced tool-call turn (structured
parsing must be skipped) followed by a replay turn that must return a
schema-conforming answer with tools still attached. Asserts the
provider-specific request shaping: no parse() for OpenAI (500s on
non-strict tools), no '{' prefill for Anthropic, no native
response_schema for Gemini.

Verified against live OpenAI (gpt-4.1, gpt-5, gpt-5.4, gpt-5.4-mini)
and Gemini (gemini-2.5-flash).

DEV-2035

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(unified): dialectic chat with response_format schema under tool use

Adds response_format pass-through to the unified runner's chat query and
a test case that forces the dialectic tool loop (reasoning off + global
enumeration question) while requiring a schema-conforming JSON answer —
end-to-end coverage of the combined tools + structured output transport
path on whichever provider each level is configured with.

Verified locally against a full harness run (json_match assertions pass;
the llm_judge assertion additionally runs in CI where the Anthropic key
is available).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: some needed unrelated test failures

* ci: add label-triggered live LLM test workflow

Adding the run-live-llm label to a PR (or workflow_dispatch) runs
tests/live_llm/ against real provider APIs — the only place the
--live-llm suite runs in CI. Reuses the unified-tests environment and
its Secrets Manager staging-dotenv resolution for provider keys; runs
on ubuntu-latest (no Fly runner, no Docker — the suite only touches the
LLM backends). Pins LIVE_LLM_ANTHROPIC_45_PLUS_MODELS=claude-sonnet-4-5
since the Anthropic family has no default models and would otherwise
silently collect empty.

Opt-in by design: live model behavior is variable, so this is a signal,
not a required check.

DEV-2035

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci: run live LLM tests on main pushes touching the transport

Mirrors unified-tests' push trigger, scoped to paths that can affect
the live suite (src/llm/, config, the tests, deps, and the workflow
itself) so provider API calls aren't spent on unrelated changes.

DEV-2035

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci: disable auth in live LLM test environment

The staging dotenv sets AUTH_USE_AUTH=true without a usable JWT secret,
and src/config.py validates the pair at import time — the same reason
unified-tests overrides it. This suite never runs the API server.

DEV-2035

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(live_llm): fix gpt-5.4 reasoning_effort and gemini replay-turn flake

- test_live_openai: gpt-5.4 dropped 'minimal' from the reasoning_effort
  vocabulary, so the gpt5 caching test 400'd — and the OpenAI backend's
  BadRequestError terminal swallowed it into an empty CompletionResult.
  Pick the effort per model generation.
- test_live_tools_structured_output: use tool_choice='auto' on the
  replay turn, matching the production dialectic loop (which never
  forces 'none') — NONE mode is what provoked gemini-2.5-flash's empty
  candidates. Drop the temperature pin so retries actually resample,
  and treat a repeat tool call as a retryable attempt.

Verified live: full suite green, gemini 4/4 consecutive passes.

DEV-2035

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci: fail live LLM run when no staging secret was loaded

If the latest-tag fetch fails and no second tag exists, the fallback
step is skipped rather than failed, and the job would proceed without
provider keys — every test then skips via require_provider_key and the
run goes green. Guard on both fetch outcomes so that path fails loudly.

DEV-2035

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(live-llm-tests-GHA): remove extra comments

* feat(structured-output): enable non-recursive schema references

* docs(structured-outputs): clean up new doc

* test(structured-output): fix caching refs memory leak, add tests

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 18:46:49 -04:00
Ulysse Pence f133797164
Merge pull request #910 from plastic-labs/ulysspence/dev-1967-dedup-document-count
feat(telemetry): counts documents deduped during representation, exact and semantically similar
2026-07-20 16:09:05 -04:00
Ulysse Pence b7a00d5f6b Reverts RepresentationCompletedEvent version increment 2026-07-20 17:50:40 -02:00
Ulysse Pence c992efbf87 Merge branch 'main' into ulysspence/dev-1967-dedup-document-count 2026-07-20 17:49:01 -02:00
ajspig 1684b84344
chore: updating cli version 0.1.2 (#921) 2026-07-20 15:05:04 -04:00
Aakash Kattelu 055d73b580
feat(cli): add device code oauth login for honcho servers (#891)
* feat(cli): add device code oauth login for honcho servers

* fix(cli): harden device-auth input validation and transport errors

Reject zero/negative auth-method choices instead of letting Python
negative indexing wrap to the tail of the options list, and wrap httpx
transport failures in the OAuth POST helpers as OAuthFlowError so
connection errors surface through existing caller handling rather than
escaping as an uncaught traceback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat: add HONCHO_CONFIG_DIR env var for cli

* refactor(cli): address review nits on device-auth PR

- split `init` into manual-key and interactive helpers
- document that access_valid checks persisted expiry, not the token
- rename single-letter local in redacted()
- cover 500 alongside 404 in supports_device_login metadata probe
- add config edge-case tests: stale apiKey drop, garbage/string
  accessExpiresAt, empty-env-var popping, refresh-rotation fallback,
  missing-token access_valid

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): stop deleting shared apiKey on device login; bind oauth grant to its host

apiKey is shared with sibling tools that read it from the same config
file, so device login must not remove it. Precedence flips to a live
OAuth token over apiKey; a dead grant now degrades to the saved key
with a warning instead of aborting. The oauth block records the host
it was minted against and is ignored (no use, no refresh) when
base_url points elsewhere, so a staging grant is never sent to prod.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 13:16:55 -07:00
Ulysse Pence d2d397f14a Counts documents deduped during representation, exact and semantically similar 2026-07-16 14:30:55 -02:00
Talha Abdur Rahman 68c46cde81
CI/CD Workflow file Added (#909)
* CI/CD Workflow file Added

* Updated Tag & SA Key

* update input Tag

* fix: pass workflow inputs to shell via env to prevent command injection

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Rajat Ahuja <rahuja445@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 13:23:29 -04:00
Eugene Eisenstein 9e087e8771
feat(llm backend): enable combined tool calling + structured output in the LLM backend transport layer (#907)
* fix(llm): support combined tool calling and structured output across backends

- OpenAI: parse() 500s on non-strict function tools; route tool-carrying
  structured requests through create() with an explicit json_schema
  response_format (mirrors the streaming path)
- Anthropic: skip the '{' JSON prefill when tools are present so tool_use
  blocks stay reachable; make the schema instruction conditional and rely
  on parse + repair
- Gemini: native response_schema + function calling is rejected before
  Gemini 3; with tools present, inject a schema instruction into the final
  turn instead and rely on parse + repair
- All backends: tool-call turns carry no consumable content, so skip
  structured-output parsing on them

Extracted from the dialectic structured-output branch (DEV-1652) so the
transport layer can land independently.

DEV-2035

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(live_llm): exercise combined tools + structured output per provider

Two-turn live flow per backend: a forced tool-call turn (structured
parsing must be skipped) followed by a replay turn that must return a
schema-conforming answer with tools still attached. Asserts the
provider-specific request shaping: no parse() for OpenAI (500s on
non-strict tools), no '{' prefill for Anthropic, no native
response_schema for Gemini.

Verified against live OpenAI (gpt-4.1, gpt-5, gpt-5.4, gpt-5.4-mini)
and Gemini (gemini-2.5-flash).

DEV-2035

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: some needed unrelated test failures

* ci: add label-triggered live LLM test workflow

Adding the run-live-llm label to a PR (or workflow_dispatch) runs
tests/live_llm/ against real provider APIs — the only place the
--live-llm suite runs in CI. Reuses the unified-tests environment and
its Secrets Manager staging-dotenv resolution for provider keys; runs
on ubuntu-latest (no Fly runner, no Docker — the suite only touches the
LLM backends). Pins LIVE_LLM_ANTHROPIC_45_PLUS_MODELS=claude-sonnet-4-5
since the Anthropic family has no default models and would otherwise
silently collect empty.

Opt-in by design: live model behavior is variable, so this is a signal,
not a required check.

DEV-2035

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci: run live LLM tests on main pushes touching the transport

Mirrors unified-tests' push trigger, scoped to paths that can affect
the live suite (src/llm/, config, the tests, deps, and the workflow
itself) so provider API calls aren't spent on unrelated changes.

DEV-2035

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci: disable auth in live LLM test environment

The staging dotenv sets AUTH_USE_AUTH=true without a usable JWT secret,
and src/config.py validates the pair at import time — the same reason
unified-tests overrides it. This suite never runs the API server.

DEV-2035

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(live_llm): fix gpt-5.4 reasoning_effort and gemini replay-turn flake

- test_live_openai: gpt-5.4 dropped 'minimal' from the reasoning_effort
  vocabulary, so the gpt5 caching test 400'd — and the OpenAI backend's
  BadRequestError terminal swallowed it into an empty CompletionResult.
  Pick the effort per model generation.
- test_live_tools_structured_output: use tool_choice='auto' on the
  replay turn, matching the production dialectic loop (which never
  forces 'none') — NONE mode is what provoked gemini-2.5-flash's empty
  candidates. Drop the temperature pin so retries actually resample,
  and treat a repeat tool call as a retryable attempt.

Verified live: full suite green, gemini 4/4 consecutive passes.

DEV-2035

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci: fail live LLM run when no staging secret was loaded

If the latest-tag fetch fails and no second tag exists, the fallback
step is skipped rather than failed, and the job would proceed without
provider keys — every test then skips via require_provider_key and the
run goes green. Guard on both fetch outcomes so that path fails loudly.

DEV-2035

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(live-llm-tests-GHA): remove extra comments

* ci(CODEOWNERS): introduce CODEOWNERS and gate GHA heavy test runs behind being a CODEOWNER

* ci(GHA-live-LLM-tests): consolidate common GHA steps

* test(test_live_openai): fix reasoning level adjustment for gpt-5

* test(live-llm-tests): temporary removal of gate to test the workflow

* test(live-llm-tests): revert removal of gate

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 11:47:49 -04:00
ajspig 0842c8e21a
fix: date dreamer conclusions to latest source observation (#890)
* fix: date dreamer conclusions to latest source observation

* fix: correct and normalize dreamer conclusion timestamps

* fix: updating documentation
2026-07-15 10:36:48 -04:00
Rajat Ahuja 5ad22840d8
feat: add support for redis cluster (#905) 2026-07-13 15:51:47 -04:00
Eugene Eisenstein c7c1597d2c
Fix `unified-tests.yml` secrets (#895)
* Structured outputs for dialectic

* Fix unified-tests.yml secrets overrides

* Revert "Structured outputs for dialectic"

This reverts commit a221e2c282.

* remove throwing validator, document assumption that config overrides be backward compatible

* fix typo

* fix: ruff format config file

* fix: update pyproject.toml to include exclude-newer

---------

Co-authored-by: Rajat Ahuja <rahuja445@gmail.com>
2026-07-13 10:36:34 -04:00
Chris Caldwell de1b4101a6
fix(llm): satisfy lowercase json_object prompt checks (#887)
* fix(llm): satisfy lowercase json_object prompt checks

* style(llm): preserve JSON acronym in prompt
2026-07-12 11:38:31 -07:00
Eugene Eisenstein 73453f892d
instruct dreamer specialists to not output summaries (#894) 2026-07-10 10:45:11 -04:00
Eugene Eisenstein 6d19c46861
Merge pull request #889 from plastic-labs/eugene/dev-1989
Split `REPRESENTATION_BATCH_MAX_TOKENS` into a "minimum work unit" setting on the producer side and a "maximum LLM tokens" setting on the consumer side
2026-07-09 15:12:27 -04:00
Eugene Eisenstein f70add3fea
Merge pull request #892 from plastic-labs/eugene/dev-2010
Prevent flooding the queue with embedding tasks
2026-07-09 15:11:18 -04:00
Eugene Eisenstein dcde4b2907 add prometheus metric for in_flight 2026-07-09 12:01:09 -04:00
Eugene Eisenstein 43d962d160 rename to REPRESENTATION_BATCH_TARGET_INPUT_TOKENS 2026-07-09 10:42:52 -04:00
Eugene Eisenstein c9bf53ac06 prevent hammering with message tasks 2026-07-08 18:28:10 -04:00
Eugene Eisenstein be26c859ad split config entry into 2 2026-07-08 12:24:55 -04:00
Eugene Eisenstein 4536612b8f add .omc to gitignore 2026-07-08 11:58:13 -04:00
ajspig 0cb0c9abf0
docs: adding codex doc (#879) 2026-07-07 11:25:01 -04:00
Aakash Kattelu 602347d76c
feat(telemetry): CloudEvents + Langfuse tracing as projections over a captured LLM stream (#845)
* feat(telemetry): CloudEvents + Langfuse tracing as projections over a captured LLM stream

Capture each LLM call once (CapturedLLMCall) and fan it out to multiple
exporters -- "one data model, two projections": a CloudEvents trace stream
(llm.call.traced / trace.content) and a Langfuse projection, both reconstructing
trace -> run -> step -> generation from the same source of truth.

- Capture seam (src/llm/capture.py): one canonicalization + content-addressed
  hashing point, with an O(N) per-span memo so repeated context isn't re-hashed.
- Session correlation threaded telemetry -> captured call -> exporters,
  namespaced only at the Langfuse export boundary.
- Span identity consolidated onto LLMTelemetryContext; dropped TRACE_ENDPOINT.
- Canonical generation/step names; dreamer branches nest under one dream trace;
  tool calls become spans under their step.
- LANGFUSE_EXPORTER_MODE toggle ("exporter" default; "inline" kept one release
  for side-by-side validation), centralized into computed settings predicates.
- Per-run/per-trace dedup registries (trace_session, langfuse_session) bounded
  by an LRU so dedup and span grouping survive long-running workers.
- Embedding-call tracing; deterministic high-volume event sampling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(telemetry): address trace-review findings (span/step_seq collisions, test, logging)

- Dreamer specialists mint a distinct span_id per execution (trace_id stays the
  shared dream run_id), so their CloudEvents trace resource ids no longer collide
  between deduction and induction.
- Tool-loop no-tool early-return streams the tail with the next ordinal
  (iteration+2) instead of reusing the in-loop call's step_seq, avoiding a
  colliding trace resource id; mirrors the synthesis path.
- Tighten test_clips_oversized_string to assert output stays within TRACE_MAX_BYTES.
- emit_trace logs the swallowed exception with exc_info for debuggability.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(telemetry): silence exporter-mode Langfuse warning + drop summarizer run_id placeholder

Two CloudEvents/Langfuse correctness fixes, independent of the trace viewer.

Langfuse exporter-mode gating: annotate_current_generation_io (and its two
executor.py call-site guards) were gated on LANGFUSE_PUBLIC_KEY instead of
langfuse_inline_enabled. In the default `exporter` mode they called
get_client().update_current_generation() with no active @observe span, logging
"No active span in current context" (~14 per dialectic run) and building
throwaway model_dump payloads on every LLM call. The LangfuseExporter projects
I/O from the captured stream, so these helpers must no-op in exporter mode.
Gated all three on langfuse_inline_enabled; added a regression test; fixed a
stale conditional_observe docstring.

Summarizer run_id placeholder: AgentToolSummaryCreatedEvent hardcoded
run_id="deriver"/iteration=0 because summarization is a single LLM call, not an
agentic run. That placeholder pollutes run_id grouping in the CloudEvents stream
(any consumer that groups by run_id sees a phantom "deriver" run). Made
run_id/iteration optional (None) and re-keyed get_resource_id on
message_id:summary_type (the real per-summary identity; run_id/iteration can no
longer identify it); bumped schema_version 2->3. Xatu ingestion stores only the
CloudEvent envelope, so the field/resource_id/version changes are transparent to it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: update docstrings to be less verbose

* fix(telemetry): address PR review on captured-stream tracing

- embedding traces get a fresh span_id under parent_span_id=run_id, so
  sibling embeddings in one run no longer share a span/idempotency key
- capture the provider finish_reason from stream chunks instead of
  hardcoding "stop" on a successful drain
- gate the Langfuse exporter behind TELEMETRY.ENABLED (master switch) so
  disabling telemetry sends no traces at all
- rename _emit_derived_content -> _emit_hashed_content
- inline the _emit_trace wrapper; drop unused trace_session.end_run

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor: rename TELEMETRY_TRACE_PAYLOADS to TELEMETRY_TRACE_PAYLOADS_ENABLED

* fix(telemetry): capture provider tool calls in trace stream

The captured trace stream dropped assistant tool calls for openai/gemini:
build_captured_messages only read {role, content, tool_call_id}, but those
providers keep tool calls outside content (openai's tool_calls, gemini's
parts), so replayed tool-call turns landed as empty content and gemini lost
its text and tool results entirely. Anthropic (tool_use in content) was fine.

Normalize each input message per provider into a unified tool_calls
[{id, name, input}] field on CapturedMessage/TraceContentEvent, recovering
gemini text/results along the way, and fold tool_calls into
compute_content_hash so empty-content openai turns no longer collide in the
dedup store. langfuse_exporter._input now surfaces the calls.

Also fix a silent serialization drop: gemini thought_signature is bytes, so
model_dump(mode="json") on the traced event raised UnicodeDecodeError and
emit_trace swallowed it -- dropping the whole tool-calling iteration from the
trace stream (billing and Langfuse were unaffected). base64-encode the
signature on the telemetry path; replay keeps the raw bytes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(telemetry): type replay tool-call dict for bytes signature

thought_signature widened to str | bytes | None, but
_tool_call_result_to_dict's literal was inferred as
dict[str, str | dict[str, Any]], so the bytes assignment failed project-wide
basedpyright (the per-file pre-commit hook didn't catch it). Annotate the
dict as dict[str, Any]; the replay path keeps the raw bytes unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: remove 3 tests

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 16:49:53 -04:00
Vineeth Voruganti 502e20a1cc
fix: Add namespace correlation to sentry monitoring (#870) 2026-07-02 16:49:23 -04:00
Vineeth Voruganti 63ea82c084
chore(docs): SDK Updates (#867) 2026-07-02 13:00:14 -04:00
Vineeth Voruganti b8d4ba5b34 chore: trigger rebuild 2026-07-01 17:25:09 -04:00
Vineeth Voruganti da0d92a475 chore(docs): Add detailed system diagram to docs 2026-07-01 16:55:15 -04:00
Vineeth Voruganti ba421a25ee chore: changelog updates 2026-07-01 16:13:12 -04:00
ajspig 14538cfc90
Abigail/conclusions level filter (#851)
* feat(conclusions): expose reasoning level + allow filtering by level

The `level` of a conclusion (explicit / deductive / inductive /
contradiction) was filterable server-side but stripped from the
`Conclusion` response and not surfaced in either SDK. This adds it
end-to-end so callers can list explicit-only ("not dreamed on")
conclusions without dropping to raw HTTP.

- api: add `level` to the Conclusion response schema
- python sdk: `ConclusionLevel` type, `level` on Conclusion/response,
  `level=` kwarg on ConclusionScope.list() and the async variant
- ts sdk: `ConclusionLevel` type, `level` on Conclusion/response,
  `level` option on list()
- tests: assert level is exposed; add level-filter list test

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(conclusions): use generic filters= on list() instead of level= kwarg

Match the documented SDK convention (peers/sessions/messages all take a
generic `filters` dict passed through to the same dynamic server-side
filter logic) instead of a one-off `level=` kwarg. `level` filtering now
works as `list(filters={"level": "explicit"})` alongside any other
supported filter/operator.

The `level` field on the Conclusion response (added in the previous
commit) is kept — it's still not otherwise returned by the API.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(conclusions): allow filtering by level on query() in py + ts SDKs

The branch's level-filter work exposed `filters=` on `list()` but left
`query()` (semantic search) hardcoding `{observer, observed}`, so callers
could filter the list endpoint by reasoning level but not semantic search —
asymmetric in both SDKs.

- Python: add keyword-only `filters` to `ConclusionScope.query` and
  `ConclusionScopeAio.query`, merged over the scope's observer/observed.
- TypeScript: add optional `filters` arg to `ConclusionScope.query`,
  mirroring the existing `list()` change.

The server `/conclusions/query` endpoint already honors filters in the body
(verified against production), so this is purely SDK surface parity.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(filters): document filtering conclusions by reasoning level

The using-filters page covered workspaces/peers/sessions/messages but not
conclusions. Add a "Filtering Conclusions" section showing level-based
filtering on both list() and query(), including the common "explicit only"
(exclude dream-derived) case and the in[deductive,inductive] inverse.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(conclusions): simplify filter merge to a single dict spread

Replace the merged_filters + if-block pattern in list()/query() (py sync,
aio, ts) with a single dict spread that layers the caller's filters over the
scope's observer/observed (and session). No behavior change — same merge
order (caller wins) — just less code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(conclusions): reject scope-managed keys in SDK conclusion filters

The generic filters= argument on ConclusionScope.list()/query() spread
user-supplied filters last, so a stray observer/observed/session key
silently overrode the scope and returned data from a different peer
pair. Add a fail-loud guard in both the Python and TypeScript SDKs that
rejects scope-managed filter keys with a clear error, directing callers
to peer.conclusions / conclusions_of(target) and the session= parameter.
session_id remains a valid filter on query() (which has no dedicated
session parameter).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-07-01 10:48:01 -04:00
Rajat Ahuja 2583c126f0
feat: add exact content deduplication in document creation (#861)
* feat: add exact content deduplication in document creation

* feat: add comment for index

* fix: harden times_derived logic across all callers to use max of inputs and existing + 1

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-07-01 10:42:56 -04:00
Vineeth Voruganti eb386c3ceb
fix: translate canonical tool_choice in OpenAI backend for cross-provider fallback (#850)
The OpenAI backend passed tool_choice through raw while the Anthropic and
Gemini backends translate Honcho's canonical vocabulary to their native
form. On a mixed-provider fallback chain (e.g. Gemini primary -> OpenAI
backup), a canonical "any" reached OpenAI unchanged and was rejected as an
invalid param, since OpenAI only accepts none/auto/required.

Add a _convert_tool_choice to the OpenAI backend mirroring the others so a
single TOOL_CHOICE value resolves correctly regardless of which provider a
fallback lands on. "any"/"required" -> "required", auto/none pass through,
a tool-name string or {"name": ...} dict -> a function selection.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 12:48:02 -04:00
Vineeth Voruganti 2f3a478948
fix(llm): stop capturing live LLM clients in Langfuse generation spans (#849)
* fix(llm): stop capturing live LLM clients in Langfuse generation spans

honcho_llm_call_inner is the @observe generation boundary, and default
auto-capture serialized every argument into the span input -- including
client_override (a live AsyncOpenAI/genai client) and selected_config
(which carries api_key). Auto-capture deep-copies the client into a
half-constructed object whose teardown raises:

  - AsyncHttpxClientWrapper ... no attribute '_state'      (OpenAI, stderr flood)
  - BaseApiClient ... no attribute '_http_options'         (Gemini, HONCHO-4HA)

and it leaked ModelConfig.api_key into traces.

Switch from auto-capture (denylist) to explicit annotation (allowlist):
disable capture_input/capture_output on the decorator and stamp curated,
serializable input (messages) and output (HonchoLLMCallResponse) via the
new annotate_current_generation_io helper. Full trace fidelity is
preserved; no client object or secret can reach a trace.

Fixes HONCHO-4HA

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(llm): track call tuning knobs as Langfuse model_parameters

Restore full trace fidelity after disabling @observe auto-capture: surface
every tuning knob (temperature, max_tokens, tools, reasoning effort, ...) on
the generation via model_parameters, sourced from the resolved effective
config instead of the raw function args.

Use a deny-list, not an allow-list: dump the whole ModelConfig and exclude
only secret-bearing fields (api_key, base_url, fallback, provider_params), so
new config knobs are traced automatically without keeping a hand-written list
in sync. The live client is never passed -- there is no useful trace
representation of it and serializing it is what triggered HONCHO-4HA.

Adds a deny-list test proving secrets never leak even when the config carries
a real api_key/base_url/provider_params (the production override-client path).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(llm): duplicate token usage to Langfuse + skip payload build when disabled

Mirror per-call token usage (input, output, prompt-cache read/creation) onto
the Langfuse generation via usage_details, so Langfuse renders native tokens
and cost in addition to the CloudEvents accounting.

Also guard both generation-annotation blocks behind settings.LANGFUSE_PUBLIC_KEY
so the model_dump-backed model_parameters payload (and the usage dict) are only
built when Langfuse is actually configured (addresses CodeRabbit: the annotate
helper no-ops when disabled, but the payload was still being constructed every
call).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 11:25:26 -04:00
Vineeth Voruganti 60a15e664d
v3.0.11 Release Candidate (#841)
* chore(docs): Release Candidate Changelog and Version Updates

* chore: fix basedpyright error
2026-06-24 12:44:13 -04:00
Aru Sharma 810e25c487
fix: checker bug_fix (#840) 2026-06-24 10:55:45 -04:00
Rajat Ahuja 1c23a2e24a
chore: use git tags to fetch secrets for unified test (#838)
* chore: use git tags to fetch secrets for unified test

* fix: test failure

* fix: override AUTH_USE_AUTH and SENTRY_ENABLED

* fix: upload traces

* chore: rm run on PR

* fix: rm bucket from logs
2026-06-24 10:29:52 -04:00
TcDrozd 70ce692079
fix(agent_tools): strip display-format "id:" prefix from source_ids and get_reasoning_chain lookups (#795)
* fix(agent_tools): strip display-format "id:" prefix from model-supplied observation IDs

Observations are presented to agents as [id:xxx], and models sometimes
copy the prefix verbatim despite tool-schema instructions to pass the
bare ID. This silently corrupts source_ids provenance on
create_observations_* (broken links stored in document metadata) and
breaks get_reasoning_chain lookups.

Normalize at both entry points. delete_observations is intentionally
not touched here since #746 already covers it.

Only the "id:" prefix is stripped: document IDs are nanoids whose
alphabet includes "-" and "_", so more aggressive cleanup could mangle
legitimate IDs.

Related to #719.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(agent_tools): strip whitespace remaining after "id:" prefix removal

Addresses CodeRabbit review: defends against "id: xxx" with a space
after the colon, and matches the docstring, which already promised
surrounding-whitespace stripping.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-23 16:49:17 -04:00
Ken Weiner a65a406301
feat: send OpenRouter app-attribution headers on OpenAI-compatible clients (#805)
* feat: send OpenRouter app-attribution headers on OpenAI-compatible clients

Sets HTTP-Referer and X-Title on every AsyncOpenAI client constructed in
src/llm/registry.py (default, override-cached, and module-level CLIENTS) and
in the embedding client, so OpenRouter attributes Honcho's requests to the
"Honcho" app in its dashboard/analytics. Other OpenAI-compatible providers
ignore unrecognized headers, so this is safe to send unconditionally.

* fix: scope OpenRouter attribution headers to OpenRouter base URL only

Address review feedback on #805:
- Only inject attribution headers when the configured base_url starts
  with https://openrouter.ai (via new _openrouter_headers() helper)
- Rename X-Title to X-Openrouter-Title per OpenRouter docs recommendation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: drive default headers from base-URL map, drop embedding path

Replace the OpenRouter-specific _openrouter_headers helper with a generic
_DEFAULT_HEADERS_BY_BASE_URL prefix map + _default_headers_for lookup, so
OpenRouter always receives its attribution headers and another provider can be
added with a single map entry. Revert the embedding-client change (OpenRouter
has no embeddings endpoint, so that gate was dead code) and add a unit test for
the lookup helper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 16:11:36 -04:00
Rajat Ahuja 715d8a90b9
fix: compact honcho logging (#836)
* fix: compact honcho logging

* fix: guard ms/s metric formatting against non-numeric values

Only apply float formatting when the metric value is numeric so a
str value with an ms/s unit falls through to a plain string instead
of raising. Applied to both the compact and rich log paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 15:55:03 -04:00
Rajat Ahuja ab8c5aeaa3
feat: add route-level latency metrics (#837) 2026-06-23 14:41:33 -04:00
Aru Sharma ff821e0b4f
docs: add Goose MCP integration guide (#831)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 13:15:30 -04:00
Aakash Kattelu a0cc938f4a
feat: add model config option for json_object mode (#820)
* feat: add model config option for json_object mode

* fix: catch possible validation error from structured output

* fix(llm): harden structured_output_mode json_object path

Follow-up fixes to the json_object structured-output mode for
OpenAI-compatible providers without Structured Outputs support:

- runtime: carry structured_output_mode onto the per-attempt fallback
  config (select_model_config_for_attempt dropped it, silently sending
  json_schema to a provider that can't parse it)
- backend: return a graceful empty on a contentless json_object
  response instead of raising, matching the json_schema path, and
  preserve token usage by normalizing the response
- backend: narrow the parse-failure catch to BadRequestError only, so
  transient JSONDecodeError/ValidationError propagate to retry/fallback
  instead of being swallowed to empty on the first attempt
- config: reject structured_output_mode on non-openai transports
  (silent no-op otherwise); trim docs to the deriver, the only
  structured-output feature
- backend: validate clean JSON before repair, cache the schema
  instruction, and share json_object setup between complete()/stream()

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(llm): consolidate structured-output repair, drop dead seam

Fold the OpenAI backend's three structured-output repair sites
(LengthFinishReasonError, parsed=None, json_object) into the one shared
_parse_or_repair_structured_content helper, gated by an empty_on_missing
flag: json_object returns a graceful empty on a contentless response so a
loose provider can't crash the call, while json_schema raises so the
retry/fallback chain engages.

Delete the dead execute_structured_output_call seam and its only
collaborators (attempt_structured_output_repair, StructuredOutputFailurePolicy)
— it was never called and its single-shot validate/repair/empty model
conflicts with the retry behavior in honcho_llm_call.

No behavior change. Adds tests covering the json_schema parse fallbacks
(repair, refusal passthrough, no-content raise).

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 10:42:03 -04:00
Aakash Kattelu e8ef1a06e5
Track user and session ID on Langfuse traces (#814)
* telemetry: use session and user IDs in langfuse

* test: update old span test

* fix: disable langfuse in unit tests

* fix: add post-loop synthesis span

* refactor: address PR review feedback on langfuse tracing

- Consolidate track_name onto LLMTelemetryContext as the sole home;
  remove the honcho_llm_call kwarg and update 4 callers to set it on
  telemetry directly. Sentry ai_track now reads telemetry.track_name.
- Decouple escaped-stream self-stamping from run-context exit ordering:
  stream_final_response now resets _in_agent_run explicitly around drain.
- Narrow langfuse_agent_step wrap in the tool loop — between-turn
  bookkeeping (iteration_callback, choice switch, increment) lifted
  outside the span so it scopes only the LLM call + tools.
- Reword test conftest comment to behavior-only language.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: switch langfuse spans to imperative handles

Replaces the context-manager-based langfuse_agent_run/step with imperative
LangfuseAgentRun/Step handles so the run span can outlive the function that
opens it. Streaming responses now own the run handle from construction and
close it after drain, stamping the accumulated streamed text as trace output
(previously blank). Multi-turn generations always stamp provider/model and
step metadata, fixing the regression where only the first turn was annotated.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(llm): record effective prompt-only input on run span

The run-level Langfuse span recorded the raw messages parameter, which is
None for prompt-only calls. Mirror execute_tool_loop's handling and record
the synthesized user message so the trace input isn't blank.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(llm): drop StreamingResponseWithMetadata.__anext__ to prevent span leak

The standalone __anext__ delegated straight to the inner stream, bypassing
the token-folding and Langfuse run-handle close that live only in the
__aiter__ generator. Any caller driving the wrapper via anext() instead of
`async for` would leak the run span and lose final-stream token accounting.
Latent today (all callers use `async for`), removed to close the footgun.

Add tests covering the run-handle drain path: full drain stamps the
accumulated streamed text as the span output and closes once; an abandoned
stream still closes via the finally rather than leaking.

* chore(llm): document intentional empty-body propagate_attributes block

The `with propagate_attributes(...): pass` stamps the active @observe trace
root via the context manager's __enter__ side effect; the empty body reads
as deletable dead code. Add a comment so it isn't removed. Addresses PR review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(llm): restore api.py types after __anext__ removal

Dropping StreamingResponseWithMetadata.__anext__ made it stop satisfying
the AsyncIterator protocol, breaking the result annotation and the
isinstance narrowing in honcho_llm_call. Widen the tool-less result
annotation to include StreamingResponseWithMetadata and narrow positively
to HonchoLLMCallResponse before reading .content.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-06-23 00:03:21 -04:00
Vineeth Voruganti e2ff106f28
Filter noisy sentry traces/profiles (#834)
* perf(reconciler): only trace Sentry transactions when work is found

The reconciler enqueues sync_vectors every ~5 min per deriver instance.
process_item wrapped every dequeued reconciler task in a single
process_reconciler_task transaction, so idle cycles (the common case,
where the cycle finds no rows and exits immediately) still created and
sampled a transaction + profile, draining Sentry tracing/profiling quota.

Remove the top-level transaction and push tracing into the sync batch
helpers, starting a per-batch transaction only after rows are confirmed.
Idle cycles now emit zero transactions; busy sweeps emit one smaller
transaction per batch operation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* perf(telemetry): drop infra/scrape transactions via a Sentry traces sampler

Sentry was sampling every transaction at a flat traces_sample_rate with no
sampler. The Prometheus /metrics scrape endpoint alone accounted for ~92% of
all traced transactions (and their profiles), with /openapi.json and the
deriver metrics server adding more pure noise.

Add a traces_sampler that returns 0.0 for infra/scrape endpoints (/metrics,
/health, /openapi.json, /docs, /redoc, and metrics/openapi transaction names)
and the configured rate for real traffic. Sampling here (vs
before_send_transaction) means dropped transactions are never recorded or
profiled and the decision propagates to child spans. Shared init covers both
the API server and the deriver worker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 21:14:55 -04:00
Harish Kukreja 414e31c960
feat(deriver): age-flush stalled representation batches (#826)
Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-06-22 17:49:08 -04:00
Rajat Ahuja 326a757cdb
Fix scoped JWTs (#679)
* Peer- and session-scoped JWTs were effectively workspace-scoped: auth() walked the route's declared scope and fell through to a workspace match, so a {w: ws-a, p: alice} token could act on any peer in ws-a.

* feat: peer keys can read sessions they belong to; require workspace on scoped keys

* fix: authorize JWTs by narrowest scope and gate member reads

Follow-up hardening on the narrowest-claim auth fix:

- Scope get_peer_config member-read to the caller's own peer; a session
  member could previously read a co-member's per-session config.
- Enforce session membership on POST /peers/{id}/chat: the session_id
  arrives in the body (invisible to require_auth), so a peer key could
  read any session's injected message history. Check is_peer_in_session
  in the handler before the dialectic runs.
- Consolidate the workspace-match check in auth() to a single hoisted
  guard so no branch can silently re-open cross-workspace access.
- Normalize empty-string scope claims to None in verify_jwt so a blank
  workspace can't satisfy the peer/session token-shape invariant.
- Extract scope_requires_workspace(), shared by verify_jwt and the keys
  API so the creation-time guard and verification invariant can't drift.
  route requires auth) and CLAUDE.md auth-scoping guidance.
- docs: describe narrow-scope key semantics in the platform reference.

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-06-22 17:30:00 -04:00
Vineeth Voruganti f8bcfa4aa5
Forward provider_params to underlying transport (#821)
* feat(llm): forward provider_params passthroughs (extra_body/headers/query) across backends

* refactor(llm): share provider_params passthrough merge + validate shapes

Extract apply_sdk_passthroughs/coerce_passthrough_mapping into
request_builder; reject non-mapping passthrough values with a clear
ValidationException; cover the Anthropic stream() path; document the
keys in configuration.mdx.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: address coderabbit docs nitpicks

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 15:48:59 -04:00
Aakash Kattelu 99312958c5
docs(env): add Dialectic base URL override examples to .env.template (#833)
The Dialectic section was the only LLM section in .env.template missing
MODEL_CONFIG__OVERRIDES__BASE_URL examples. Without them, users routing
to OpenAI-compatible providers (e.g. Siliconflow) weren't aware the
per-level override existed and fell back to the default OpenAI endpoint,
hitting AuthenticationError. The override already works; this just
enumerates it per reasoning level.

Fixes #818

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 11:45:34 -04:00
Aru Sharma a2adeb9f45
Fix surprisal tree kwarg mismatch (#749)
* bug fix

* test: add tests for create_tree k-kwarg handling
2026-06-19 13:49:04 -04:00
Hafiz Ahmad Ashfaq 99cbebe30e
fix(llm): coerce None output_tokens to 0 in completion_result_to_response (#809)
Some providers return output_tokens=None on certain completions (observed with
Gemini on tool-loop completions). HonchoLLMCallResponse types output_tokens as
int, so the None propagates into a Pydantic validation error that aborts the
call. In practice this surfaces in the Dreamer: a dream starts, deduction
succeeds, then induction crashes before inductive conclusions are persisted.

Coerce None -> 0 so token accounting degrades gracefully (under-counts rather
than crashing) for providers that omit output token counts.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 16:55:10 -04:00
Vineeth Voruganti aa993a6ddd
chore(docs): Release Candidate for v3.0.10 (#813) 2026-06-15 17:19:51 -04:00
adavyas 40513845f8
fix: optimize deriver and dreamer prompt cache prefixes (#806)
* Optimize dreamer prompt cache prefix

* Optimize deriver prompt prefix caching

* fix: specifying target peer

* fix: specifying target observee
2026-06-15 10:58:02 -07:00
xianzuyang9-blip 340175ad5f
fix: declare click as honcho-cli dependency (#787)
* fix: declare click as honcho-cli dependency

* fix: declare click as honcho-cli dependency

* fix: declare click as honcho-cli dependency
2026-06-11 12:58:11 -04:00
Eri Barrett f20a13926e
fix(dedup): reinforce times_derived on duplicate detection (#768)
* fix(dedup): reinforce times_derived on duplicate detection

times_derived was never incremented: the reject-new branch dropped the
reinforcement and the new-wins branch reset the count to 1, so the column
stayed pinned at 1 for nearly every conclusion. With every value equal,
ORDER BY times_derived DESC resolved to arbitrary heap order (oldest rows
first), which froze stale conclusions to the front of injected context.

- reject-new: increment existing_doc.times_derived
- new-wins: carry existing count forward onto the replacement
- add created_at DESC tiebreaker to both most_derived queries

* test(dedup): guard times_derived reinforcement + recency tiebreak

Three regression tests, each fails on pre-fix code:
- most-derived ties break toward recency, not insertion order
- rejecting a duplicate reinforces the surviving doc
- a winning duplicate inherits the replaced doc's count + 1

* fix(dedup): atomic reinforcement increment + deterministic tiebreak

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-06-11 12:37:19 -04:00
Rajat Ahuja 44c85fa3e2
fix: private ip address check for webhook creation (#793) 2026-06-11 10:35:20 -04:00
Rajat Ahuja 6aa6033a16
feat: defer embedding messages (#704)
* feat: defer embedding messages

* fix: rm gauges

* feat: embed messages immediately on create with reconciler fallback (#766)

Adds embed_messages_now background task so newly created messages are
searchable within seconds instead of waiting up to the reconciler
interval. Three-phase claim/lease → embed → persist never holds a DB
session across the embedding call; the reconciler remains the fallback
for failures and stragglers.

* fix: harden immediate-embed fast path and cover its error branches

Wrap embed_messages_now in a top-level try/except so a failure in the
claim or persist phase degrades to "reconciler will retry" instead of
escaping into the background-task runner; the rows stay pending+leased
and the reconciler heals them.

Add tests for the previously-uncovered branches: external-store-unavailable
persist path, the file-upload endpoint's embed scheduling, and direct unit
tests for the shared compute_chunk_positions / build_message_vector_record
helpers.

Document the semantic-search eventual-consistency window in search.mdx
(keyword matches are immediate; vector matches lag creation by seconds).

* fix: don't hold DB session across vector-store upserts

* fix: align semantic-search function to filter null rows

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-06-11 10:31:04 -04:00
Rajat Ahuja bf494257b8
add read db (#773)
* feat: implement read DB and fix queue stale cleanup

* fix: use read_db in internal methods

* fix: mention read db in the CLAUDE.md

* fix: make TRACING checkout hook autocommit-safe; sample cleanup-gate jitter once

The DB.TRACING checkout hook ran `SELECT set_config(...)` at pool checkout,
before the dialect applies the read engine's AUTOCOMMIT isolation level. That
statement autobegins a transaction, and psycopg then refuses to switch the
connection into AUTOCOMMIT ("can't change 'autocommit' now: connection in
transaction status INTRANS"), so every read_only session 500s under TRACING and
the INTRANS connection leaks back to poison later write checkouts. Run the hook
in autocommit and restore the prior mode so it never leaves an open transaction;
set_config(..., is_local=false) is session-scoped and survives the boundary.
Add a regression test (fails without the fix) covering read_only + TRACING.

Also sample the stale-cleanup gate's jittered interval once per attempt instead
of re-rolling it every poll, so the spacing is a fixed deadline per cycle rather
than a random walk (and is testable at non-zero jitter ratios).

* fix: reset request_context in TRACING checkout-hook test

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-06-10 13:28:36 -04:00
Anthony Yuan f75b336a3c
feat: add generate_jwt.py script for creating scoped JWTs (#757)
* feat: add generate_jwt.py script for creating scoped JWTs

Adds a CLI utility script for generating Honcho JWTs without needing
to call the /v1/keys API endpoint. Useful for local development and
bootstrapping admin tokens.

Features:
- --admin flag for full-access tokens
- --workspace / --peer / --session flags for scoped tokens
- --expires flag with human-friendly duration syntax (e.g. 5h, 30d, 1y)
- --print-only flag for scripting (outputs bare token)

Examples:
  uv run python scripts/generate_jwt.py --admin
  uv run python scripts/generate_jwt.py --admin --expires 24h
  uv run python scripts/generate_jwt.py --workspace my-ws --expires 30d
  uv run python scripts/generate_jwt.py --workspace my-ws --peer my-peer --expires 1y

* docs: document generate_jwt.py in README auth setup section

* fix: remove t='' override to preserve utc_now_iso default in JWTParams

Per CodeRabbit review: explicitly setting t="" bypasses JWTParams's
default utc_now_iso timestamp, causing tokens for the same scope to
become byte-identical. Omitting t lets the default apply, ensuring
each generated token is unique.

* fix: address JWT script review feedback

* fix: type, lint

---------

Co-authored-by: Rajat Ahuja <rahuja445@gmail.com>
2026-06-09 13:49:55 -04:00
Raúl Anatol 5a3b598cb4
feat(config): make CORS allowed origins configurable via env (#697)
* feat(config): make CORS allowed origins configurable via env

Replaces the hardcoded `origins` list in `src/main.py` with a new
`CORSSettings` block (env prefix `CORS_`), exposed as `settings.CORS.ORIGINS`.
Defaults match the prior hardcoded values, so self-hosted deployments behind
custom domains can now whitelist their frontend without editing source.

Documented in `.env.template` under a new CORS Settings section.

* docs(config): add docstring to CORSSettings

* refactor(config): inline CORS_ORIGINS into AppSettings

Drop the dedicated CORSSettings nested model and expose CORS_ORIGINS
directly on AppSettings. The CORS_ORIGINS env var keeps working as
before since AppSettings has no env prefix.
2026-06-09 13:49:46 -04:00
Vineeth Voruganti 9f26fdd2ea
Deriver Jitter (#765)
* fix(deriver): Remove connection retry logic and add jitter to polling interval

* chore(docs): Update changelog and document new configurations

* chore: increment version numbers
2026-06-02 11:48:36 -04:00
Vineeth Voruganti bb6dad9157
v3.0.8 Release Candidate (#763)
* chore(docs): Update changelogs for v3.0.8

* chore: update configuration docs
2026-06-01 15:37:05 -04:00
Vineeth Voruganti 396976db34
Connection Exponential Backoff (#758)
* feat(db): add connection retry, adaptive deriver polling, and pool metrics

Add resilience and visibility for DB connection handling under transaction-
pooler (Supavisor) saturation, where client-connection limits get exhausted
across many tenants.

- get_db/tracked_db now force an eager pool checkout with bounded exponential
  backoff (tenacity), retrying SQLAlchemy TimeoutError + OperationalError so
  transient pooler rejections degrade gracefully instead of 500ing. Toggle via
  DB_CONNECTION_RETRY_ENABLED (+ delay/backoff knobs); ~10s default budget.
- Deriver polling backs off when idle or erroring (base -> max, x2 each cycle)
  and snaps back to base on claimed work, cutting steady-state query load.
  Toggle via DERIVER_POLLING_BACKOFF_ENABLED (+ max/multiplier).
- Add scrape-time db_pool_connections Prometheus gauge (checked_out/checked_in/
  size/overflow, labeled api|deriver), registered in both the API lifespan and
  the deriver metrics server.
- Make SqlalchemyIntegration explicit in both Sentry inits; wrap connection
  acquisition in a db.pool.acquire span and capture live pool stats on
  retry-exhaustion.

* feat(db): add acquisition counter and in-flight query gauge

Build on the pool-connection metrics with two signals that turn detection
into diagnosis under transaction-pooler saturation:

- db_connection_acquisitions{outcome=ok|retried|exhausted}: counts how often
  connection checkout retries through pooler rejection — the alertable early
  warning before requests start failing.
- db_queries_in_flight: statements actually executing on the wire (via
  SQLAlchemy cursor-execute events, drift-proof across query errors). Pairs
  with checked_out: the gap reveals connections held but parked (the "idle in
  transaction during an external call" antipattern). Labeled namespace +
  instance_type only; gated on METRICS.ENABLED for zero overhead when off.

Add DB-free unit tests for retry outcomes, polling backoff, and in-flight
gauge drift handling.

* fix: address CodeRabbit review on PR #758

- db: roll back the session on a retryable checkout failure before
  retrying — a failed autobegin can leave it pending-rollback, making the
  next db.connection() raise instead of re-checking-out cleanly. Cheap
  Python-side cleanup when no connection was bound.
- metrics: guard DBPoolCollector.collect() so a pool-read/import hiccup
  can't raise and abort the whole /metrics scrape (Prometheus drops ALL
  metrics if any collector raises) — log and fall back to empty.

* fix(db): lazy retrying session + review fixes for connection backoff

Address Codex/CodeRabbit review on PR #758.

- Replace eager checkout with HonchoAsyncSession: a lazy AsyncSession that
  checks out its connection (with retry) on the first DB-touching call, not at
  construction. Request handlers doing non-DB work (embedding/file/LLM) before
  their first query no longer pin a connection across it, while the API path
  still gets checkout retry. Only the checkout is retried — the statement runs
  once via super(), so writes are never duplicated. Tracing's set_config moves
  into the same lazy acquire hook.
- Roll the session back on a retryable checkout failure before retrying, so a
  failed autobegin can't leave it pending-rollback.
- Lower default POOL_TIMEOUT to 5s and validate it stays under the retry budget
  for pooled (non-null) POOL_CLASS; update config.toml.example and v2/v3 docs.
- Clamp pool overflow gauge to >= 0 (was negative before the pool fills).
- Remove double-sleep in the deriver idle poll (true backoff cap, not 2x);
  make in-flight instrumentation registration idempotent.
- Tests: HonchoAsyncSession lazy/idempotent acquire, statement-runs-once,
  tracing, commit/rollback flag reset, get_db no-acquire-at-entry, polling-loop
  single-sleep, and the POOL_TIMEOUT/retry-budget validator.

* fix(db): cover all DB-touching session methods; clear flag on close/reset

Address Codex follow-up review on PR #758 (polish, no behavior-critical bug).

- HonchoAsyncSession: wrap get/get_one/stream/stream_scalars/delete in addition
  to execute/scalar/scalars/flush/merge/refresh/commit, so the "lazy checkout
  with retry on first DB use" guarantee has no holes. connection() stays
  unwrapped (acquire_connection_with_retry calls it — wrapping would recurse).
- Reset the acquired flag on close()/reset() too, so a session reused after
  close/reset re-acquires (and re-wraps retry) on its next DB use.
- Fix stale comments: connection retry now applies lazily to the request path
  via HonchoAsyncSession (config.py), and the FakeSession helper note.
- Tests: close/reset flag reset, and get/delete route through acquisition.
2026-06-01 12:57:07 -04:00
ajspig 85239a69b2
Updating Design Patterns (#717)
* docs: draft of design-patterns

* fix: minor language changes

* docs: adding unified memory guide

* docs: simplifying design patterns

* docs: minor edits

* docs: language clarification

* docs: simplification of intro
2026-05-27 15:25:55 -04:00
Vineeth Voruganti 7470866d12
chore(docs): Update changelogs and increment version (#713) 2026-05-21 14:32:41 -04:00
adavyas 0cf63c10da
feat(api): restore reverse pagination (#685)
* feat(api): restore v3 reverse pagination

* docs: add reverse pagination docstrings

* docs: document session reverse parameter

* fix: add fallback column for ties

* refactor: tighten reverse query typing

* chore: pre-commit styling

* chore(tests): Add additional validation tests and update changelogs

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-05-21 13:40:47 -04:00
Vineeth Voruganti 4f579d5c66
fix: reframe peer card prompts as stable identity markers (#686)
* fix: reframe peer card prompts as stable identity markers

* fix: remove strict parameter validation for thinking on anthropic and openai transports

* fix(dreamer): Add backwards compatability instructions for peer card prompt
2026-05-21 13:25:34 -04:00
Vineeth Voruganti 10f72a7d0d
fix(sdk): add peer field to session creation methods (#705) 2026-05-21 12:31:53 -04:00
Rajat Ahuja b5f24a6ac5
feat: add new cloudevents for api routes (#637)
* feat: add new cloudevents for api routes

* fix: add total input tokens to RepresentationCompletedEvent

* feat(telemetry): inject honcho_version + emitter health metrics

* feat(telemetry): per-LLM-call event with try/finally emission + sampler

Adds LLMCallCompletedEvent (llm.call.completed) — fires once per provider hit
with full cost-attribution context: transport/provider_label, model, token
counts with cache breakdown, finish_reason, outcome (success or error),
is_final_attempt flag, retry/fallback state, duration, tool-call shape,
streaming flag, and agent correlation (run_id + iteration).

- src/telemetry/events/llm.py: new event class + CallPurpose closed enum
  (deriver.representation, dialectic.answer, dream.deduction|induction,
  summary.short|long). Resource id includes attempt so multi-attempt retries
  in one iteration get distinct deterministic ids.
- src/telemetry/events/base.py: BaseEvent._volume_class ClassVar (default
  "ground_truth"); the new event opts into "high_volume".
- src/config.py: TelemetrySettings.HIGH_VOLUME_SAMPLE_RATE (default 1.0).
- src/telemetry/emitter.py: deterministic sampler keyed on run_id (so an
  entire agent trace is kept or dropped together). Aggregate envelopes
  bypass the sampler. Sampled-out events increment the dedicated counter
  separate from buffer_full/send_failed drops.
- src/llm/runtime.py: AttemptPlan gains attempt/retry_attempts/is_fallback
  so the executor reads retry state without re-deriving it.
- src/llm/types.py: LLMTelemetryContext dataclass carrying workspace,
  call_purpose, run_id, iteration, peer fields. Iteration is mutable so
  the tool loop can set it per inner call.
- src/llm/executor.py: honcho_llm_call_inner wraps the backend call in
  try/finally — emits on success AND on exception, with is_final_attempt
  computed from AttemptPlan. Stream path emits a was_stream=True placeholder
  (token totals deferred until streaming completion is wired through).
  Telemetry failures swallowed.
- src/llm/api.py: threads telemetry kwarg through all 4 signatures into
  both honcho_llm_call_inner and execute_tool_loop.
- src/llm/tool_loop.py: _telemetry_for_iteration helper copies the caller
  context with iteration set per call — covers both the normal iteration
  loop AND the max-iteration synthesis call (iteration N+1).

Tests cover success/error emission, sampler trace-coherence (same run_id →
same decision), volume_class enforcement, unknown call_purpose tolerance,
provider_label inference, and telemetry failure isolation. 378/378 pass.

* feat(telemetry): emit agent.iteration on every LLM response + synthesis

AgentIterationEvent was defined but never emitted on this branch. Phase 2
wires it up in execute_tool_loop so every LLM call inside an agentic loop
produces one event — including the no-tool terminating iteration and the
max-iteration synthesis call — and threads LLMTelemetryContext from dialectic
and dreamer specialists down through honcho_llm_call.

- src/telemetry/events/agent.py: AgentIterationEvent opts into
  _volume_class="high_volume" so the Phase 1 sampler throttles it.
- src/llm/tool_loop.py: _emit_agent_iteration() helper fires once per
  honcho_llm_call_inner response, BEFORE the no-tool early return so the
  terminating iteration is counted. A second emission fires for the
  max-iteration synthesis call BEFORE final_response is mutated with
  cumulative totals (otherwise the per-iteration counts would double-count).
  Emission is defensively skipped when telemetry context lacks run_id /
  agent_type / parent_category / workspace_name; emit failures are swallowed.
- src/dreamer/specialists.py: BaseSpecialist.run passes LLMTelemetryContext
  with parent_category="dream", agent_type=self.name, observer/observed,
  call_purpose=f"dream.{self.name}".
- src/dialectic/core.py: _telemetry_context() builds a shared context for
  both answer() and answer_stream(), using self._run_id (always set) +
  workspace + observed peer.

Tests cover fresh-copy semantics, per-iteration vs terminating emission,
defensive skip cases, telemetry-failure isolation, and volume_class. 408/408
pass across telemetry + llm + utils + dreamer + dialectic.

* feat(telemetry): agent.tool.call.completed event + ToolResult metadata

Adds the missing generic per-tool-call event so read-only tools (search_*,
get_recent_history, get_observation_context, etc.) and the four existing
state-change tools all produce a telemetry record. Built on a new internal
ToolResult(content, metadata) contract so handlers can surface
search-specific fields (top_k/used_embedding/query_tokens/results_count)
to Phase 3 and create/delete counts to Phase 5's specialist rollups.

- src/telemetry/events/agent.py: AgentToolCallCompletedEvent at v1 with
  _volume_class="high_volume". Resource id = {run_id}:{iteration}:{tool_call_seq}
  so two calls to the same tool in one iteration don't collide
  deterministic ids and get dedup-dropped downstream.
- src/utils/types.py: ToolResult dataclass; two new ContextVars
  (_current_tool_call_seq + _last_tool_metadata) so tool_loop and the
  execute_tool closure can communicate per-call telemetry without changing
  the public Callable[[str, dict], Any] signature.
- src/utils/agent_tools.py: execute_tool times handlers, unwraps ToolResult,
  publishes metadata, emits the event. Handlers updated to ToolResult
  where useful: create/delete observations, update_peer_card, search_memory,
  search_messages. Other handlers continue to return str.
- src/llm/tool_loop.py: set_current_tool_call_seq before each executor call;
  read get_last_tool_metadata after and stash on all_tool_calls[i] for
  Phase 5 rollups.

Tests cover ToolResult str-likeness, ContextVar round-trip, full-context
emission with search metadata, resource-id disambiguation, defensive skip
cases, telemetry isolation, truncation metadata, volume_class. 420/420 pass.

* feat(telemetry): RepresentationCompletedEvent v2 token breakdown + tool-less truncation

Bulks out the deriver's per-batch telemetry without bumping the event schema
version. New additive fields capture the full token breakdown (queued vs.
extra-context vs. scaffold), the cap configuration (batch_max_tokens,
max_input_tokens, was_flush_enabled), real cap-hit flags, and observer
fanout. `input_tokens` stays unchanged as the queued-message-tokens billing
key Xatu's Stripe meter reads.

The big enabler: src/llm/api.py now actually enforces max_input_tokens on
the tool-less LLM path. Before this, the deriver passed the kwarg but the
path silently dropped it — so the configured cap was advisory and
hit_input_token_cap couldn't be measured. Phase 4 wires truncation through
the same truncate_messages_to_fit helper the tool loop uses and surfaces
input_was_truncated on HonchoLLMCallResponse.

- src/telemetry/events/representation.py: 12 additive fields, schema_version
  stays at 2.
- src/llm/types.py: input_was_truncated on HonchoLLMCallResponse.
- src/llm/api.py: tool-less path truncates messages before dispatch, flips
  input_was_truncated on the response when clamping occurs. Split into
  Literal[True]/Literal[False] branches for typecheck.
- src/deriver/queue_manager.py: QueueBatchResult dataclass replaces the
  3-tuple return from get_queue_item_batch; carries hit_batch_token_cap
  (computed from cumulative token sum vs cap), was_flush_enabled snapshot,
  and batch_max_tokens. Worker loop unpacks + forwards.
- src/deriver/consumer.py: process_representation_batch gains the three
  flag kwargs and forwards.
- src/deriver/deriver.py: derives the breakdown fields locally, populates
  the new fields on emit, sources hit_input_token_cap from
  response.input_was_truncated.

Tests cover schema stability, defaultable fields, input_tokens semantic
preservation, cap-hit flag round-trip, model_dump completeness, and
HonchoLLMCallResponse.input_was_truncated mutability. Existing
test_queue_processing.py tests updated for QueueBatchResult and mock
process_representation_batch signature. 479/479 pass.

* feat(telemetry): DreamRunEvent v2 scheduler reasons + DreamSpecialistEvent v2 rollups

Bumps both dream events to v2 with additive fields. DreamRunEvent gains
scheduler context (threshold_reason / delay_reason / documents_since_last_dream_at_schedule /
document_threshold / dream_type / enabled_types_count) threaded through the
dream queue payload — the two scheduler gates stay as separate fields rather
than collapsing into one trigger_reason, preserving the WHY-vs-WHEN
semantics. DreamSpecialistEvent gains denormalized rollups
(created_observation_count / deleted_observation_count / peer_card_updated /
search_tool_calls_count) sourced from Phase 3's ToolResult.metadata so the
counts reflect observation truth, not call truth.

- src/telemetry/events/dream.py: schema_version → 2 for both events; new
  fields all defaultable so older producers still construct valid events.
- src/utils/queue_payload.py: DreamPayload + create_dream_payload accept
  threshold_reason / delay_reason / documents_since_last_dream_at_schedule /
  document_threshold.
- src/dreamer/dream_scheduler.py: check_and_schedule_dream computes the two
  reasons at decision time and threads them through schedule_dream →
  _delayed_dream → execute_dream → enqueue_dream.
- src/deriver/enqueue.py: create_dream_record / enqueue_dream gain the
  kwargs and persist on the queue payload.
- src/dreamer/orchestrator.py: process_dream unpacks the payload; run_dream
  accepts the kwargs and stamps them on DreamRunEvent.
- src/dreamer/specialists.py: BaseSpecialist.run walks response.tool_calls_made
  and sums ToolResult.metadata.created_count / .deleted_count, sets
  peer_card_updated, counts search-tool calls by name.

Tests cover schema_version bumps, defaultable Phase 5 fields,
threshold-vs-delay semantics, observation-vs-call-count rollup distinction,
and DreamPayload round-trip. Existing tests updated for the schema bump
and the new enqueue_dream kwargs. 488/488 pass.

* feat(telemetry): AgentToolSummaryCreatedEvent v2 token breakdown

Bumps schema_version to 2 and adds three additive breakdown fields so
analytics can answer "how much of a summary call's cost was the previous-
summary rollup vs. the new messages vs. the scaffold instructions".

- src/telemetry/events/agent.py: previous_summary_tokens, message_tokens,
  prompt_scaffold_tokens added with sensible 0 defaults. input_tokens
  retains its current semantic (provider-side LLM tokens) — the plan's
  proposed `provider_input_tokens` was omitted because input_tokens
  already serves that purpose and a duplicate would fork queries.
- src/utils/summarizer.py: emit now populates the three new fields from
  values already in scope (messages_tokens, previous_summary_tokens,
  prompt_tokens). Hoisted prompt_tokens calculation out of the
  is_fallback conditional so both the save-summary path and the emit
  share one binding — basedpyright couldn't prove the sibling-scope
  binding was safe, and the compute is cheap + idempotent.

Tests cover schema bump, defaultable fields, input_tokens semantic
preservation, first-summary edge case, and breakdown round-trip.
493/493 pass.

* feat(telemetry): embedding.call.completed event + call-purpose ContextVar

Adds the final piece of cost-attribution telemetry: per-embedding-call
events covering every provider hit (single + batch + retry attempts).
Embedding calls are real provider spend that was invisible before this
phase; search-heavy paths (dialectic agentic) can produce more embedding
calls than LLM calls, so the new event participates in the shared
HIGH_VOLUME_SAMPLE_RATE.

- src/telemetry/events/llm.py: EmbeddingCallCompletedEvent at v1 with
  _volume_class="high_volume". EmbeddingCallPurpose closed enum
  (search_memory / search_messages / create_observations / vector_sync /
  summary / message_create). Resource id = run:purpose:provider:model:input_count
  so per-iteration calls in one agentic run don't collide.
- src/utils/types.py: _embedding_call_purpose ContextVar plus
  @contextmanager wrapper. Nesting-safe via ContextVar.reset(token).
  Callers wrap embedding-driving operations in
  `with embedding_call_purpose("search_memory"): ...` — no changes to
  the embedding client signature.
- src/embedding_client.py: _emit_embedding_call wraps each provider hit
  with try/finally so success AND error paths emit. Errors propagate
  unchanged. Each retry attempt of _process_batch emits its own event.
  Unknown call_purpose slugs drop to None (validation against the enum
  happens at emit time, not at context-manager-set time).
- src/utils/agent_tools.py: search_memory / search_messages /
  search_messages_temporal / create_observations (batch + fallback) all
  tag their embedding calls.
- src/crud/representation.py: save_representation tags with
  CREATE_OBSERVATIONS; get_working_representation precompute tags with
  SEARCH_MEMORY.
- src/crud/message.py: create_messages batch embed tags with
  MESSAGE_CREATE; search_messages/temporal fallback tags with
  SEARCH_MESSAGES.

Tests cover event shape, enum closure, ContextVar nesting/exception
cleanup, wrapper success+error emission, unknown-purpose graceful
fallback, telemetry-failure isolation. 550/550 pass across the full
telemetry+llm+utils+dreamer+dialectic+deriver+crud test set.

* chore: fix tests

* fix(telemetry): address review findings on stream events, context propagation, and cap detection

Five findings from a post-Phase-7 review (one resolved by the merge from
main, four addressed here):

- src/llm/executor.py: stream-path LLMCallCompletedEvent now fires AFTER
  the stream is set up and drained (or on exception), with real duration
  and accurate outcome. Previously the event was emitted before
  execute_stream() ran and was always recorded as outcome="success" with
  duration_ms=0, which silently masked stream-setup and stream-drain
  failures. Wrapping the async generator in try/finally surfaces the real
  outcome; token counts stay 0 because we still don't have them at stream
  end (aggregate envelopes carry totals).

- src/deriver/deriver.py + src/utils/summarizer.py: deriver and summarizer
  LLM calls now thread LLMTelemetryContext into honcho_llm_call. Before
  this, the closed CallPurpose enum had DERIVER_REPRESENTATION /
  SUMMARY_SHORT / SUMMARY_LONG slugs but those production call sites
  didn't actually pass `telemetry=`, so their LLMCallCompletedEvents lost
  workspace_name, parent_category, and call_purpose. summarizer threads
  workspace_name through _create_and_save_summary → _create_summary →
  create_short_summary / create_long_summary.

- src/utils/types.py + src/embedding_client.py: embedding_call_purpose
  ctx manager now accepts workspace_name and run_id kwargs, backed by
  two new ContextVars. EmbeddingCallCompletedEvent's publisher reads
  both via get_embedding_workspace_name / get_embedding_run_id so
  embedding events carry workspace and run correlation. All call sites
  updated: search_memory / search_messages / search_messages_temporal /
  _handle_create_observations_impl pass ctx.workspace_name +
  ctx.run_id; create_observations standalone and create_messages pass
  workspace_name; RepresentationManager.save_representation and
  get_working_representation pass self.workspace_name.

- src/deriver/queue_manager.py: hit_batch_token_cap detection rewritten.
  Previously summed kept-rows' token_count and checked against
  batch_max_tokens, but the SQL filter `cumulative_token_count <= cap`
  guarantees kept rows stay under the cap, so the flag almost never
  fired. Now uses two follow-up queries: total token_count across the
  included id range + EXISTS check for any session message past the
  last-kept id. Both true → cap was actually binding.

(The fifth finding — deriver scaffold-token computation needing
estimate_deriver_prompt_tokens(custom_instructions) — was resolved by
the merge from main; the Phase 4 emit at src/deriver/deriver.py:283
already sources prompt_scaffold_tokens from the wrapped helper.)

567/567 telemetry+llm+utils+dreamer+dialectic+deriver+crud tests pass.
ruff + basedpyright clean.

* chore: ruff linting

* chore: clean AI generated comments references specs

* fix: address coderabbit changes

* fix(telemetry): address remaining PR review findings

Six findings from the PR 637 telemetry review batched into one commit.

- src/llm/executor.py + src/embedding_client.py: asyncio.CancelledError
  now surfaces as outcome="cancelled" on both stream and sync paths,
  distinct from "error". Client disconnects mid-stream and server
  shutdowns are normal control flow and should not feed error-rate
  alerting. LLMCallCompletedEvent and EmbeddingCallCompletedEvent
  outcome Literal extended; docstrings + tests cover the new state.

- src/utils/types.py + src/llm/tool_loop.py: new iteration_scope()
  context manager captures and resets the four per-tool-loop
  ContextVars (_current_iteration, _current_tool_call_seq,
  _current_provider_tool_call_id, _last_tool_metadata). Applied as a
  typed decorator to execute_tool_loop so back-to-back loops in the
  same asyncio Task (worker batches, tests) don't observe stale state.

- src/telemetry/events/api.py + src/routers/messages.py:
  MessageCreatedEvent schema v1 → v2. Added required last_message_id
  (nanoid public_id of the trailing message); get_resource_id now keys
  on it instead of message_count, eliminating the collision case where
  two same-size batches in the same session+source produced identical
  event ids. message_count stays on the body for analytics.

- src/deriver/queue_manager.py: hit_batch_token_cap now computed from
  the FINAL post-config-filter batch. Previously the flag used the
  pre-filter messages_context[-1].id, which produced false positives
  when _resolve_batch_configuration trimmed the trailing queue item —
  telemetry reported a cap-hit when the actual returned batch was
  short for unrelated reasons. Cap-detection block moved inside the
  async with after the filter; no extra DB connection.

- src/config.py + src/telemetry/emitter.py: documented the
  HIGH_VOLUME_SAMPLE_RATE orphan trade-off (rate<1.0 keeps aggregates
  but drops children, so JOIN ON run_id queries see partial traces).
  Behavior unchanged — rate defaults to 1.0.

- src/deriver/deriver.py: WARNING-level invariant logs when
  response.input_tokens < messages_tokens (provider tokenization
  drift) or prompt_scaffold_tokens <= 0 (estimator silent failure).
  Best-effort — telemetry never bleeds into the deriver path

* fix(telemetry): stream retry, embed attempts, truncation, dedup

Address remaining audit findings on the cloudevents PR:

- Stream setup now runs inside the awaited honcho_llm_call_inner so
  tenacity's retry wrapper in stream_final_response catches transient
  setup failures (rate-limit, auth, network). Previously the returned
  generator deferred execute_stream until first iteration — outside
  the retry wrapper — crashing the request and bypassing telemetry.
- Embedding _emit_embedding_call gains an is_final_attempt parameter;
  _process_batch threads the real retry index so dashboards stop
  conflating one-shot, mid-retry, and exhausted-retry calls.
- _truncate_tool_output returns (text, original_chars, was_truncated)
  and a new _maybe_truncated_result helper wraps in ToolResult when
  truncation happens. Five handlers migrated. AgentToolCallCompletedEvent
  fields was_truncated and result_chars_before_truncation are now
  populated instead of always None/False.
- execute_tool_loop tracks any_iteration_truncated and stamps
  input_was_truncated on the final response (both HonchoLLMCallResponse
  and StreamingResponseWithMetadata). Dialectic now reports
  hit_input_token_cap correctly.
- GetContextEvent.get_resource_id uses empty-string sentinel instead
  of literal "none" so a peer named "none" can't collide with absent.
- generate_event_id folds honcho_version into the deterministic id so
  same logical event from different deploys produces distinct ids.

* fix(telemetry): address audit findings across LLM/embed/event paths

Three rounds of telemetry audit findings, grouped by area:

Retry correctness
- Stream LLM setup now runs inside the awaited honcho_llm_call_inner so
  tenacity's outer retry catches setup failures (Fix 1). Previously the
  inner generator deferred execute_stream past the retry wrapper.
- stream_final_response bumps the per-retry attempt index via
  dataclasses.replace so emitted events show [1, 2, 3] instead of
  [1, 1, 1] (Fix 13).
- Embedding _emit_embedding_call takes is_final_attempt; _process_batch
  threads the real retry index (Fix 2).

Token + cost reporting
- HonchoLLMCallResponse.hit_input_token_cap (renamed from
  input_was_truncated) uses a token-based rule so single-message
  over-cap inputs are correctly flagged — the deriver's prompt-only
  path used to silently fly through. Propagated through tool_loop's
  per-iteration check (Fix 4) and into RepresentationCompletedEvent.
- DialecticCompletedEvent gains hit_input_token_cap; output_tokens now
  folds in the final-stream's cumulative usage via
  StreamingResponseWithMetadata.__aiter__ (Fix 7).

Event emission completeness
- AgentToolCallCompletedEvent's was_truncated /
  result_chars_before_truncation populated by _truncate_tool_output via
  a new _maybe_truncated_result wrapper; 5 handlers migrated (Fix 3).
- DreamSpecialistEvent emits on failure with success=False + new
  error_class field, via try/finally (Fix 11).
- DeletionCompletedEvent emits on failure paths via try/finally
  (Fix 12).
- CleanupStaleItemsCompletedEvent.queue_items_cleaned populated from
  deleted_count (Fix 8).

Embedding call attribution (Fix 9)
- embedding_call_purpose context manager accepts parent_category.
- 4 new EmbeddingCallPurpose enum values: DIALECTIC_PREFETCH,
  SESSION_CONTEXT_SEARCH, PREFERENCE_EXTRACTION, GENERIC_DOCUMENT_SEARCH.
- Wrapped previously-unattributed sites: dialectic prefetch, session
  context search, preference extraction, conclusions search, vector
  sync (×2).

Deterministic event ID + dedup
- generate_event_id folds honcho_version into the hash so cross-deploy
  events don't silently collide on ID (Fix 6).
- GetContextEvent resource_id uses empty-string sentinel instead of
  "none" so a peer literally named "none" can't collide (Fix 5).

Queue batch cap detection (P2.1)
- hit_batch_token_cap keys on the pre-config-filter SQL boundary so the
  "kept=900 of 1000 cap, next=300 excluded by cap" case reports True
  while still avoiding the config-filter false positive.

Tool result metadata
- search_messages_temporal returns ToolResult with the same search_meta
  shape as search_memory / search_messages (P2.3) — top_k,
  used_embedding, embedding_query_count, query_tokens, results_count.

Tests: stream-setup retry, stream-retry attempt sequence, post-stream
output_tokens write-back, is_final_attempt matrix, truncation E2E,
tool-loop hit_input_token_cap propagation, honcho_version in event id,
GetContextEvent disambiguation, queue_items_cleaned round-trip.

* fix(telemetry): address audit findings across LLM/embed/event paths

Four rounds of telemetry audit findings (initial + 3 follow-ups), grouped
by area:

Retry correctness
- Stream LLM setup now runs inside the awaited honcho_llm_call_inner so
  tenacity's outer retry catches setup failures (Fix 1). The inner
  generator previously deferred execute_stream past the retry wrapper.
- stream_final_response bumps the per-retry attempt index via
  dataclasses.replace so emitted events show [1, 2, 3] instead of
  [1, 1, 1] (Fix 13).
- Embedding _emit_embedding_call takes is_final_attempt; _process_batch
  threads the real retry index (Fix 2).

Token + cost reporting
- HonchoLLMCallResponse.hit_input_token_cap (renamed from
  input_was_truncated) uses a token-based rule so single-message
  over-cap inputs are correctly flagged — the deriver's prompt-only
  path used to silently fly through. Propagated through tool_loop's
  per-iteration check (Fix 4) and into RepresentationCompletedEvent.
- DialecticCompletedEvent gains hit_input_token_cap; output_tokens now
  folds in the final-stream's cumulative usage via
  StreamingResponseWithMetadata.__aiter__ (Fix 7).

Queue batch cap detection
- hit_batch_token_cap previously required total_in_range >= cap, which
  produced false negatives whenever the kept range didn't fully exhaust
  the budget. Replaced with a pre-config-filter SQL boundary check
  (P2.1), then further refined to a queue-item boundary comparison
  (Fix 14) so trailing-context trimming doesn't false-negative either.

Event emission completeness
- AgentToolCallCompletedEvent's was_truncated /
  result_chars_before_truncation now populated by _truncate_tool_output
  via _maybe_truncated_result; 5 handlers migrated (Fix 3).
- DreamSpecialistEvent emits on failure with success=False + new
  error_class field, via try/finally (Fix 11). except BaseException
  catches cancellations too (Fix 16).
- DeletionCompletedEvent emits on failure paths via try/finally
  (Fix 12), and uses ValidationException for unsupported types per
  project guideline (Fix 17).
- CleanupStaleItemsCompletedEvent.queue_items_cleaned populated from
  deleted_count (Fix 8).

Embedding call attribution (Fix 9)
- embedding_call_purpose accepts parent_category.
- 4 new EmbeddingCallPurpose values: DIALECTIC_PREFETCH,
  SESSION_CONTEXT_SEARCH, PREFERENCE_EXTRACTION, GENERIC_DOCUMENT_SEARCH.
- Wrapped previously-unattributed sites: dialectic prefetch, session
  context search, preference extraction, conclusions search, vector
  sync (×2).

Reconciler no longer holds DB session during embedding (Fix 15)
- _sync_documents and _sync_message_embeddings refactored into
  three phases per CLAUDE.md guideline: fetch+detach in a small DB
  scope, external embedding call without DB locks, writes in a fresh
  short-lived DB scope. New _apply_*_sync helpers; orchestrators
  expunge ORM objects before invoking. Vector store upsert + sync_state
  updates stay in the apply phase together.

Deterministic event ID + dedup
- generate_event_id folds honcho_version into the hash so cross-deploy
  events don't silently collide on ID (Fix 6).
- GetContextEvent resource_id uses empty-string sentinel instead of
  "none" so a peer literally named "none" can't collide (Fix 5).

Tool result metadata
- search_messages_temporal returns ToolResult with the same search_meta
  shape as search_memory / search_messages (P2.3).
- Dialectic.prefetched_conclusion_count uses Representation.len() so
  inductive + contradiction observations count too (Fix 10).

* fix(telemetry): orchestrator emit + review feedback

Three more rounds of audit findings + inline PR review, grouped:

Orchestration / emit reliability
- run_dream wrapped in try/finally so DreamRunEvent always emits, even
  on unexpected exceptions including CancelledError (`finally` still
  runs while cancellation propagates). Specialist except clauses
  broadened from SpecialistExecutionError (never raised in src/) to
  Exception so provider/DB/tool failures are recorded with
  deduction_success=False / induction_success=False instead of crashing
  past the emit.
- BaseSpecialist.run() telemetry state initialization + try/finally
  hoisted above the preflight phase (peer lookup, peer-card preload,
  create_tool_executor, get_model_config, prompt construction) so
  preflight failures emit DreamSpecialistEvent(success=False) instead
  of being dropped on the floor.
- Reverted the Round-4 _sync_documents / _sync_message_embeddings
  phase split. The split introduced a race: rows were released from
  FOR UPDATE SKIP LOCKED before the embed call, allowing two workers
  to claim and clobber the same batch. Long-held DB transaction
  restored (pre-existing CLAUDE.md violation accepted as a deliberate
  trade-off; proper fix requires a claim/in_flight migration tracked
  separately).

Schema + naming (PR-internal — none of these have shipped)
- threshold_reason → trigger_reason on DreamRunEvent, DreamPayload, and
  every emit/scheduler/router/test call site (~45 src + 21 test lines).
  Name now accurately reflects the field's role across "manual",
  "surprisal", and "document_threshold" values.
- MessageCreatedEvent reset to schema v1 (was internally bumped to v2
  for last_message_id but never shipped at v1 — downstream sees it
  for the first time at merge).
- DreamSpecialistEvent gains created_counts_by_level /
  deleted_counts_by_level: dict[str, int] keyed on the closed
  level taxonomy. Per-tool-call events use list[str] (≤10 items),
  but specialist runs aggregate 20+ — dict keeps emissions compact.
- QueueBatchResult marked frozen=True.

Per-call embedding attribution
- Agent tool embedding_call_purpose wraps for search_memory,
  search_messages, search_messages_temporal, create_observations now
  driven embedding cost rolls up under the right workflow.
- create_observations() signature gains parent_category kwarg
  (mirrors existing run_id pattern).

Manual dream scheduling
- Manual /schedule_dream route now passes trigger_reason="manual" and
  delay_reason="immediate". Previously both arrived as null in
  DreamRunEvent, breaking analytics joins.

Queue-batch SQL perf
- next_exists_check folded into the main CTE query via
  bool_or(cumulative_token_count > batch_max_tokens) OVER () in a
  nested subquery. Cap detection is now one roundtrip per batch
  instead of two.

Code/doc cleanup
- representation.py docstring uses generic "downstream metering key"
  language (was "Xatu's Stripe meter"). bench runner --base-url help
  uses a generic example host (was "groudon.fly.dev"). Public-facing
  code/docs shouldn't reference internal service names.

Tests added for: orchestrator failure-path DreamRunEvent emission,
specialists preflight try/finally coverage, manual-dream
trigger_reason/delay_reason round-trip, dict-rollup accumulation across
multiple tool calls in a specialist run, CTE-fold one-roundtrip
behavior. Full Python suite passes (1236).

* fix(telemetry): correctness + attribution + emitter robustness

- Dreamer iteration count: read response.iterations directly so
  one-shot runs no longer report iterations=0 and tool-using runs
  include the terminal/synthesis LLM call.
- RepresentationCompletedEvent.observer_count counts successful
  saves, not attempts.
- search_memory empty-memory fallback reports the snippet count when
  message context is returned (was always 0).
- Wire parent_category through every embedding emit path: message
  create (api), save_representation (representation), per-observation
  fallback (caller-supplied), and the peer/session context routes
  (api). get_working_representation accepts parent_category and
  embedding_purpose so the internal fallback embed lands in the same
  analytics bucket as the route-level precompute even when the
  precompute is suppressed.
- BatchItem carries token_count so _process_batch reuses chunk-prep
  counts instead of re-encoding every chunk for the telemetry proxy.
- Drop vestigial EmbeddingCallCompletedEvent.batch_size (always ==
  input_count).
- Emitter: release the lock during HTTP send so a failing endpoint's
  retry+backoff (~36s worst case) doesn't block other flushers;
  edge-trigger the 80%-capacity warning so sustained backpressure
  doesn't flood logs; defer event_id generation past the high-volume
  sampler for events with run_id so sampled-out children don't pay
  the sha256; harden emit() against sync callers with no running
  loop; track threshold-flush tasks so shutdown() drains in-flight
  sends before closing the HTTP client.

* fix(telemetry): tool cancellation emit, nanoid run_ids, version unification

- execute_tool: wrap post-work in finally so AgentToolCallCompletedEvent
  fires on CancelledError; explicit handler sets is_error/result_str
  before re-raising.
- run_id: replace str(uuid.uuid4())[:8] with generate_nanoid() across
  dialectic/dreamer/specialists; matches project-wide nanoid convention.
- Bump _schema_version on events touched by run_id widening:
  DialecticCompletedEvent v1→v2 (also covers hit_input_token_cap field),
  AgentIterationEvent v1→v2, AgentToolConclusionsCreatedEvent v1→v2,
  AgentToolConclusionsDeletedEvent v2→v3, AgentToolPeerCardUpdatedEvent
  v1→v2.
- Unify honcho_version: single HONCHO_VERSION constant in src/_version.py
  read from pyproject.toml (importlib.metadata fallback). Drop
  TELEMETRY.HONCHO_VERSION setting. Use the constant for the FastAPI app
  version (no more hardcoded "3.0.6") and for emitter body injection.
- Delete 17 tautological per-event test_schema_version methods; the
  parametrized contract test still enforces version >= 1 across all events.

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-05-20 18:25:30 -04:00
Xiangzhe b0f0295fd1
fix(docker): gate deriver startup on api healthcheck (#689) 2026-05-19 10:29:08 -04:00
Vineeth Voruganti b8bfe06285
fix: (crewai) update crew ai package and examples for latest protocol (#631)
Co-authored-by: ajspig <dragon@monstercode.com>
2026-05-18 17:37:36 -04:00
Vineeth Voruganti 8fcbb54a49
Align API contract with DB contract for IDs (#684)
* fix: update api schema to support full 512 ids

* fix: update tests and increment docs version
2026-05-14 16:37:39 -04:00
Vineeth Voruganti b84da15d03
Make embeddings configurable (#678)
* feat(embedding): add dimensions_mode for OpenAI dimensions= forwarding

Add EMBEDDING_MODEL_CONFIG__DIMENSIONS_MODE (auto|always|never) controlling
  whether the dimensions= parameter is forwarded on OpenAI embeddings.create
  calls. auto (default) sends it when the operator explicitly set
  EMBEDDING_VECTOR_DIMENSIONS and the configured model is not on the
  known-rejecting allowlist (currently text-embedding-ada-002).

  The provenance check (was VECTOR_DIMENSIONS explicitly set?) lives as
  EmbeddingSettings.resolve_send_dimensions() because it needs access to
  model_fields_set, which the standalone resolver does not have. The
  resolved boolean is passed into _EmbeddingClient at construction time;
  the client never inspects mode or provenance.

  Also pins cloudevents <2.0 — 2.0.0 reorganized the package and dropped
  cloudevents.conversion and cloudevents.http, which src/telemetry/emitter.py
  imports. The original `>=1.12.0` constraint allowed the broken 2.0 resolve.
  With the pin, the imports resolve cleanly and the basedpyright warning
  cascade (37+ warnings about unknown types) disappears.

  Drive-by cleanups (all unnecessary cast/ignore comments flagged by
  basedpyright after the cloudevents downgrade):
  - vector_store/lancedb.py, tests/conftest.py, and
    tests/deriver/test_vector_reconciliation.py — drop dead pyright ignores
  - sdks/python/src/honcho/http/{async_,}client.py — drop unnecessary
    cast(datetime, ...) (parsedate_to_datetime already returns datetime)
  - vector_store/turbopuffer.py — cast(Any, rows) for the upsert_rows
    TypedDict that the SDK exposes but our row builder doesn't satisfy
  - tests/test_datetime_parsing.py — ignore reportArgumentType on the
    test that deliberately passes wrong types to assert raises

* feat(models): honor EMBEDDING_VECTOR_DIMENSIONS in pgvector columns

* feat(startup): atomic swap dim-vs-MIGRATED guard for runtime schema validator

Add src/startup/embedding_validator.py that introspects the actual pgvector
  column dim at boot and refuses to start if it does not match
  EMBEDDING_VECTOR_DIMENSIONS. Runs after the DB pool is up and before the
  embedding client is constructed, in both src/main.py (FastAPI lifespan) and
  src/deriver/__main__.py.

  Implementation details:
  - Schema-qualified pg_attribute join through pg_class/pg_namespace respects
    DB.SCHEMA rather than relying on search_path
  - Bounded retry (3 attempts, 1s backoff) for transient introspection failure,
    then fail-closed with "could not validate embedding schema" — uncertainty
    is not a green light to serve traffic
  - External-store sampler (turbopuffer, lancedb) enumerates workspaces from
    the application DB and probes their lazy-created namespaces; current
    per-namespace probe is a no-op stub since the SDKs do not expose
    uniform dim introspection — full enumeration is left to
    `configure_embeddings --report` in Phase 3

  Atomic guard swap: deletes the old dim-vs-MIGRATED config validator (which
  forbade non-1536 pgvector unless MIGRATED=True) in the same commit as the
  new runtime validator. There is no release window where non-1536 pgvector
  can start unprotected. The 9 dual-write branches that use VECTOR_STORE.MIGRATED
  remain untouched and load-bearing for legacy-tenant backend swaps.

  VECTOR_STORE_DIMENSIONS deprecation: drop the "must match" raise; in
  propagate_namespace, check model_fields_set and emit logger.warning +
  DeprecationWarning (DeprecationWarning alone is filtered by Python's default
  config and would not reach operators). Always overwrite with
  EMBEDDING.VECTOR_DIMENSIONS regardless.

  Test changes:
  - tests/test_models_vector_dim.py: Phase 1's VECTOR_STORE_TYPE=lancedb +
    MIGRATED=true escape hatches removed; the test now passes on plain
    EMBEDDING_VECTOR_DIMENSIONS=768
  - tests/llm/test_model_config.py: the two tests asserting the old guards
    replaced with tests for the new deprecation + acceptance behavior
  - tests/startup/test_embedding_validator.py: 10 new tests — dim assertion
    logic (pass/mismatch/missing/unbounded/non-public-schema), fail-closed
    retry budget, real-test-DB pass, real-DB ALTER-then-validate, deprecation
    warning capture, non-1536 + pgvector + MIGRATED=false at config time

* feat(scripts): add configure_embeddings bootstrap CLI

Adds scripts/configure_embeddings.py alongside the other one-off scripts
  (provision_db, migrate_db, generate_jwt_secret, etc.). Invoked as
  `uv run python scripts/configure_embeddings.py` — same convention as the
  existing scripts in that directory, including the sys.path shim that
  lets src.* imports resolve when run directly.

  Bootstrap step for self-hosted installs at a non-default
  EMBEDDING_VECTOR_DIMENSIONS — runs between `alembic upgrade head` and
  starting the API/deriver.

  pgvector ALTER safety (single transaction):
  - LOCK TABLE {schema}.documents, {schema}.message_embeddings IN ACCESS
    EXCLUSIVE MODE — closes the TOCTOU window between population check
    and ALTER
  - COUNT(*) WHERE embedding IS NOT NULL on both tables; refuse with a
    non-zero exit if either is populated (ALTER ... USING NULL would
    silently wipe those vectors)
  - Snapshot HNSW index DDL from pg_indexes; drop, ALTER, recreate from
    the captured DDL so operator-set HNSW params (m, ef_construction)
    survive the round trip

  External vector stores (turbopuffer, lancedb) are never created or
  modified — namespaces are per-workspace and lazy-created on first write.
  The --report mode enumerates workspaces and collections from the
  application DB, derives the expected namespaces via
  get_vector_namespace(), and prints a per-namespace status table.

  CLI modes (mutually exclusive):
  - (default) interactive: print plan, prompt to confirm
  - --dry-run: print plan and exit 0 without touching the DB
  - --yes: apply without prompt
  - --report: print external-store namespace inventory and exit

  Also updates src/startup/embedding_validator.py error-message paths and
  docs/v3/contributing/configuration.mdx invocations to point at the new
  script location.

  Tests cover plan no-op, plan needs-alter, plan raises on missing column,
  ALTER + HNSW round-trip, refuse-when-populated (monkeypatched count to
  avoid wiring the full workspace/peer/collection/document FK chain just
  to land one vector row), and idempotency.

* docs: add changing-embeddings operations page

Document the supported way to change EMBEDDING_VECTOR_DIMENSIONS or
EMBEDDING_MODEL_CONFIG__MODEL on a Honcho deployment: provision a new
deployment at the desired configuration, replay source data out of
band, cut over at the application layer.

The page explains the asymmetry:
- Dimension is machine-enforced as immutable. The startup validator
  introspects pg_attribute and crashes the API/deriver on mismatch.
- Model is operator-owned. There is no persistent metadata recording
  which model produced each vector, so a same-dim model swap is
  silently undetectable — flagged with a Warning callout.

Also documents the truncation edge case (text-embedding-3-large                                                                                                truncated to 1536 with EMBEDDING_VECTOR_DIMENSIONS left at default)
and the DIMENSIONS_MODE=always mitigation, plus a pointer that
storage-backend swap (VECTOR_STORE_MIGRATED + reconciler) is a                                                                                                 distinct operation unaffected by this work.
Registers the page in docs/docs.json under the Self-Hosting nav group
and cross-links from configuration.mdx.

* fix(embedding): correct turbopuffer regex + tighten DIMENSIONS_MODE docs

- Turbopuffer attribute type for a vector column is `[N]f32` / `[N]f16` /
  `[N]i8`, not `f32_vector(N)` as the earlier probe assumed. The earlier
  regex returned None for the real SDK format, so existing Turbopuffer
  namespaces would have been reported as "missing" instead of validated
  for mismatch. Regex switched to `\[(\d+)\]` which is the
  vendor-stable shape. Test cases rewritten to lock the actual format.

- docs/v3/contributing/configuration.mdx had a contradictory pair of
  bullets: 223 said explicit 1536 makes `auto` forward dimensions=, 224
  said `auto` would skip the parameter because 1536 is the default.
  Operators reading both would (rightly) conclude they need `always`
  even when `auto` would work. Rewrote both bullets so:
  - `auto` is provenance-driven (explicit-set, not non-default-value).
  - `always` is positioned as defense-in-depth for config layers that
    might strip explicit default-valued envs, not the only path for
    same-as-default truncation.

* fix(embedding): address PR #678 review comments

CodeRabbit + Rajat review feedback. All actionable items addressed
except two false-positives (responded on PR).

Bug fixes:
- deriver telemetry leak: validator was called outside try/finally so
  shutdown_telemetry() did not run on validation failure. Moved inside.
- _emit_report printed "no effect with pgvector" unconditionally,
  including from implicit post-apply calls. Added is_report_mode flag;
  only print on explicit --report.
- LanceDB and Turbopuffer probes returned None when the namespace
  existed but its schema was malformed (no vector field / unparseable
  type string), silently bucketing real corruption as "missing"
  (lazy-create) and letting it pass the startup validator. Now raise
  VectorStoreError with actionable diagnostics; None remains valid only
  for "namespace does not exist."
- Startup validator only sampled message namespaces; added a parallel
  Collection-row sample so document namespaces are probed too, with the
  same dim assertion. Mirrors the --report path.

Hygiene:
- StartupValidationError now subclasses HonchoException so existing
  exception handlers recognize it. ValidationException is @final and
  has 422 request-validation semantics that would be misleading here.
- scripts/configure_embeddings.py main() no longer spins up two event
  loops. engine.dispose() moved into a try/finally inside _async_main
  so cleanup runs in the same loop as the pipeline.
- Replaced hand-rolled retry loop with tenacity.AsyncRetrying; same
  fail-closed semantics, less code, before_sleep_log for visibility.
- Added _validate_identifier() defense-in-depth: DB.SCHEMA and HNSW
  index names are regex-checked against [A-Za-z_][A-Za-z0-9_]* before
  SQL interpolation. Operator config + DB catalog are not user input
  under the current threat model, but the constraint is cheap to gate.

Test + docs:
- test_app_settings_accepts_non_1536_with_any_vector_store_configuration
  now actually exercises turbopuffer (was missing); supplies a dummy
  TURBOPUFFER_API_KEY to satisfy the model_validator.
- changing-embeddings.mdx: hyphenated "out-of-band" per reviewer style.

* fix: modify conftest to fix ci

* fix: ci tests for typescript server
2026-05-14 15:03:35 -04:00
ShellyBot b3f371ba51
fix(llm): pass base_url to default provider clients from LLMSettings (#643)
* fix(llm): pass base_url to default provider clients from LLMSettings

Fixes plastic-labs/honcho#641

* fix(registry): use public genai.Client instead of genai.client.Client

---------

Co-authored-by: Hermes Agent <hermes@example.com>
Co-authored-by: ShellyBot <shellybotmoyer@gmail.com>
2026-05-14 14:17:05 -04:00
Marianne a1895e9ecc
Kass/readme refresh (#681)
* docs(readme): repositioning pass + staleness fixes (P0-P4 audit)

Restructure README to match dual audience (AI-tool users + product
developers) per Vineeth's audit. No content deleted - long internal
sections collapsed under `<details>` for scannability.

Staleness fixes:
- Replace 404'd doc links (.../tutorial/SDK, /api-reference/introduction)
  with verified replacements under /v3/documentation/reference/sdk
  and /v3/api-reference/introduction
- Fix Python quickstart to pass api_key (managed default api.honcho.dev
  would 401 otherwise)
- Drop hardcoded `gpt-4` model reference; read OPENAI_MODEL from env
- Replace archived Dialectic blog link with current Chat Endpoint docs
- Drop M3-Macbook-specific note; minor grammar ("deriver's" -> "derivers")
- Replace TL;DR Python-only example with side-by-side Python + TypeScript
  framed around the "Honcho Loop" (store / reason / query / inject)

New sections:
- Start Here: three-path table (AI tools / building product / self-host)
- The Honcho Loop: operation model before code
- What Honcho Gives You: API-at-a-glance table
- Integrations: verified install commands for Claude Code (plugin + raw
  MCP), OpenCode, OpenClaw, Hermes
- Honcho vs RAG: stubbed with TODO; copy deferred to marketing
- SDKs section with clearer Python/TypeScript landing pointers

Restructured:
- Core Concepts moved above Architecture; Collections/Documents reframed
  as internal mechanism (Conclusions is the public surface)
- Storage / Reasoning / Retrieving deep-dive wrapped in <details>
- Local Development, Pre-commit hooks, Fly deployment, full config
  matrix wrapped in <details>

Known follow-up (not in this branch): SDK docs at docs.honcho.dev and
PyPI PKG-INFO advertise `HONCHO_BASE_URL`, but the actual SDK code
(sdks/python/src/honcho/client.py:234, sdks/typescript/src/client.ts:154)
reads `HONCHO_URL`. README aligned with code; docs + PKG-INFO need
separate fix.

* docs(readme): restore "stateful agents" in opening sentence

Plastic Labs' canonical positioning uses "stateful agents" across
materials, and the original README opened with "for building stateful
agents." The repositioning pass in d6d60435 dropped the term entirely
(now zero occurrences) by following Vineeth's suggested opening copy
verbatim - but his audit's executive summary explicitly praised the
"stateful agents" positioning and didn't ask to remove it. Restoring
it in the bolded thesis sentence.

* docs(readme): drop self-referential "observations" in Conclusions bullet

The Conclusions definition shouldn't define itself in terms of
"observations." Per Plastic's positioning, "conclusions" is the
documentation-facing name for what the Deriver produces;
"observations" remains the internal code symbol. The README's
two remaining "observations" references (inside the <details>
Internal storage block and the Storage primitives block) are
explicit code-internal framing and stay.

* docs(readme): restore content dropped without audit instruction

Self-audit against Vineeth's audit found seven items I'd dropped that weren't in the audit's instructions to drop: outcome-marketing line, Contents TOC (audit said rename, not remove), multi-repo prose, org-onboarding detail, peer-paradigm feature bullets, Architecture "Key Features" bullets, and Learn More pointers. Also fixes two residual "Dialectic API" → "Chat Endpoint" mentions the original P0 sweep missed.

* docs(readme): add "Why Honcho" capability table + agent-skill onboarding

Closes the two gaps flagged in the freshness/repositioning audit: adds Vineeth's recommended "Why Honcho" capability table between Start Here and The Honcho Loop, and adds the `npx skills add plastic-labs/honcho` + `/honcho-integration` agent-skill path as a subsection of Integrations (verified against current docs).

* docs: split contributor-only sections out of README; trust auth for local postgres

- Move pre-commit hooks setup from README to CONTRIBUTING.md (pure
  contributor content; the README still links to it).
- Move Fly.io deployment notes from README to the self-hosting docs.
- Wrap remaining <details>/<summary> blocks with markdownlint
  disable/enable to clear pre-existing MD033/MD001 failures.
- Add POSTGRES_HOST_AUTH_METHOD=trust to the example compose template
  with an inline warning, so host-side tests and tooling can connect
  without supplying a password.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: (docs) update docs and evals urls and split pre-commit into contributing docs

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 13:15:37 -04:00
Rajat Ahuja 092b60520f
feat: stop fetching embedding vectors on vector store query - DEV-1727 (#682)
* feat: stop fetching embedding vectors on vector store query

* fix: add similar filtering for lancedb

* fix: add lancedb tests

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-05-14 13:14:15 -04:00
Marianne 7554c96d6f
Kass/claudemd staleness (#680)
* docs: refresh CLAUDE.md to match current architecture

Bring the project CLAUDE.md in line with the codebase as of main:

- API prefix /v1 -> /v3; add Conclusions + Webhooks to route listing;
  note that Collections/Documents are now partially exposed via the
  Conclusions API
- New Runtime Architecture section describing the API server / deriver
  worker split and the in-process Reconciler scheduler
- Rewrite Agent Architecture:
  * Deriver - "minimal" single-LLM-call architecture (no agentic tool
    loop); current entry point and prompts
  * Dialectic - actual DIALECTIC_TOOLS list, 5 reasoning tiers,
    DIALECTIC_TOOLS_MINIMAL for the minimal level
  * Dreamer - orchestrator + DeductionSpecialist + InductionSpecialist,
    surprisal-based prioritization, reasoning trees
  * Summarizer documented as a distinct agent
- Refresh project structure tree: add cache/, llm/ (+backends/),
  reconciler/, telemetry/, vector_store/; correct schemas/ (now a
  directory); drop nonexistent dialectic/agent/, deriver/agent/,
  dreamer/agent.py + dreamer.py
- Architectural decisions: fill in the missing #2 (Peer Paradigm); add
  hybrid search (FTS + vector), pluggable external vector stores,
  composite-FK multi-tenancy, dialectic reasoning tiers

* docs(CLAUDE.md): rename "observations" -> "conclusions" in agent prose

Per Plastic's positioning, "conclusions" is the documentation-facing
term for what the Deriver produces; "observations" remains the
internal code-symbol vocabulary (`create_observations`,
`get_observation_context`, etc.). Swap conceptual prose, preserve
all backticked code references.

- New terminology callout at the top of Agent Architecture so the
  mapping is explicit for coding agents reading this file
- Deriver/Dreamer prose: observations -> conclusions for the abstract
  noun; specialist tool lists keep their `get_recent_observations`,
  `create_observations_deductive`, etc. unchanged
- Reasoning-trees bullet rephrased to "each conclusion links to its
  premises and downstream conclusions" (the original "observations
  link to premises/conclusions" becomes recursive after the swap;
  rephrasing makes intent clearer)
- Tree comment for surprisal.py updated to prose-style "conclusion
  prioritization"

Companion to a3fa16ef on kass/readme-refresh, which made the same
swap in the README's Conclusions definition.

* docs(CLAUDE.md): align "Dialectic API" with README's "Chat Endpoint" rename

Two spots framed the public surface as "Dialectic API" — preserve the code-agent name (Dialectic) while matching the documentation-facing "Chat Endpoint" we standardized on in the README.

* chore: nits in CLAUDE.md

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-05-14 12:18:43 -04:00
adavyas a420264152
feat: deriver custom instructions (#609)
* feat: wire deriver custom instructions on main

* refactor: simplify custom instruction normalization

* chore: lower deriver custom instruction cap

* chore: raise deriver custom instruction budgets

* docs: update deriver input token example

* fix: hide deriver config guidance from validation

* chore: address custom instruction review nits

* docs: document deriver custom instruction cap

* fix: remove unused tests/validation and simplify enable flag for custom instructions

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-05-11 18:05:42 -04:00
Rajat Ahuja 5de8a3b81a
fix: use model-aware tokenizer and skip empty messages - DEV-1238 (#647)
* fix: use model-aware tokenizer and skip empty messages

* fix: change default model

* fix: types

* fix: guard on empty msg content
2026-05-11 17:22:50 -04:00
Rajat Ahuja 1478cbf1d5
fix: levels merging in src/config - DEV-1733 (#656)
* fix: src/config for dialectic level defaults

* fix: add test

* fix: test
2026-05-11 17:05:42 -04:00
Rajat Ahuja a4ae372932
fix: internal N+1 query in dialectic agent calls - DEV-1721 (#652)
* fix: internal N+1 query in dialectic agent calls

* fix: comments
2026-05-06 12:04:29 -04:00
Marianne ad7c1b3040
Merge pull request #650 from plastic-labs/kass/fix-unified-runner-mutex-args
fix(tests/unified): use argparse mutex group for --test-dir/--test-file
2026-05-05 16:03:35 -04:00
thrialectics 5eafd67c33 fix(tests/unified): use argparse mutex group for --test-dir/--test-file
The previous mutual-exclusion check compared --test-dir against its
default string literal, so passing --test-file together with an
explicit --test-dir tests/unified/test_cases silently bypassed the
check. Replace with argparse.add_mutually_exclusive_group() and apply
the default path post-parse so the bare invocation still works.
2026-05-05 12:31:23 -04:00
ajspig c165c51fae
docs: add skill install section to Vercel AI SDK guide (#649)
Add "Use the Skill" section recommending `npx skills add plastic-labs/vercel-ai-sdk`
with the manual symlink approach as a collapsed alternative.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-05 11:26:33 -04:00
Lily e38085177c
docs(integrations): rewrite Vercel AI SDK guide as cookbook style (DEV-1485) (#635)
* docs(integrations): add @honcho-ai/vercel-ai-sdk guide

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs(integrations): rewrite Vercel AI SDK guide as cookbook style (DEV-1485)

Reshapes the guide to cookbook formula, adds Full Script section, fixes
maxSteps → stopWhen for ai-sdk v5, renames package, and prunes stale notes.
See PR for full decision log.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs(integrations): lead Vercel AI SDK verification with direct-inspection check

- Restructure Verifying section: direct inspection (token delta + dashboard) is now step 1 so readers isolate Honcho's contribution before grading model behavior
- Behavioral tests (first turn, multi-turn, cross-session, tool calling) follow as steps 2-5
- Note `result.toolCalls` as the way to confirm which Honcho tool fired (tool names don't appear in `result.text`)
- Signpost the Full Script from Complete Example so the two snippets read as a staircase, not a duplicate

Addresses review comments on PR #635.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(tests): satisfy basedpyright in test_representation_manager

The save-representation tests added in #615 were structurally correct but
failed strict typing in two places. Static Analysis has been red on main
since the merge.

- `mock_save.await_args` is `_Call | None`; assert it's not None before
  reading `.kwargs` / `.args` so basedpyright can narrow the type
- `SimpleNamespace(...)` passed as `message_level_configuration` is an
  intentional duck-typed mock (only `.dream.enabled` is read by
  `save_representation`), so opt out at the call site with
  `# pyright: ignore[reportArgumentType]` rather than constructing a
  full `ResolvedConfiguration` (matches the existing `reportPrivateUsage`
  ignore pattern in this file)

No runtime behavior changes; `uv run basedpyright` is now clean
project-wide.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(tests): pad timestamp windows in test_messages for clock skew

Three timestamp tests captured `before_request` / `after_request` with
`datetime.now(UTC)` on the host and asserted the server's `created_at`
fell within. Under Docker, the Postgres container's clock can skew tens
of ms from the macOS host, flipping the assertion intermittently under
parallel pytest load.

Pad each window by 1 second on both sides — wide enough to absorb
realistic skew, narrow enough that the test still proves the timestamp
is server-current.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(integrations): tighten Verifying section after end-to-end smoke

Smoke-tested all five verification steps against a fresh Sonnet 4.6 + Honcho integration. Three findings, all reflected here:

- Cross-session recall (#4): added Note about DERIVER_REPRESENTATION_BATCH_MAX_TOKENS=1024 — short warmups don't accumulate enough content to flush observations, so cross-session recall returns empty even on a working integration.
- Tool calling prompt (#5): replaced the honcho_chat patterns prompt with a verbatim-retrieval honcho_search prompt. Sonnet skips honcho_chat when middleware-injected context already answers; verbatim retrieval forces a fire.
- Tool inspection (#5): replaced result.toolCalls reference with result.steps[i].toolCalls + flatMap snippet. Top-level toolCalls is empty in multi-step calls (stopWhen: stepCountIs(N)) — the fires are nested inside steps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(integrations): make Step 4 cross-session test durable via honcho_search

Replace the prose-recall test ("Based on what we've talked about, what do you know about me?") with a forced honcho_search call. Prose recall depended on the model getting deriver-built representation/peer-card in its system prompt, which is gated behind DERIVER_REPRESENTATION_BATCH_MAX_TOKENS=1024 — short tutorial-length conversations don't trigger it, producing false negatives on a working integration.

honcho_search hits message embeddings, which are computed synchronously at message persist time (src/crud/message.py:262-276), so peer-scoped retrieval works regardless of how short the prior session was. Also folds the result.steps[i].toolCalls inspection snippet from the old Step 5 into Step 4 — same prompt, no need for two sections.

Drops Step 5 entirely.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 10:41:18 -04:00
Lily f37338b855
fix(dreamer): threshold and time-guard semantics (#573)
* fix(dreamer): threshold and time-guard semantics

Finding 2: filter count_stmt on documents.level == 'explicit' in
check_and_schedule_dream. Dreamer-created levels (deductive, inductive,
contradiction) are consolidation output, not input, and would otherwise
inflate the threshold count and create a feedback loop.

Finding 3 (code-level): relocate last_dream_at write from enqueue_dream
(enqueue.py) to process_dream (orchestrator.py), inside the
'if result is not None' block. Duplicate enqueues can no longer reset
the 8-hour time guard clock. Failed/never-run dreams don't advance it.

Success criteria: lenient (any non-null DreamResult counts). Pending
Vineeth confirmation — will adjust to strict/middle if requested.

Tests pending in follow-up commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(dreamer): threshold filter + last_dream_at relocation regression tests

Tests for Finding 2 and Finding 3 (code-level):

- TestThresholdFilter (tests/dreamer/test_dream_scheduler.py):
  * Mixed levels below explicit threshold: 30 explicit + 40 deductive
    + 10 inductive → no trigger (core regression, buggy count would trigger)
  * Explicit-only at threshold: 60 explicit → triggers
  * Contradiction excluded: 100 contradiction + 10 explicit → no trigger
    (confirms positive == "explicit" filter excludes all dreamer output)

- TestEnqueueDreamMetadataShape (tests/deriver/test_enqueue_dream.py):
  * AsyncMock-patched update_collection_internal_metadata verifies
    enqueue writes last_dream_document_count but NOT last_dream_at

- TestLastDreamAtCompletionWrite (tests/dreamer/test_dreamer_integration.py):
  * Happy path: run_dream returns DreamResult → last_dream_at written
  * Failure path: run_dream returns None → last_dream_at absent
  * Exception path: run_dream raises → last_dream_at absent,
    process_dream swallows exception (queue-processed semantics preserved)

Docstring on check_and_schedule_dream tightened: "document threshold"
-> "explicit-observation threshold" to reflect filter semantics.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(dreamer): preserve last_dream_document_count in completion write

CodeRabbit caught this: update_collection_internal_metadata uses a
top-level JSONB `||` merge, so passing {"dream": {"last_dream_at": ...}}
replaces the entire "dream" subkey and drops last_dream_document_count
that was written by enqueue_dream.

Symptom: after every completed dream, the baseline drops to 0. Next
check_and_schedule_dream reads documents_since_last_dream as
current_count - 0 = current_count, so any collection with >= 50
explicit observations can re-trigger immediately once the 8h guard
expires, even with no new raw material.

Fix: read-modify-write. Fetch current collection, merge last_dream_at
into the existing "dream" dict, write the merged dict back. Preserves
sibling keys (current: last_dream_document_count; future-proof for
telemetry fields that might land in PR 4).

Regression test added to tests/dreamer/test_dreamer_integration.py:
pre-seeds {"dream": {"last_dream_document_count": 42}}, runs
process_dream, asserts both last_dream_at is written AND
last_dream_document_count == 42 is preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(dreamer): address CodeRabbit feedback on b89997c

- enqueue.py: read-modify-write preserves last_dream_at when writing baseline
- dream_scheduler.py: explicit-level filter on execute_dream count query
- test fixture: pin DOCUMENT_THRESHOLD and ENABLED_TYPES for stability
- integration test: timezone-aware assertion on last_dream_at

Regression test added for enqueue sibling-drop (symmetric to c8fe40a).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(dreamer): session lookup symmetry + row lock on dream metadata RMW

- dream_scheduler.py: explicit-level filter on execute_dream session lookup
  (baseline and session pick must agree on the same document set)
- crud.collection.get_collection: optional with_for_update flag for callers
  that need serialized read-modify-write on internal_metadata
- enqueue.py + orchestrator.py: pass with_for_update=True on the RMW reads
  to close the TOCTOU between concurrent enqueue and completion writes

Follow-up filed for jsonb_set-based nested updates (docs/factory/backlog/).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(dreamer): explicit-only count on manual schedule_dream route

The third caller of enqueue_dream — POST /workspaces/{id}/schedule_dream —
was passing an all-levels document count as the baseline, breaking symmetry
with check_and_schedule_dream and execute_dream after Loop 2's filter fixes.
Filter the manual route's count to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(dreamer): document explicit-only invariant on enqueue_dream.document_count

Loop 3 follow-up on d76627a. The parameter's semantic tightened across Loop
2 (check_and_schedule_dream, execute_dream) and Loop 3 (schedule_dream route)
to "explicit-level count, used as the baseline," but the signature still read
"Current document count for metadata update." The next caller would have no
way to know from the function contract.

Docstring now spells out: (1) the value is explicit-only, (2) it's written
as last_dream_document_count, (3) it's the baseline that
check_and_schedule_dream subtracts from to compute
documents_since_last_dream, (4) passing a count that includes non-explicit
levels (deductive, inductive, contradiction) inflates the baseline and
suppresses the next scheduled dream.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(dreamer): rename current_document_count → current_explicit_count

Loop 3 follow-up on d4e10e3. After Loop 2's filter landed, the local in
check_and_schedule_dream held an explicit-only count but was still named
current_document_count — asymmetric with execute_dream's current_explicit_count
(line 201) and contradicting the filter on line 269 that produces the value.

Pure rename: three occurrences (definition at 271, subtraction at 274, log
extra key at 282). No test references. Naming-as-invariant alignment with
d76627a (query filters), d4e10e3 (parameter docstring), and Loop 1's local
rename in execute_dream.

The persisted JSONB key last_dream_document_count is the one remaining
drift-layer; filed as plastic-claudebook backlog item for a separate PR
with an intentional migration path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(dreamer): atomic guard-pair write + in-flight stampede defense

Loop 4 response to Vineeth's CHANGES_REQUESTED on PR #573.

The pre-Loop-4 enqueue-time write of last_dream_document_count was serving
double duty: rate limiter AND stampede latch. By arming the 8h guard the
moment a dream entered the pipeline, it implicitly blocked a second dream
from being scheduled during the in-flight window. Loop 3 relocated the
last_dream_at write to completion without moving its sibling baseline,
splitting the semantic pair and exposing the latch role that had lived
only in Vineeth's head.

Invariant (now pinned to check_and_schedule_dream's docstring): from the
moment a dream is scheduled until it completes or fails, no second dream
may be enqueued for the same (workspace, observer, observed) — and the
baseline count advances only when consolidation actually happened.

Changes:
- enqueue_dream: remove the last_dream_document_count write entirely and
  drop the document_count parameter. enqueue no longer touches dream
  metadata; the implicit stampede latch is replaced by an explicit
  queue-backed defense.
- process_dream: extend the existing row-locked RMW to write both guard
  fields atomically. Current explicit-doc count is recomputed inside the
  locked block (not carried on DreamPayload) so the pair reflects the
  actual consolidation moment.
- check_and_schedule_dream: query QueueItem for pending dreams on this
  collection's work_unit_keys (mirrors uq_queue_dream_pending_work_unit_key)
  before arming a timer. Uses queue state as source of truth rather than
  reflecting it into metadata.
- Tests: two new coherence tests under TestGuardPairCoherence —
  test_pending_queue_item_blocks_second_schedule walks the stampede timeline,
  test_silent_failure_allows_retry_on_same_corpus verifies failed dreams
  don't consume the baseline. Existing tests updated to the new contract.

* chore(dreamer): trim comment slop from loop-4 atomic pair work

Compress three verbose comments added in d24958d — the invariant itself
is captured in check_and_schedule_dream's docstring, so the inline
narrative restates what the code already says.

- dream_scheduler.py defense C block: 5 lines → 2
- orchestrator.py atomic pair write: 4 lines → 1
- enqueue.py docstring paragraph: 5 lines → 2

Net: +5/-14. Follows Eri's eef27be precedent on sillytavern-honcho PR #7.

---------

Co-authored-by: lilyplasticlabs <lily@plasticlabs.ai>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 11:40:51 -04:00
Phil a05c2f8ec1
fix(deriver): ignore blank observations before embedding (#615)
* fix(deriver): ignore blank observations before embedding

* Address PR review on observation normalization

* Harden mock await arg access in tests

* Unify blank observation filtering across tool paths

* Move soft-delete query test back to fixture class
2026-04-29 15:18:00 -04:00
banteg 94ade07c12
fix(config): use auto tool choice for dialectic defaults (#630) 2026-04-29 13:19:44 -04:00
Rajat Ahuja 03a2374ea1
fix: give vector sync a substantial retry budget (#604) 2026-04-28 16:01:33 -04:00
Rajat Ahuja b778d82319
fix: add levels to AgentToolConclusionsDeletedEvent (#612) 2026-04-28 15:15:18 -04:00
adavyas 8a95edb79b
docs: update opencode install command (#623)
* docs: update opencode install command

* docs: use native opencode plugin install
2026-04-28 15:05:41 -04:00
Lily e659b6b31f
Merge pull request #433 from plastic-labs/eri/dev-1430
docs: add SillyTavern to integrations
2026-04-24 15:22:01 -04:00
Erosika 9d68149ded docs(sillytavern): correct panel labels, split installer per-platform, surface other knobs 2026-04-24 13:57:13 -04:00
adavyas a3e8000778
docs: add Windows opencode install instructions (#611) 2026-04-24 11:53:02 -04:00
adavyas 07e7a99f3c
docs: add opencode docs (#606)
* docs: adding opencode

* docs: align opencode guide with latest plugin changes

* chore: updating language

* docs: remove interview command from opencode guide

---------

Co-authored-by: ajspig <dragon@monstercode.com>
2026-04-23 16:11:06 -04:00
Rajat Ahuja f351db6055
fix: rm stop sequence from tests (#607) 2026-04-23 16:09:22 -04:00
Erosika 28dcb136ab docs(sillytavern): unify peer modes and session naming, move group chats last, drop event flow 2026-04-23 15:56:21 -04:00
Sanjay Santhanam 5f9cb3f3c1
fix(surprisal): use correct filter format for level observations (#581)
The Surprisal module passes `{"level": levels}` directly to
`get_all_documents()`, but `apply_filter()` expects operator syntax:
`{"level": {"in": levels}}`.

Without the `in` operator, the filter is silently ignored, causing
`_fetch_level_observations()` to return 0 results. This makes the
entire Surprisal phase of the Dream cycle a no-op.

Fixes #559
2026-04-23 15:55:16 -04:00
Eri Barrett 9e0f24f387
Merge branch 'main' into eri/dev-1430 2026-04-23 15:53:02 -04:00
Erosika b81762f501 docs(sillytavern): group chat + session behavior, add missing tool
- New Group Chats subsection: documents per-character peer routing
  (each group member gets their own peer, not a collapsed group-<id>
  peer) and lazy peer registration for characters joining mid-chat.
- Session Naming: documents the freeze-on-first-assign invariant
  (changing the naming mode doesn't reroute existing chats) and
  the Reset button for explicit session rollover.
- Tool table: add honcho_save_conclusion — prior fix undercounted
  (2 -> 3 tools). The extension registers all three.
2026-04-23 14:26:59 -04:00
Erosika 6d26666df2 docs: drop architecture ASCII from sillytavern guide 2026-04-23 13:33:59 -04:00
ajspig b389627194
docs: adding opencode (#596)
* docs: adding opencode

* docs: align opencode guide with latest plugin changes

* chore: updating language

---------

Co-authored-by: adavyas <adavyasharma@gmail.com>
2026-04-23 10:28:12 -07:00
Erosika 3d37b343cb docs(sillytavern): fix tool count (2, not 3)
honcho_save_observation is not registered in the extension — only
honcho_query_memory and honcho_search_history exist in code.
2026-04-23 13:02:25 -04:00
Erosika ee7ef1f167 docs(sillytavern): move Global Config after How It Works 2026-04-23 12:55:11 -04:00
Erosika f30eb1b442 docs(sillytavern): drop internal sessions-map detail 2026-04-23 12:52:04 -04:00
Erosika d7fdf6d48a docs(sillytavern): clarify write scope
The plugin also writes to a root-level `sessions` map (ST dir → last
Honcho session ID), not only to `hosts.sillytavern.*`. The earlier
phrasing overstated the isolation claim.
2026-04-23 12:50:16 -04:00
Erosika d8d625f470 docs(sillytavern): update for PR#10 surface + review fixes
- Add Prerequisites section with SillyTavern install link + Node >= 18
  requirement (was buried in Next Steps; users hit install step with no
  awareness ST needed to exist first).

- Expand restart step into a callout: restart required for server-plugin
  reload, not for client-side edits.

- Configure step now documents the three editable inputs (API key,
  Workspace ID, Your peer name) and where each saves.

- Fix 'three-cubes icon' -> 'puzzle piece icon'.

- Installer step list fleshed out: 6 steps (was 4), including config.yaml
  bootstrap and enableServerPlugins flip. Dropped the false claim that
  the plugin seeds a minimal ~/.honcho/config.json on first run.

- Global Config section rewritten: resolution order now generalized to
  apiKey / workspace / peerName (was apiKey-only); documents panel
  write-back to hosts.sillytavern.*; dropped aiPeer references (it's a
  telemetry-only field, not user-facing).

- Add a Disable / Enable global config subsection covering the opt-out
  toggle and the Inherit / Push local / Cancel diff dialog.

- Troubleshooting: two new rows (stale peer name on new chat, cancelled
  diff dialog).
2026-04-23 12:43:49 -04:00
qxxaa 7d1ce9c1f4
fix: remove hardcoded stop_sequences override from Deriver model config (#587)
* Update deriver.py

* Simplify model configuration in deriver.py

Removed stop_sequences from model configuration.
2026-04-23 12:22:16 -04:00
Erosika 2ffe30bd4f docs(sillytavern): post-review polish pass (DEV-1430)
- Clarify installer step 4 — the plugin seeds config.json if absent
- 'Puzzle piece' -> 'three-cubes' for the Extensions icon (current ST UI)
- API key step notes the UI-overrides-config precedence explicitly
- 'Honcho workspace ID' -> 'default Honcho workspace ID (configurable)'
- Add Note after Context-modes table — Context only is session-scoped
  and returns empty until enough messages accumulate; Reasoning is the
  better default for fresh peers
- Next Steps gains two cards: Install SillyTavern (upstream docs) and
  the Claude Code setup skill (skills/setup/SKILL.md)

Follow-ups tracked separately — tool rename (observation -> conclusion,
matching the /conclusion endpoint), architecture Excalidraw.
2026-04-21 18:17:17 -04:00
lilyplasticlabs 1e7a3461e5 docs(sillytavern): apply DEV-1482 review findings (DEV-1430)
Applies eight review findings from the DEV-1482 integration review. All
scoped to docs/v3/guides/integrations/sillytavern.mdx; no code changes.

- DOC-3: curl -fsSL in install command (fails loud on 4xx/5xx)
- DOC-4: Note now reflects installer auto-config + manual-fallback
- DOC-6: LLM-backend prerequisite callout at top of Quick Start
- DOC-14: restart step warns about live-session clobbering
- DOC-5: Global Config intro names resolution order + precedence;
  disambiguates "sillytavern" workspace vs hosts.sillytavern key
- DOC-7: new Peer Observability subsection (asymmetric default)
- DOC-2: route count in Architecture diagram 7 → 9
- DOC-8: troubleshooting row for "plugin on disk, drawer absent"

Findings index + rationale: plastic-labs/sillytavern-honcho#3
2026-04-21 14:56:06 -04:00
ajspig ae05ab5bc8
fix: moving cli skill (#591)
* fix: moving cli skills to root

* chore: updating cli readme

* chore: updating language

* chore: updating docs
2026-04-21 12:41:16 -04:00
ajspig ca1dc858ec
cli docs (#589)
* docs: adding cli doc

* docs: adding generated script and content and github workflow

* chore: removing workflow

* fix: (docs) re-format and add details to cli-reference docs

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-04-20 23:25:12 -04:00
Rajat Ahuja 7fae16b351
handle turbopuffer server errors (#561)
* fix: catch InternalServerError from turbopuffer

* fix: remove unused VectorUpsertResult

* fix: downgrade vector store sync errors to warnings

* fix: remove upsert_with_retry

* fix: (vector) add silent path and explicit path for vector db server errors

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-04-20 16:56:51 -04:00
qxxaa 1c3e3f8816
fix: embed() sends string input instead of array, breaking OpenAI-compatible providers (#586)
* fix: wrap single embed() input in array for OpenAI-compatible provider compatibility

* Fix input format in embedding test assertion
2026-04-20 16:35:13 -04:00
Rajat Ahuja 2c50791642
fix: add namespace, model, and provider to langfuse metadata so we can filter (#565) 2026-04-20 16:30:35 -04:00
Alfredo Colangelo 51497f174f
feat(mcp): read HONCHO_API_URL env var to support self-hosted Honcho (#575)
The MCP Worker hardcoded https://api.honcho.dev for every request, forcing
anyone running a self-hosted Honcho instance to patch the source before
deploying their own Worker alongside it.

Route the baseUrl through the Worker env so operators can set
HONCHO_API_URL (via .dev.vars for local development or wrangler secret for
deployed Workers) and point the Worker at their instance. The variable is
intentionally not exposed as a request header: that would let public
clients steer traffic to internal URLs, which is a latency and security
regression.

When HONCHO_API_URL is unset, the Worker falls back to
https://api.honcho.dev, so existing deployments are unaffected.

Closes #508
2026-04-20 16:13:16 -04:00
ajspig 3dbf0e66fc
feat: adding honcho-cli package (#424)
* feat: adding honcho-cli package

* feat: adding more support for command-level flags, also including workarounds for getting raw SDK info

* feat: adding peer config

* feat: adding setup commands

* chore: setting up package dependencies for cli

* feat: promote init/doctor to top-level + polish wizard

* feat: make init --yes fall back to existing config

* chore: updating documentation

* chore: updating tagline

* feat: structurally updating recomended settings for CLI

* fix: style

* fix: removing redundant describe method

* fix: delete key generation commands and fixing session ID

* fix: removing defaults and changing config write path.

* chore: pagnating conclusions

* chore: require workspace

* fix: polish command surfaces — scoping, validation, perf, consistency

* chore: removing session message

* fix: CLI output shape, destructive-confirm previews, skip needless round-trips

* chore: CLI polish — peer inspect config, drop dead helper, doc/help consistency

* chore: update readme

* chore: updating tests

* chore: doc updates

* fix: config command

* chore: unused code

* fix: doctor command

* fix: removing quiet tag and fixing session key ordering

* fix: config commands and session id command

* fix: removing message_count

* fix: branding circular dependency

* fix: refactor lazy imports to use common.py correctly.

* fix: removing all lazy imports

* chore: cr fixes

* fix: config, env, flag setup

* chore: updating skill

* feat: adding workspace, session, and message create

* fix: init now supports local honcho

* chore: cr

* feat(cli): CLI surface polish — reasoning flag, peer-scoped messages, help sync

Add --reasoning/-r to peer chat (minimal..max), -p peer filter to
message list with newest-first ordering, and a curated welcome panel
with getting-started/memory/commands sections.

Sync the welcome panel and group help strings with the actual
registered commands — drop phantom 'session clone', add the 4 missing
peer commands and 7 missing session commands, fix conclusion/message/
workspace group docstrings that claimed commands that don't exist.

* feat(cli): themed, unified help system with pattern/example

Replace the hand-rolled welcome with a layered system:

- Theme typer.rich_utils (dim borders, brand color) so every --help
  inherits the voice.
- HonchoTyperGroup subclass renders a curated 3-panel welcome
  (getting started / memory / commands) with recipes Typer can't
  auto-generate.
- Unify the front door: bare 'honcho', 'honcho --help', and
  'honcho help' all render the same welcome via one code path;
  sub-groups and leaf commands still get Typer's themed renderer.
- Replace Click's 'Usage: …' line with pattern/example rows at every
  sub-group and leaf command, so the help voice stays consistent from
  top to leaves.

* refactor(cli): address review — typed exceptions, chmod 600, tighter redaction, class-based help, tests

- Replace module-level monkey-patch of TyperGroup/TyperCommand.get_usage
  with HonchoTyperGroup applied via cls= on every sub-Typer. Lives in
  a new _help.py module to avoid circular imports. No longer leaks
  behavior changes into other Typer users in the same process.
- _test_connection dispatches on the SDK's typed exceptions
  (AuthenticationError, ConnectionError, TimeoutError, APIError)
  instead of substring-matching error messages.
- Config.save() now chmods ~/.honcho/config.json to 0o600 after write
  so the plaintext API key isn't world-readable on multi-user hosts.
- Tighten api_key redaction to '***<last4>' (was 'header...last4'),
  matching setup._redact for consistency. Short keys fully masked.
- Add test_validation.py covering safe IDs, unsafe chars, path
  traversal, and empty input. Update test_config.py redaction cases
  and add 0o600 permission assertion. Fix stale patch paths in
  test_commands.py that pointed at honcho_cli.main instead of the
  command modules where get_client is actually imported.

* feat(cli): add options panel to welcome menu

Append a fourth panel listing the global flags (-w/-p/-s, --json,
--version, --help) with their env-var counterparts. Discoverable
from bare 'honcho' without needing to hunt for --help.

* chore(cli): drop --version from welcome options panel

* feat(cli): add pixel-honcho icon to banner

Prepend a 13-char ASCII rendering of honcho-pixel.svg to the HONCHO
wordmark. Uses Unicode half-blocks to pack 12 pixel rows into 6 text
rows, faithfully preserving the SVG outline (two eye dots, mouth slit,
tapering foot). Appears in bare 'honcho', 'honcho --help', 'honcho
--version', and 'honcho init'.

* fix: polish Honcho CLI wolcome panel and error messages

* fix: honcho workspace inspect speed

* chore: minor fix to session pagination

* fix: removing NDJSON output

* chore: consolidating honcho CLI's dula argv grammar onto Pattern A (command-first)

* chore: clean up imports

* fix: four `-s` consistency fixes applied

* chore: minor changes to memory rows

* fix: changing package name to honcho-cli

* fix: removing pixel face

---------

Co-authored-by: Erosika <eri@plasticlabs.ai>
2026-04-20 13:27:35 -04:00
Vineeth Voruganti b65d03d297
Refactor clients.py to add modern features and more flexible configuration (#459)
* fix: Add JSON repair for truncated LLM responses across all providers and Gemini thinking budget support

LengthFinishReasonError from OpenAI-compatible providers (custom, openai, groq) was crashing the deriver
with 14k+ occurrences in production. The vLLM path already had repair logic but it was gated on
provider=="vllm", unreachable when routing through litellm as a custom provider.

- Extract shared _repair_response_model_json() helper for all providers
- Catch LengthFinishReasonError in OpenAI/custom parse() path and repair truncated JSON
- Add repair fallback to Anthropic and Gemini response_model paths
- Add repair fallback to Groq response_model path
- Pass thinking_budget_tokens to Gemini 2.5 models via thinking_config
- Add 14 tests covering repair paths for all providers and Gemini thinking budget

Fixes HONCHO-YC

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: live llm integration tests

* feat: Consistent Model Config Protocol

* fix: migrate the remaining app callers off the legacy llm_settings path

* fix: Docs and regression tests

* fix: refactor llm runtime path to model-config-only API

* fix: refactor config to nested model-config source of truth

* fix: refactor llm streaming and tool dispatch through backends

* fix: cut over llm config to nested model_config only

* fix: collapse vllm and custom into openai_compatible transport

* feat: refactor llm config to explicit transports and bare model ids

* feat: (embed) Add configurability for embedding model

* fix: tests for embedding provider

* fix: Address Review Comments

* fix: (llm) remove Groq backend and per-vendor base URLs

* chore: move llm tests

* fix: (llm) address review findings — config regressions, backend bugs, dead code

* fix: address backend end silly errors

* chore: (docs) update configuration and self-hosting guides

* chore: fix tests

* fix: address code rabbit comments

* fix: add validation to the dream settings

* fix: further address code rabbit comments

* fix: Address Code Rabbit Comments

* fix: Another round of code rabbit

* fix: Address Code Rabbit Nits

* fix: tests

* refactor: rename thinking validator to reflect transport scope

_validate_anthropic_thinking_minimum only enforces the >=1024 rule for
Anthropic and no-ops for other transports, so the name was misleading
now that it's shared across ConfiguredModelSettings, FallbackModelSettings,
and ModelConfig. Renamed to _validate_thinking_constraints with a docstring
clarifying per-transport behavior. No logic change.

* fix(config): drop transport-specific thinking params when env override changes transport

_fill_defaults_for_nested_field previously preserved the default MODEL_CONFIG's
thinking_budget_tokens/thinking_effort across a transport override. This leaked
Gemini-family defaults (e.g. thinking_budget_tokens=1024) into OpenAI-transport
overrides, and the OpenAI backend then correctly rejected the unsupported param
at call time (OpenAI uses reasoning.effort, not a token budget).

The helper now strips thinking_budget_tokens and thinking_effort from the
default dict when the env override supplies a transport different from the
default's. Explicit thinking params in the override are preserved.

* fix(config): apply thinking-param strip to dialectic level merge too

DialecticSettings._merge_level_defaults does its own inline MODEL_CONFIG
merge (parallel to _fill_defaults_for_nested_field), so the previous fix
missed dialectic-level overrides. E.g. flipping
DIALECTIC_LEVELS__minimal__MODEL_CONFIG__TRANSPORT from gemini (default)
to openai still leaked the default thinking_budget_tokens=0 into the
openai config, which the OpenAI backend then rejected at call time.

The level-merge path now applies the same 'strip transport-specific
thinking params when transport changes' rule as the generic helper.
Added a regression test exercising the merge validator directly.

* refactor(llm): wire ModelConfig knobs through, prune clients.py migration leftovers

Three connected fixes to finish carving the LLM stack out of src/utils/clients.py
and into src/llm/:

1. Propagate ModelConfig tuning knobs into backend calls.
   honcho_llm_call_inner built extra_params from only {json_mode, verbosity},
   silently dropping top_p, top_k, frequency_penalty, presence_penalty, seed,
   and operator-supplied provider_params from any ModelConfig. Thread the
   selected config through ProviderSelection and merge
   build_config_extra_params(selected_config) into extra_params; per-call
   kwargs still win over provider_params defaults. Makes
   _build_config_extra_params public as build_config_extra_params so
   clients.py and request_builder.py share one translation. Adds
   TestModelConfigExtraParamsPropagation covering OpenAI/Anthropic knob
   propagation, provider_params passthrough, and per-call override
   precedence.

2. Drop dead extract_openai_* duplicates in clients.py.
   extract_openai_reasoning_content, extract_openai_reasoning_details, and
   extract_openai_cache_tokens had no callers outside their own definitions
   — the live implementations live in src/llm/backends/openai.py. -103
   lines from clients.py.

3. Unify on ModelTransport, delete SupportedProviders.
   The "google" vs "gemini" split forced a _provider_for_model_config
   translation shim in two places. Replace all SupportedProviders usages
   with ModelTransport, rename CLIENTS["google"] → CLIENTS["gemini"],
   update provider branches + LLMError labels + reasoning-trace entries
   accordingly. Trace JSONL now writes "provider": "gemini" instead of
   "google" — consistent with the broader env-var rename cutover.

Also tidies up pre-existing basedpyright findings in tests/llm/test_model_config.py
(pydantic before-validator dict inputs + descriptor-proxy call).

ruff: clean. basedpyright: 0 errors, 0 warnings. Tests: 153/153 pass across
tests/utils/test_clients.py, tests/utils/test_length_finish_reason.py,
tests/llm/, tests/dialectic/, tests/deriver/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(llm): finish the src/utils/clients.py → src/llm/ migration

honcho_llm_call_inner now delegates to request_builder.execute_completion
and execute_stream instead of re-implementing backend call scaffolding
inline. The new _effective_config_for_call helper carries per-call kwargs
(temperature, stop_seqs, thinking_budget_tokens, reasoning_effort) onto
the selected ModelConfig — or synthesizes a minimal config for the
test-only callers that pass provider+model directly. max_output_tokens
is zeroed on the effective config to preserve the current
"per-call max_tokens wins" semantic; honoring ModelConfig.max_output_tokens
is a separable correctness concern.

Side effect of routing through the new path: ConfiguredModelSettings'
thinking_budget_tokens validator now fires on synthesized configs.
test_anthropic_thinking_budget was asserting that a sub-1024 budget
propagated to Anthropic — bumped to 1024 to match what Anthropic actually
accepts.

Unified client construction. Promoted the cached client factories in
src/llm/__init__.py (get_anthropic_client, get_openai_client,
get_gemini_client, get_{anthropic,openai,gemini}_override_client) to
public API and added them to __all__. Promoted
credentials._default_transport_api_key → default_transport_api_key.
Deleted the duplicate _build_client and _default_credentials_for_provider
from clients.py; _client_for_model_config now falls through to the
public factories. CLIENTS dict and _get_backend_for_provider stay as the
mockable seam for the ~50 patch.dict(CLIENTS, {...}) test call sites.

Wired operator-configurable Gemini cached-content reuse end-to-end.
PromptCachePolicy moved from src/llm/caching.py into src/config.py so
ModelConfig can reference it as a field without a circular import;
caching.py re-exports the name for existing imports. Added
cache_policy: PromptCachePolicy | None on ConfiguredModelSettings,
FallbackModelSettings, ResolvedFallbackConfig, and ModelConfig.
resolve_model_config, _resolve_fallback_config, and
_select_model_config_for_attempt copy the field through.
honcho_llm_call_inner passes effective_config.cache_policy into
execute_completion / execute_stream, so operators opt in via
e.g. DERIVER_MODEL_CONFIG__CACHE_POLICY__MODE=gemini_cached_content
and the selection actually fires instead of sitting on a dead path.

New regression test test_cache_policy_reaches_gemini_backend asserts the
PromptCachePolicy object reaches the Gemini backend's extra_params.

ruff + basedpyright: clean. Tests: 154/154 pass across
tests/utils/test_clients.py, tests/utils/test_length_finish_reason.py,
tests/llm/, tests/dialectic/, tests/deriver/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(llm): move all LLM orchestration into src/llm/ and delete clients.py

The 1624-line src/utils/clients.py has been carved up into focused modules
under src/llm/ and deleted. There is now one golden path for LLM
orchestration and no dual entrypoint.

New module layout:

  src/llm/
    __init__.py       thin stable re-export surface
    api.py            public honcho_llm_call with retry + fallback + tool
                      loop delegation
    executor.py       honcho_llm_call_inner (single-call executor); bridges
                      to request_builder.execute_completion / execute_stream
    tool_loop.py      execute_tool_loop + stream_final_response, plus
                      assistant-tool-message and tool-result formatting
    runtime.py        AttemptPlan dataclass (replaces the loose
                      ProviderSelection NamedTuple), effective_config_for_call,
                      plan_attempt, per-retry temperature bump, attempt
                      ContextVar
    registry.py       single owner of CLIENTS dict + cached default and
                      override SDK-client factories + backend/history-adapter
                      selection + high-level get_backend(config)
    conversation.py   count_message_tokens, tool-aware message grouping,
                      truncate_messages_to_fit
    types.py          HonchoLLMCallResponse, HonchoLLMCallStreamChunk,
                      StreamingResponseWithMetadata, IterationData,
                      IterationCallback, ReasoningEffortType, VerbosityType,
                      ProviderClient
    request_builder.py low-level request assembly (ModelConfig → backend
                      complete/stream); no longer owns credential resolution
    credentials.py    default_transport_api_key, resolve_credentials
    caching.py        gemini_cache_store; re-exports PromptCachePolicy
                      from src.config
    backend.py        Protocol + normalized result types
    history_adapters.py provider-specific assistant/tool message shapes
    structured_output.py
    backends/         AnthropicBackend, OpenAIBackend, GeminiBackend

handle_streaming_response had no production callers; it is deleted. The
three tests that used it now drive honcho_llm_call_inner(stream=True,
client_override=...) directly, which exercises the same code path the
public API uses.

Dead credential passthrough removed. The ProviderBackend Protocol and
all three concrete backends no longer accept api_key / api_base — those
are baked into the underlying SDK client at registry construction time
and were being del'd everywhere they appeared. request_builder also
stops resolving and forwarding them.

Client construction is unified. The cached default-client factories
(get_anthropic_client, get_openai_client, get_gemini_client) and override
factories (get_*_override_client) are promoted to public API; the
module-level CLIENTS dict populates from them and remains the
patch.dict(CLIENTS, {...}) mocking seam tests rely on. Old duplicate
helpers (_build_client, _default_credentials_for_provider) are gone.
default_transport_api_key is promoted to public.

Application imports now come from src.llm (dreamer, dialectic, deriver,
summarizer, telemetry-adjacent tests). No code imports from
src.utils.clients anywhere in the repo.

ruff: clean. basedpyright: 0 errors, 0 warnings. Tests: 1013/1013 pass
across the entire non-infra test suite (excluding tests/unified,
tests/bench, tests/live_llm, tests/alembic).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(llm): sanitize tool schemas for Gemini's function_declarations validator

Gemini's native-transport function-declarations validator accepts a narrow
subset of JSON-Schema / OpenAPI: type, format, description, nullable, enum,
properties, required, items, minItems, maxItems, minimum, maximum, title.
Anything else — additionalProperties, allOf, if/then/else, $ref, anyOf,
oneOf, $defs, patternProperties — triggers an INVALID_ARGUMENT 400 at call
time.

Our agent tool schemas in src/utils/agent_tools.py use several of those
(additionalProperties: false, allOf + if/then conditionals) because they
were authored for OpenAI strict-mode + Anthropic, which need the richer
vocabulary. GeminiBackend._convert_tools was passing them straight through.

Add _sanitize_schema(): walks the parameters tree and drops unsupported
keywords while preserving semantics for the keywords that hold user data
(properties maps field-name → sub-schema; required / enum are lists of
literals; items is a single sub-schema). Other backends are untouched and
continue to receive the full strict schemas.

Regression tests:
- test_gemini_sanitize_schema_strips_unsupported_keywords: confirms
  additionalProperties, allOf + if/then, and $defs are stripped at nested
  levels while legitimate fields survive.
- test_gemini_convert_tools_sanitizes_parameters_schema: end-to-end
  _convert_tools output has no forbidden keys.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: fix tool calling syntax for gemini

* refactor(llm): normalize defaults, widen OpenAI reasoning-model routing

* chore: fix test

* fix(llm): address post-migration review feedback

* fix(llm): gemini robustness + dreamer specialist ergonomics

* chore: addres review comments

* chore: (docs) unrelease changelog addition

* chore: (docs) merge commit changes

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Erosika <eri@plasticlabs.ai>
2026-04-20 02:46:37 -04:00
adavyas 96765263f4
docs: update Paperclip integration guide (#572) 2026-04-17 16:25:08 -04:00
Maestra 952435848c
fix(docker): remove api-specific healthcheck from shared image (#530)
The HEALTHCHECK directive probes an HTTP endpoint that only the API
serves. The deriver service reuses this image but is a background queue
worker with no HTTP server — the probe can never succeed, so Docker
permanently marks the deriver container as unhealthy.

Remove the HEALTHCHECK from the shared image. Service-level health
checks belong in each service's own configuration (e.g. Kubernetes
readiness/liveness probes on the API Deployment only).

Closes #521

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 10:48:36 -04:00
adavyas 58f9abba98
docs: add paperclip integration docs (#549)
* Simplify Paperclip integration instructions

Clarified instructions for local Honcho setup and removed unnecessary details.


* Update docs.json

* Update links in Paperclip integration guide

* Revise memory initialization instructions in Paperclip guide

Updated instructions for initializing memory and removed optional checks section.
2026-04-10 14:35:14 -04:00
Vineeth Voruganti 317b4a6cba
v3.0.6 Release Candidate (#550)
* chore: (docs) Update changelogs and version numbers

* chore: remove extraneous dep on mintlify
2026-04-10 13:16:42 -04:00
Vineeth Voruganti 5b6bd59030
Tighten Transaction Scopes (#525)
* fix: further remove extraneous transactions

* fix: (search) use 2 phase function to reduce un-needed transaction

* fix: refactor agent search to perform external operations before making a transaction

* fix: reduce scope of queue manager transaction

* fix: (bench) add concurrency to test bench

* fix: address review findings for search dedup, webhook idempotency, and bench throttling

* Fix Leakage in non-session-scoped chat call (#526)

* fix: (search) reduce scope for peer based searches

* fix: tests

* fix: (test) address coderabbit comment

* fix: drop db param from deliver_webhook

---------

Co-authored-by: Rajat Ahuja <rahuja445@gmail.com>
2026-04-08 11:14:50 -04:00
Eri Barrett ff116b0601
Self-hosting docs overhaul: single-provider default, restructured config guide (#510)
* fix: Inconsistencies in Docs, health endpoint, troubleshooting guide

* fix: (docs) maintain consistency on postgres db name

* chore: (docs) update v2 contributing docs with updates db paths

* docs: overhaul self-hosting docs for provider-agnostic setup

- .env.template: lead with provider options (custom, vllm, google,
  anthropic, openai, groq) instead of baking in vendor-specific keys.
  All provider/model settings commented out so server fails fast until
  configured. Separate endpoint config from per-feature provider+model
  from tuning knobs.
- docker-compose.yml.example: fix healthcheck -d honcho -> -d postgres
  to match POSTGRES_DB=postgres.
- config.toml.example: reorder and document LLM key section with
  OpenRouter and vLLM examples.
- self-hosting.mdx: replace multi-vendor key table with provider options
  table. Add examples for OpenRouter, vLLM/Ollama, and direct vendor
  keys. Remove duplicated key lists from Docker/manual setup sections.
- configuration.mdx: replace scattered provider docs with provider types
  table. Fix Docker Compose snippet to match actual compose file. Note
  code defaults as fallback, not recommended path.
- troubleshooting.mdx: add alternative provider issues section (custom
  provider config, model name format, Docker localhost, structured
  output failures).

* docs: add Docker build troubleshooting for permission errors

- Document BuildKit requirement (RUN --mount syntax)
- AppArmor/SELinux blocking Docker builds on Linux
- Volume mount UID mismatch between host and container app user
- Note in self-hosting docs that Docker path builds from source

* docs: reframe self-hosting as contributor/dev path, point to cloud service

* Revert "docs: reframe self-hosting as contributor/dev path, point to cloud service"

This reverts commit 3e766eb1a9.

* docs: add production compose, model guidance, thinking budget docs

- Add docker-compose.prod.yml for VM/server deployment: no source
  mounts, restart policies, 127.0.0.1-bound ports, cache enabled
- Add model tier guidance and community quick-start link to self-hosting
- Document THINKING_BUDGET_TOKENS gotcha for non-Anthropic providers
- Add reverse proxy examples (Caddy + nginx) to production section
- Add backup/restore commands to production considerations

* docs: simplify self-hosting to single provider, restructure config guide

Self-hosting page now defaults to one OpenAI-compatible endpoint
with one model for all features. Moved model tiers, alternative
providers, and per-feature tuning into the configuration guide.
Eliminated duplicate config priority sections, dev/prod split,
and redundant TOML examples.

* docs: merge compose files, restore provider/model to feature sections in .env.template

Single docker-compose.yml.example with dev sections commented out.
Moved PROVIDER and MODEL back alongside each feature in .env.template
so settings stay colocated with their module. Updated self-hosting
docs to reference single compose file.

* fix: broken anchor links, redundant migration step, minor inconsistencies

Fix 4 broken internal links (#llm-provider-setup, #llm-api-keys,
#which-api-keys-do-i-need, #alternative-providers) to point to
correct headings. Remove redundant Docker migration step (entrypoint
already runs alembic). Fix cache URL missing ?suppress=true in
reference config. Fix uv install command to use official method.

* docs: env template ready to use, simplify self-hosting flow

.env.template now has provider/model lines uncommented with
placeholder values — user just sets endpoint, key, and model name.
Thinking budgets default to 0 for non-Anthropic providers.

Self-hosting page: removed 30-line env var wall, LLM setup now
points to the template. Merged duplicate verify sections.
Removed api_key from SDK examples (auth off by default).

* docs: reorder next steps, configuration guide first

* fix: default embedding provider to openrouter for single-endpoint setup

Without this, embeddings default to openai which requires a separate
LLM_OPENAI_API_KEY. Setting to openrouter routes embeddings through
the same OpenAI-compatible endpoint as everything else.

* fix: review issues — hermes page, thinking budget, production wording

Hermes integration page: replaced inline Docker/manual setup with
link to self-hosting guide, added elkimek community link. Removed
old env var names (OPENAI_API_KEY without LLM_ prefix).

Troubleshooting: removed "or 1" from thinking budget guidance.
Self-hosting: softened "production-ready" to "production-oriented"
since auth is disabled by default.

* docs: model examples in template, expanded LLM setup, better verify flow

.env.template: added "e.g. google/gemini-2.5-flash" hints next to
model placeholders so users know the expected format.

Self-hosting: expanded LLM Setup to show the 3 things users need to
set (endpoint, key, model name) with find-replace tip. Added build
time note, deriver log check, and real smoke test (create workspace)
to verify section. Health check now notes it doesn't verify DB/LLM.

* fix: smoke test uses v3 API path, not v1

* docs: clarify deriver metrics port vs Prometheus host port

* fix: remove deprecated memoryMode from hermes config example

* docs: update hermes page to match current memory provider config

Updated config to match hermes-agent docs: removed apiKey (not needed
for self-hosted), added hermes memory setup CLI command, added config
fields table (recallMode, writeFrequency, sessionStrategy, etc.).

Better verification tests: store-and-recall across sessions, direct
tool calling test. Links to upstream hermes docs for full field list.

* fix: invalid THINKING_BUDGET_TOKENS=0 and missing docker/ in image

Comment out THINKING_BUDGET_TOKENS=0 in .env.template — deriver,
summary, and dream validators require gt=0. Dialectic levels also
commented out since non-thinking models don't need the override.

Add COPY for docker/ directory in Dockerfile so entrypoint.sh is
available when docker-compose.yml.example references it.

* chore: Additional troubleshooting step

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-04-07 22:49:57 -04:00
ajspig 95c72d76d3
docs: add Zo Computer integration page (#504) 2026-04-07 10:45:00 -04:00
Luba Kaper fa8f0b1a19
feat(examples): add Honcho memory skill for Zo Computer (#495)
* chore: add .worktrees/ to .gitignore

* feat(examples): add Zo Computer memory skill integration

* feat(examples): add Zo Computer memory skill integration

* fix(examples): address CodeRabbit review on Zo skill integration

  - Fix version inconsistency: SKILL.md matches pyproject.toml (>=2.1.0)
  - Move client.py into tools/ package and use relative imports
  - Add assistant_id parameter to save_memory() for consistency with get_context()
  - Use UUID-based IDs in tests to prevent state leakage between runs
  - Add pytest.mark.skipif guard on integration tests (requires HONCHO_API_KEY)
  - Fix import ordering, move pytest to module level, sort __all__ alphabetically
  - Fix markdown blank lines around fenced code blocks (MD031)
  - Add rate limit delay fixture to avoid hitting Honcho free tier limits

* fix(examples): validate HONCHO_API_KEY early in client initialization

* docs(examples): note cross-peer memory behavior in shared workspaces

* docs(examples): fix save_memory and query_memory signatures in README

* docs(examples): fix markdown linting issues in README

* docs(examples): add assistant_id parameter to save_memory example in
  SKILL.md

---------

Co-authored-by: Luba Kaper <lubakaper@lubas-air.mynetworksettings.com>
2026-04-06 17:06:08 -04:00
Vincent Koc e4873586fa
docs: improve OpenClaw integration page (#462)
- Fix tool names to match actual plugin code (honcho_context,
  honcho_search_conclusions, honcho_search_messages, honcho_ask)
- Add link to OpenClaw Honcho Memory docs (docs.openclaw.ai)
- Add OpenClaw Memory Docs card in Next Steps
- Fix QMD setup: remove manual collection commands, link to OpenClaw QMD docs
2026-04-03 12:38:48 -04:00
Rajat Ahuja 1e0f539fe5
feat: retry on more httpx exceptions (#467)
* feat: retry on more httpx exceptions

* fix: Add retry parity to typescript and update docs

* chore: (skills) update skills to match latest state of the sdk

* chore: (docs) update stale sdk code

* chore: (docs) clean up inconsistencies in docs

* chore: Rebuild Package

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-04-03 12:20:53 -04:00
Vineeth Voruganti 302a6808e7
Vineeth/force rollback (#486)
* fix: Explicit Rollback in Transaction

* chore: update tests
2026-04-03 11:58:03 -04:00
Vineeth Voruganti 29ff4653e5
chore: (docs) v3.0.4 Release Candidate (#480) 2026-04-02 13:48:44 -04:00
Vineeth Voruganti d803c546f0
fix: (search) add logic to use external vectore store for message search (#479)
* fix: (search) add logic to use external vectore store for message search

* fix: (search) oversample to reduce duplicate errors
2026-04-02 13:00:54 -04:00
Vineeth Voruganti 0533c6dd26
fix dialectic held connection (#477)
* fix: dialectic held connection

* fix: (agent) pre-compute embeddings for agent tools

* fix: (tests) refactor tests to use smaller test db connections

* fix: Embedding client to branch depending on vector store

* fix: reflect dedup-skipped observations in created counts and isolate DB sessions in extract_preferences

* fix: (tests) update tests to match changes

* fix: expunge docs + don't pass in db to query_documents

---------

Co-authored-by: Rajat Ahuja <rahuja445@gmail.com>
2026-04-02 11:13:09 -04:00
doria cc3483bfaa
refactor: MCP server improvements (#379)
* refactor: MCP server improvements

* feat: add missing tools and polish descriptions. Add inspect_workspace, list_workspaces, inspect_session, get_session_message, and get_session_summaries tools. Fix CORS preflight handling and default route path. Extract shared formatting helpers for messages and summaries. Trim redundant tool descriptions.

* chore: cr updates

* chore: cr

* fix: adding pagnation

* fix: use nanoid to match Honcho's server-side ID format

* fix: simplified tool list. 10 removed, in favor of more unified tools

* docs: clarifying language

* docs: updating agentic dev & MCP.mdx docs

* docs: minor fix

* fix: adding codex

* chore: updating with latest SDK

* docs: update

* fix: adding assistantName config

* chore: clarifying language

---------

Co-authored-by: ajspig <dragon@monstercode.com>
2026-03-31 17:34:36 -04:00
ajspig a5423b52e8
ts SDK fix: adding strict checking (#421)
* fix: adding strict checking and updating readme

* chore: changelog and version

* fix: Add strict validation to all Python and TypeScript classes

* fix: Address Code Rabit Comments

* fix: duplicate searchQuery param in typescript session.context()

* feat: add created_at, is_active fields, and get_message method

* feat: Add pagintion params to sdk

* fix: Remove lazy initalization behavior from sdks

* fix: Address File Upload Validation, add compatibility shims, address review comments

* chore: Docs updates

* fix: Convert session config from API format in Peer.sessions()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: Pass all args to Session constructor in Peer.sessions()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: Preserve createdAt in Peer.refresh(), pass all data in session.peers()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: changelog for ts

* fix: Review Comments

* fix: Add createAt and to peers call

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 10:57:12 -04:00
Agney Menon e9ee9d9ddb
docs: add pi-honcho-memory community integration (#458)
Co-authored-by: Agney <agney@Agneys-MacBook-Pro.local>
2026-03-30 10:28:16 -04:00
Hart ef3426d81b
fix(representation): add missing deleted_at filter to working representation queries (#456)
RepresentationManager._query_documents_recent() and
._query_documents_most_derived() do not filter soft-deleted documents,
unlike every other document query function in the codebase. This causes
the deriver's working representation to include documents that are being
garbage-collected.

Refs #444

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 13:39:57 -04:00
Rajat Ahuja 7275372128
fix: only emit CleanupStaleItemsCompletedEvent if queue item was deleted (#454) 2026-03-26 16:12:36 -04:00
Lalit Aditya Julapalli 9ee331f79b
docs(self-hosting): fix database name in connection URI (#448) 2026-03-25 02:38:43 -04:00
Viktor Szépe b3c1917a38
Fix typos (#440) 2026-03-22 15:52:55 -04:00
ajspig 92914abdeb
Abigail/dev 1436 (#438)
* docs: comprehensive overview guide with all recent integrations

- Add all recent integrations: Gmail, Granola, OpenClaw, Agent Zero, Hermes
- Organize by use case: Quick Start, Agent Frameworks, Platform Integrations, Community Agents, Communication Platforms, Data Import
- Include feature comparison matrix with support levels
- Add clear categorization by setup time and complexity
- Structure follows OpenClaw documentation format with clear sections

* docs: update overview guide to match dev-1372-new format

- Use simpler, cleaner organization with focused sections
- Group by: AI Assistants, Platform Connectors, Agent Frameworks, Migrations
- Match icon and formatting style from dev-1372-new branch
- Include all recent integrations: Gmail, Granola, Hermes, Agent Zero, OpenClaw

* docs: clean up Platform Connectors section description

Remove redundant description text for cleaner presentation
2026-03-22 15:50:01 -04:00
Eri Barrett 254510db63
Merge pull request #427 from plastic-labs/docs/hermes-honcho-guide
docs: add Hermes Agent Honcho integration guide
2026-03-20 17:19:06 -04:00
Erosika d8a01230ae docs: delete old community hermes stub 2026-03-19 18:06:06 -04:00
Erosika e2e309148b docs: move Hermes from community to integrations in sidebar nav 2026-03-19 17:43:48 -04:00
Erosika c9de186b80 docs: update Hermes guide description 2026-03-19 17:42:49 -04:00
Erosika 65e68fae55 docs: trim Hermes guide, remove CLI/gateway mode section 2026-03-19 17:42:49 -04:00
Rajat Ahuja 1cbcbc0263
fix: populate test harness DB config from docker compose (#435) 2026-03-19 17:39:57 -04:00
LRRuan 2097c2cbcf
fix(files): handle empty json uploads safely (#434)
* fix(files): handle empty json uploads safely

* fix(files): normalize invalid json upload errors

* fix(files): restore file processing error import

---------

Co-authored-by: LRRuan <lrruan@users.noreply.github.com>
2026-03-18 18:36:34 -04:00
Vineeth Voruganti 09a980c2fb
Sanitization and Memory Bug Fixes (#419)
* fix: use WeakValueDictionary for _observation_locks to prevent memory leak

* fix: harden input sanitization across API surface (DEV-1400)

- Parameterize SQL in set_config calls to prevent injection via request context
- Strip NUL bytes from string inputs (message content, queries, peer cards)
- Add JSONB metadata validation (100 key limit, 5 depth limit)
- Add filter recursion depth limit (max 5) to prevent stack overflow
- Update changelogs with unreleased entries

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: Refactor Schemas into separate files

* fix: Code Rabbit Comments

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 15:01:52 -04:00
Rajat Ahuja 122ebfe998
fix: add observability to docker compose + get docker compose into a usable state (#429)
* fix: add observability to docker compose + get docker compose into a usable state

* fix: init.sql location

* fix(docker): use built venv at runtime

---------

Co-authored-by: adavyas <adavyasharma@gmail.com>
2026-03-18 13:16:22 -04:00
ajspig db6543acf8
Adding granola integration (#417)
* feat: adding python granola example

* docs: adding granola documentation and cleaning up the script

* chore: addressing pr comments

* chore: nav update

* chore: cr updates

* fix: sig restructure and simplification

* docs: updating for clarity

* chore: cr

* fix: cleaning up script

* feat: adding gmail guide

* refactor: replace globals with param passing, add Granola rate limit retry, surface unparseable transcripts

* chore: cr

* fix: adding example script and added more of a natural flow/progression

* fix: final fix
2026-03-18 13:08:20 -04:00
Erosika 4cda36c860 Add SillyTavern integration docs
New integration guide for the sillytavern-honcho extension covering
install, global config, context architecture, enrichment modes, and
troubleshooting. Added to v3 integrations nav.
2026-03-17 15:18:32 -04:00
Vineeth Voruganti 24f94f3ff8
chore: (docs) Add information on the peer card (#418) 2026-03-09 21:52:37 -04:00
ajspig f61d58f56e
Abigail/dev 1406 (#416)
* docs: replace Chatbots section with Tutorials, move Reachy Mini into it

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: restructure nav — move modeling-data to Core Concepts, file-uploads to Advanced, merge Migrations into Tutorials

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: final moving

* docs: minor changes

* docs: adding community integrations

* chore: cr updates

* fix: Rename patterns page and nitpicks on organization

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-03-06 23:21:28 -05:00
Vineeth Voruganti 2f895efb7f
chore: (docs) update claude code plugin copy (#414)
* chore: (docs) update claude code plugin copy

* docs: adding reachy-mini

* docs: adding openclaw multi-agent setup

* docs: updating

* chore: cr

---------

Co-authored-by: ajspig <dragon@monstercode.com>
2026-03-05 10:30:01 -05:00
ajspig ee1ffada21
docs: updating to match recent updates (#413) 2026-02-27 12:22:46 -05:00
Vineeth Voruganti f686205167
fix: Update Changelogs and OpenAPI Docs (#412) 2026-02-25 22:16:44 -05:00
Vineeth Voruganti 10ef7b96a8
Add Stricter limits to Summary & Peer Card (#400)
* fix: Add bounds to gemini client

* fix: Prevent empty summaries from being saved to DB (HONCHO-M7)

Raise LLMError on blocked Gemini responses (SAFETY, RECITATION, etc.)
so retry/backup-provider logic triggers. Treat empty LLM responses in
the summarizer as fallback instead of persisting empty strings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: Summary Eval via Locomo

* fix: Code Rabbit Comments

* fix: Code Rabbit Comments

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 15:08:09 -05:00
Vineeth Voruganti 5a42f9b3cd
Use savepoints to prevent race condition in get_or_create chain (#399)
* fix: Use savepoints to prevent race condition in get_or_create chain

Replace commit()/rollback() with begin_nested() savepoints in
get_or_create_workspace, get_or_create_peers, and get_or_create_session
so that an IntegrityError rollback in a nested call doesn't undo flushed
work from the caller. Moves transaction commit responsibility to the
outermost caller and defers cache operations to post-commit callbacks
on GetOrCreateResult.

Closes DEV-1321

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: Add missing commit and post_commit in chat endpoint

The chat() endpoint in routers/peers.py called get_or_create_peers()
but discarded the result without committing or invoking post_commit().
This meant new peers created lazily via SDK chat calls were never
persisted, and cache invalidation was skipped.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 23:28:04 -05:00
Vineeth Voruganti 3b1e964372
Modify Queue Status to only relevant tasks (#398)
* feat: Update Honcho system benchmarks

* fix: Align with runner common functions and fixed basedpyright issues

* fix: Address coderabbit issues

* chore: Address Ruff Errors

* fix: Queue Status to remove unhelpful info

* chore: (docs) Add docs for dreaming and buffering

* chore: Remove claude workflow

---------

Co-authored-by: 3un01a <3un01a@plasticlabs.ai>
2026-02-23 22:54:21 -05:00
3un01a 2631209256
feat: Update Honcho system benchmarks (#393)
* feat: Update Honcho system benchmarks

* fix: Align with runner common functions and fixed basedpyright issues

* fix: Address coderabbit issues

* chore: Address Ruff Errors

---------

Co-authored-by: 3un01a <3un01a@plasticlabs.ai>
Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-02-23 21:30:30 -05:00
3un01a eba9279af2
Oolong Benchmark (#323)
* (feat) Add Oolong Benchmarks

* (fix) Address issues to fix basedpyright and coderabbit comments

* (fix) Address basedpyrwright additional warnings

* (fix) Address additional coderabbit issues

* (fix) Replace huggingface data loading to local filesystem-based

* (fix) Address coderabbit issues regarding data paths

* fix: Align with test harness conventions

* fix: Code Review Comments

* fix: stream data rather than load all at once

---------

Co-authored-by: 3un01a <3un01a@plasticlabs.ai>
Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-02-23 16:55:59 -05:00
Vineeth Voruganti 780bfe1c30
Refactor cache to store data rather than orm objects (#395)
* fix: refactor cache to store data rather than orm objects

* fix: Add Cache Version Keys
2026-02-23 16:03:29 -05:00
Vineeth Voruganti 78df86dc66
fix: Remove noisy sentry error on llm failure and upgrade deps (#396) 2026-02-23 15:53:29 -05:00
ajspig 5bd93a2bef
adding test reasoning levels script (#337)
* chore: 3.0 honcho and 2.0 sdks changelog

fix: use PeerContextResponse in peer.ts

* chore: move docs to /v3/, build SDKs

* chore: code review

* feat: [WIP] migrate away from stainless in typescript sdk

* chore: move api from /v2/ to /v3/

* feat: no-stainless typescript with real tests

* feat: migrate python sdk off of stainless

* feat: clean typescript sdk

* chore: add tests for ts http client

* fix: rewrite entire python sdk in new format, update typescript sdk to use `configuration` not `config` for consistency with API

* fix: clean up SDKs, synchronize

* chore: update sdk examples

* chore: update OpenAPI documentation and SDK examples to reflect changes

* fix: better test

* fix: install deps in test runner, improve robustness of streaming in sdk, coderabbit nits

* fix: standardize around camelCase in TS SDK

* refactor: update configuration handling in SDKs to use typed models for workspace, session, and peer configurations

* docs: clarify queue status usage and remove polling methods from SDKs

add claude skills for migrations

* chore: fix links in docs

* feat: add deriver flush mode to bypass batch token threshold

- Introduced `is_deriver_flush_enabled` function to check if flush mode is active.
- Updated `QueueManager` to conditionally apply batch token thresholds based on flush mode.
- Enhanced `UnifiedTestExecutor` to enable flush mode via Redis.
- Added `flush` parameter to test cases to facilitate testing of flush mode behavior.
- Updated various test cases to utilize the new flush functionality.

* feat: implement schedule_dream functionality in SDKs, use in unified test runner

- Added `schedule_dream` method to both Python and TypeScript SDKs for scheduling dream tasks.
- Updated HTTP routes to include endpoint for scheduling dreams.
- Enhanced test runner to utilize the new `schedule_dream` method for scheduling actions.
- Updated TypeScript client to support the new scheduling functionality with appropriate parameters.

* feat: update single deriver task to support multiple observers

- Changed the `observer` parameter to `observers` as a list in multiple functions across the deriver module.
- Updated the processing logic to handle multiple observers for representation tasks.
- Adjusted related payload and queue management functions to accommodate the new observers structure.
- Modified tests to reflect changes in the representation task handling and ensure proper functionality.

* refactor: update enqueue tests to support deduplication of queue items with multiple observers

- Modified tests in `test_enqueue.py` to reflect changes in the queue item structure, where each message now results in a single queue item containing a list of observers.
- Updated assertions to validate that the `observers` field correctly includes all relevant peers, ensuring proper functionality of the deduplication logic.
- Removed redundant payload matching logic to streamline test cases and improve clarity.

* fix: add backwards compatibility for representation work unit keys and payload observers

* add: results

* add: adoption journey

* feat: update dialectic configuration and introduce cost calculator

- Adjusted LLM and dialectic settings in `.env.template`, `config.toml.example`, and `src/config.py` to reduce maximum tool output characters and session history tokens for cost efficiency.
- Implemented a new `dialectic_cost_calculator.py` script to estimate costs based on reasoning levels and model pricing.
- Enhanced `DialecticAgent` to utilize minimal tools and adjusted output token settings based on reasoning level to optimize performance and reduce costs.

* feat: add reasoning level to chat input in unified test runner

- Enhanced the `UnifiedTestExecutor` to include a `reasoning_level` parameter in the chat method call.
- Updated the `QueryAction` model to support the new `reasoning_level` attribute, allowing for more nuanced chat interactions.

* add: adding script for testing reasoning levels

* fix: moving script

* fix: remove stale doc files deleted in main

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: Code Review Changes

---------

Co-authored-by: Benjamin McCormick <docterformer@protonmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-02-19 15:51:15 -05:00
Rajat Ahuja 046935a6bb
fix: backup provider failover bugs and tool input type safety (#392)
* fix: backup provider failover bugs and tool input type safety

* fix: Add maximum boundaries to agent_tools

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-02-18 11:55:29 -05:00
Rajat Ahuja 233df802f1
chore: filter out GET /metrics logs (#391) 2026-02-18 11:42:05 -05:00
Eri Barrett e0afc386ce
feat: bot-integrations claude skill (honcho for bot frameworks) (#382)
* feat: add nanobot-honcho claude skill

guided integration skill for adding honcho long-term memory to
HKUDS/nanobot instances. includes SKILL.md with step-by-step
instructions and reference implementations for client, session
manager, and agent tool.

* restructure: nanobot-honcho -> bot-integrations skill

Replace one-off nanobot-honcho skill with general bot-integrations skill
targeting the common architectural pattern shared by conversational bot
frameworks (agent loop, session manager, tool registry, message bus).

Structure:
  .claude/skills/bot-integrations/
    SKILL.md                    # adaptive skill for any bot framework
    references/nanobot/         # concrete nanobot implementations

SKILL.md walks through 4 phases (explore, interview, implement, verify)
with awareness of bot frameworks. When it detects a known framework, it
pulls from the matching reference folder for concrete implementations.

Reference files updated with:
- sync flag moved to after API call success
- cache consistency for aliased sessions
- MEMORY.md/HISTORY.md migration support
- migration transcript formatting with XML context tags

Future framework references (openclaw, picoclaw, etc) drop into
references/<framework>/ as they trend.

* fix: merge skills and repair syntax inconsistencies (#385)

* fix: updating docs based on new clawhub skill (#381)

* fix: use ORM mutation for re-embedded vectors in reconciler (#384)

* feat: implement async workspace deletion with active session checks (#378)

* feat: implement async workspace deletion with active session checks

- Updated the DELETE /workspaces/:id endpoint to return 202 Accepted, indicating that the deletion request is processed in the background.
- Added a check for active sessions before allowing workspace deletion, raising a ConflictException if any exist.
- Updated related tests to ensure proper handling of active sessions during workspace deletion.

* fix: Address review issues

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>

* fix: merge skills and repair syntax inconsistencies

---------

Co-authored-by: ajspig <46900795+ajspig@users.noreply.github.com>
Co-authored-by: Rajat Ahuja <rahuja445@gmail.com>
Co-authored-by: doria <93405247+dr-frmr@users.noreply.github.com>

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
Co-authored-by: ajspig <46900795+ajspig@users.noreply.github.com>
Co-authored-by: Rajat Ahuja <rahuja445@gmail.com>
Co-authored-by: doria <93405247+dr-frmr@users.noreply.github.com>
2026-02-16 17:58:42 -05:00
Rajat Ahuja a6f029e164
fix: always create new machine + don't run on PR (#390) 2026-02-16 17:56:27 -05:00
Rajat Ahuja 8115b160b4
fix: rm volume from unified test fly (#388) 2026-02-16 16:21:05 -05:00
Vineeth Voruganti beb282bfbc
fix: Various Codex Audits (#386)
* fix: Various Codex Audits

* fix: Address Comments
2026-02-13 12:00:15 -05:00
Rajat Ahuja 97df0a80cd
feat: consolidate db calls in session context (#380)
* feat: consolidate db calls in session context

* fix: guard against embedding failures

* Parallelize Async Calls (#383)

* fix: parallelize db calls in context()

* fix: (test) use session factory to further isolate tests

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-02-13 11:54:41 -05:00
Rajat Ahuja 33ef0f8eab
perf: add redis caching and reduce embedding API calls (#372)
* feat: add redis to get collection + get session peer config

* fix: reduce embedding calls

* fix: delete session caches when soft-deleting session

* fix: batch embeddings in create observatiosn tool

* fix: batch embeddings call in extract_preferences

* fix: double embed in fallback for _handle_search_memory

* fix: claude comments

* fix: return ObservationResult

* fix: make cache delete/set retryable. remove session peer config cache

* chore: claude nitpicks

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-02-13 00:53:46 -05:00
doria 3aaced2cd1
feat: implement async workspace deletion with active session checks (#378)
* feat: implement async workspace deletion with active session checks

- Updated the DELETE /workspaces/:id endpoint to return 202 Accepted, indicating that the deletion request is processed in the background.
- Added a check for active sessions before allowing workspace deletion, raising a ConflictException if any exist.
- Updated related tests to ensure proper handling of active sessions during workspace deletion.

* fix: Address review issues

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-02-12 18:48:00 -05:00
Rajat Ahuja 5f7dad97b9
fix: use ORM mutation for re-embedded vectors in reconciler (#384) 2026-02-12 14:08:37 -05:00
ajspig f6e1f237e2
fix: updating docs based on new clawhub skill (#381) 2026-02-12 09:28:57 -05:00
Vineeth Voruganti f330cce386
chore: add model to the github actionw orkflow (#377)
* chore: add model to the github actionw orkflow

* chore: fix model arg to new syntax
2026-02-10 11:46:26 -05:00
ajspig 56aed23939
docs: add teams and logging sections to Claude Code guide (#376)
* docs: add teams and logging sections to Claude Code guide

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: updating header

* fix: minor typo

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 10:54:37 -05:00
Vineeth Voruganti 7e439e1802 chore: (docs) update sdk changelogs 2026-02-09 16:12:13 -05:00
doria 2522cc5ee6
sdks: add set peer card function (#371)
* feat: add set peer card to SDK, bump version, document

* fix: pytest -> pytest -x

* chore: deprecate .card(), move to .getCard() / .get_card()

* fix: get_or_create when crudding peer cards

* chore: review nits

* chore: document .get_card / .set_card

* chore: (docs) update language from deriver to dreamer agent

* fix: get_peer_card should not create peer/workspace

* fix: change api contract to return ResourceNotFoundException (#375)

* fix: PR nitpicks

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-02-09 15:35:57 -05:00
doria 3eab54374c
chore: parallelize tests for speed (#374)
* chore: parallelize tests for speed

* fix: truncate all tables after each test

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-02-06 21:10:34 -05:00
Vineeth Voruganti e93e57175e chore: (docs) fix links 2026-02-04 13:49:18 -05:00
vintro 0d3a01ef49
fix: add local hosting note to the top of openclaw guide (#369) 2026-02-04 13:15:29 -05:00
Rajat Ahuja c794c73bc0
fix: trigger claude-review on everything (#368) 2026-02-04 11:54:58 -05:00
ajspig 586444900b
docs: adding claude code integration (#365)
* docs: adding claude code integration

* chore: including interview and MCP integration

* chore: updating vibecoding

* chore: (docs) Update Claude Code Plugin Names

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-02-04 10:19:51 -05:00
doria 445f55a6c0
chore: add 3.0.2 changelog, update version (#367) 2026-02-03 15:25:22 -05:00
Vineeth Voruganti 058bf6254f
Add Claude Code GitHub Workflow (#364)
* "Claude PR Assistant workflow"

* "Claude Code Review workflow"

* fix: delete separate code-review.yml

* fix: condense and fix claude and claude review workflows

* add PR guard

---------

Co-authored-by: Rajat Ahuja <rahuja445@gmail.com>
2026-02-03 13:44:50 -05:00
doria a2c004f7b5
feat: document reasoning_level parameter (#366) 2026-02-03 13:42:18 -05:00
doria 44577e048c
feat: add message-search fallback in dialectic tools for short sessions (#362) 2026-02-03 11:17:24 -05:00
doria c9dee51fb9
refactor: enhance specialist exploration and observation processes (#357)
* refactor: enhance specialist exploration and observation processes

- Updated the orchestration logic to allow specialists to explore freely with optional hints from high-surprisal observations.
- Removed predefined probing questions, enabling a more dynamic approach to observation gathering.
- Adjusted the deduction and induction specialists to utilize the current peer card context and exploration hints in their prompts.
- Improved documentation within the code to clarify the roles and responsibilities of specialists in the observation process.

* feat: add thorough dream test

* refactor: standardize hint terminology and dedup peer card context
2026-02-03 11:02:07 -05:00
Rajat Ahuja d3637a9d73
fix: make FLUSH_ENABLED a config value (#361)
* fix: use cashews for DERIVER_FLUSH_KEY

* fix: make FLUSH_ENABLED a config value
2026-02-03 10:53:14 -05:00
Rajat Ahuja c50362d505
fix: N+1 query in search_messages (#359) 2026-02-03 10:35:53 -05:00
ajspig f0874e29ff
feat: adding openclaw integration guide (#360)
* feat: adding openclaw integration guide

* fix: (docs) update link and openclaw description

* chore: (docs) update npm package to match standard

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-02-02 17:59:26 -05:00
doria 8ae21bb4fa
chore: delete old unused test system (#358) 2026-01-29 17:40:27 -05:00
ajspig b578f4897f
Updating Docs & Packages with 3.0.0 SDK (#356)
* chore: (docs) Reconcile new SDK conventions in docs

* chore: updating docs references to SDK.

* chore: final edits to docs and packages for SDK and API updates

* chore: (docs) fix typescript context() options method

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-01-29 17:10:33 -05:00
Vineeth Voruganti 4fcf6c4574
chore: (docs) Reconcile new SDK conventions in docs (#355) 2026-01-28 10:58:40 -05:00
Vineeth Voruganti 4add711711
v3.0.1 Patch (#354)
* chore: (docs) changelog for patch version

* chore: fix types
2026-01-27 12:33:08 -05:00
ajspig 19d1084302
fix: adding n8n guide (#353) 2026-01-27 11:34:49 -05:00
ajspig ac49a7cedb
Abigail/minor fix (#351)
* fix: updating vibecoding

* fix: brooken links
2026-01-27 11:18:41 -05:00
doria a4a341e8b5
fix: add migration for 'deriver' to 'reasoning' in ResolvedConfiguration (#352) 2026-01-27 11:17:49 -05:00
Rajat Ahuja 110787cdca
feat: use messages from queue items for rep completed token count (#350) 2026-01-26 18:00:53 -05:00
Vineeth Voruganti bd68c19a07
Documentation edits made through Mintlify web editor 2026-01-26 15:27:38 -05:00
Vineeth Voruganti 911aa6029f
v3.0.0 Release Candidate (#346)
* fix: (docs) update changelog and examples

* chore: (docs) Add note on Vector Stores

* fix: (mcp) WIP to sync with 3.0.0 conventions

* fix: (mcp) Sync MCP with final API changes for 3.0.0

* chore: (docs) sync changelog and openapi spec

* fix: fixing .mdx files to match openapi.json spec

* chore: Add script to estimate event creation

---------

Co-authored-by: ajspig <dragon@monstercode.com>
2026-01-26 15:06:02 -05:00
Rajat Ahuja 2b4fb953f8
fix: RepresentationCompletedEvent -> input tokens to messages tokens (#349) 2026-01-26 14:36:00 -05:00
doria 5371463f39
Merge pull request #348 from plastic-labs/ben/trim-test-trial-tools
feat: give bench tools access to remote honchos
2026-01-26 14:18:39 -05:00
doria d79a9f21d2
feat: make session_name nullable for documents and update related SDKs (#347)
* feat: make session_name nullable for documents and update related SDKs

- Introduced a migration to make the `session_name` column in the documents table nullable, allowing for sessionless dreams.
- Updated Python and TypeScript SDKs to reflect the optional nature of `session_id` in conclusion creation and related methods.
- Enhanced tests to cover scenarios for creating conclusions without a session ID, ensuring proper handling of sessionless conclusions.
- Adjusted documentation and type definitions to clarify the optional session context in various components.

* chore: add migration test

* fix: ensure orphaned sessions exist during downgrade for nullable session_name migration
2026-01-26 13:33:11 -05:00
Rajat Ahuja 421bbb0aab
chore: lazy-load imports (#345)
* chore: lazy-load imports

* fix: lazy-load pdfplumber
2026-01-26 11:35:41 -05:00
Rajat Ahuja 2270e5666f
switch OTEL metrics to prometheus (#344)
* feat: replace OTEL with Prometheus

* fix: second pass of docs and cleanup
2026-01-25 17:26:42 -05:00
Benjamin McCormick 6c3f671636 fix: clean Redis URL by removing query parameters in cache client and update flush mode logging in benchmark runner 2026-01-23 17:57:58 -05:00
Rajat Ahuja d5e66dd565
fix: bump memory from 512mb to 1gb (#343) 2026-01-23 16:22:18 -05:00
Benjamin McCormick 258fd3736b fix: turn off summaries on test runs directly 2026-01-23 16:13:00 -05:00
Rajat Ahuja 6de7a8885a
fix: don't expose metrics endpoints for fly to scrape (#342) 2026-01-23 16:11:10 -05:00
Benjamin McCormick ffff6afb1a chore: refactor test runners to be cleaner, deduplicate code, add params for testing remote servers 2026-01-23 15:47:51 -05:00
doria e9d8b8759d
refactor: update semantic search parameter from `last_user_message` to `search_query` across documentation and SDKs (#341)
* refactor: update semantic search parameter from `last_user_message` to `search_query` across documentation and SDKs

- Changed references in documentation and code to use `search_query` instead of `last_user_message` for fetching semantically relevant observations and conclusions.
- Updated related function signatures and descriptions in Python and TypeScript SDKs to reflect this change.
- Adjusted tests to ensure compatibility with the new parameter naming.

* chore: openapi v3 formatted how we like it

* fix: reorder docs, update examples in README, update skills

* fix: message type option in sdk reference

* chore: update remaining getcontext and representation language

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-01-23 13:11:20 -05:00
Rajat Ahuja afa9f7b589
fix: add _total suffix to metrics (OpenMetrics convention) (#340) 2026-01-23 11:49:45 -05:00
doria dce96889bc
feat: honcho 3.0, sdks 2.0, excise stainless, update v3 docs, changelogs (#331)
* chore: 3.0 honcho and 2.0 sdks changelog

fix: use PeerContextResponse in peer.ts

* chore: move docs to /v3/, build SDKs

* chore: code review

* feat: [WIP] migrate away from stainless in typescript sdk

* chore: move api from /v2/ to /v3/

* feat: no-stainless typescript with real tests

* feat: migrate python sdk off of stainless

* feat: clean typescript sdk

* chore: add tests for ts http client

* fix: rewrite entire python sdk in new format, update typescript sdk to use `configuration` not `config` for consistency with API

* fix: clean up SDKs, synchronize

* chore: update sdk examples

* chore: update OpenAPI documentation and SDK examples to reflect changes

* fix: better test

* fix: install deps in test runner, improve robustness of streaming in sdk, coderabbit nits

* fix: standardize around camelCase in TS SDK

* refactor: update configuration handling in SDKs to use typed models for workspace, session, and peer configurations

* docs: clarify queue status usage and remove polling methods from SDKs

add claude skills for migrations

* chore: fix links in docs

* feat: add deriver flush mode to bypass batch token threshold

- Introduced `is_deriver_flush_enabled` function to check if flush mode is active.
- Updated `QueueManager` to conditionally apply batch token thresholds based on flush mode.
- Enhanced `UnifiedTestExecutor` to enable flush mode via Redis.
- Added `flush` parameter to test cases to facilitate testing of flush mode behavior.
- Updated various test cases to utilize the new flush functionality.

* feat: implement schedule_dream functionality in SDKs, use in unified test runner

- Added `schedule_dream` method to both Python and TypeScript SDKs for scheduling dream tasks.
- Updated HTTP routes to include endpoint for scheduling dreams.
- Enhanced test runner to utilize the new `schedule_dream` method for scheduling actions.
- Updated TypeScript client to support the new scheduling functionality with appropriate parameters.

* feat: update single deriver task to support multiple observers

- Changed the `observer` parameter to `observers` as a list in multiple functions across the deriver module.
- Updated the processing logic to handle multiple observers for representation tasks.
- Adjusted related payload and queue management functions to accommodate the new observers structure.
- Modified tests to reflect changes in the representation task handling and ensure proper functionality.

* refactor: update enqueue tests to support deduplication of queue items with multiple observers

- Modified tests in `test_enqueue.py` to reflect changes in the queue item structure, where each message now results in a single queue item containing a list of observers.
- Updated assertions to validate that the `observers` field correctly includes all relevant peers, ensuring proper functionality of the deduplication logic.
- Removed redundant payload matching logic to streamline test cases and improve clarity.

* fix: add backwards compatibility for representation work unit keys and payload observers

* feat: update dialectic configuration and introduce cost calculator

- Adjusted LLM and dialectic settings in `.env.template`, `config.toml.example`, and `src/config.py` to reduce maximum tool output characters and session history tokens for cost efficiency.
- Implemented a new `dialectic_cost_calculator.py` script to estimate costs based on reasoning levels and model pricing.
- Enhanced `DialecticAgent` to utilize minimal tools and adjusted output token settings based on reasoning level to optimize performance and reduce costs.

* feat: add reasoning level to chat input in unified test runner

- Enhanced the `UnifiedTestExecutor` to include a `reasoning_level` parameter in the chat method call.
- Updated the `QueryAction` model to support the new `reasoning_level` attribute, allowing for more nuanced chat interactions.

* feat: run deriver once for multiple observers (#335)

* feat: update single deriver task to support multiple observers

- Changed the `observer` parameter to `observers` as a list in multiple functions across the deriver module.
- Updated the processing logic to handle multiple observers for representation tasks.
- Adjusted related payload and queue management functions to accommodate the new observers structure.
- Modified tests to reflect changes in the representation task handling and ensure proper functionality.

* refactor: update enqueue tests to support deduplication of queue items with multiple observers

- Modified tests in `test_enqueue.py` to reflect changes in the queue item structure, where each message now results in a single queue item containing a list of observers.
- Updated assertions to validate that the `observers` field correctly includes all relevant peers, ensuring proper functionality of the deduplication logic.
- Removed redundant payload matching logic to streamline test cases and improve clarity.

* fix: add backwards compatibility for representation work unit keys and payload observers

* feat: refactor benchmark runners to share common functionality

- Introduced a new `runner_common.py` module containing shared utilities for benchmark test runners, including common argument parsing, client creation, and queue management.
- Updated `BEAMRunner`, `LoCoMoRunner`, and `LongMemEvalRunner` to inherit from `RunnerMixin`, leveraging shared functionality for metrics collection and logging.
- Added `reasoning_level` and `redis_url` parameters to runner constructors for enhanced configuration.
- Streamlined argument parsing by utilizing `add_common_arguments` for shared command-line options across all runners.

* fix: update last_user_message handling to use message content instead of ID

* fix: standardize config vs configuration

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-01-22 15:16:28 -05:00
Vineeth Voruganti 6b3ecef601
Telemetry Overhaul (#333)
* chore: Refactor a top-level telemetry folder

* fix: Add OTEL push based metrics

* chore: cleanup otel to match prometheus implementation

* fix: Remove prometheus

* feat: Scaffold CloudEvent Emitter

* chore: remove dead code

* feat: PoC Cloud Events

* chore: cleanup test

* fix: Event naming conventions and OTEL Settings

* fix: Revamped Dream Event Structure

* fix: Instrument Event Code

* fix: Add Tests for CloudEvent Telemetry

* fix: Code Rabbit Nits

* fix: Dedupe Event IDs
2026-01-21 16:49:14 -05:00
Rajat Ahuja 8c8a8c4103
fix: use separate db tx for create and save summary (#334) 2026-01-21 13:22:53 -05:00
Rajat Ahuja 833a89e70a
Turbopuffer and LanceDB Integration (#287)
* feat: init turbopuffer and lanceDB

* fix: remove destructive embedding migration

* fix: bug fixes

* fix: LanceDB

* fix: turbopuffer

* fix: search and add create_observations

* fix: use Async clients

* fix: search; protect agaainst failed vector create/delete

* fix: coderabbit comments

* fix: set up compose vector store and reconciliation loop

* feat: sync docs without embeddings

* fix: reduce batch size; comments; types; add indexes for reconciliation

* fix: add message embedding resilience

* fix: clean-up and migration test

* fix: cleanup 2

* fix: centralize retry logic; bump reconciliation batch; use tracked db; fix soft-delete race condition

* fix: skip double query when pgvector is primary

* fix: down migration

* fix: remove hard-delete from critical path and make PgVectorStore deletions a no-op

* fix: use soft-delete pattern for duplicate detection

* fix: steps toward deprecating MessageEmbedding table

* fix: remove composite and pgvector store -> make more specific

* fix: migration order

* fix: shorten reconciliation cycle + fix 'IN' equality check

* fix: coderabbit comments

* fix: add test for migration 7c0d9a4e3b1f

* feat: refactor to use ReconcilerScheduler

* fix: CR / opus comments

* fix: work unit key and reserve system workspace

* fix: make workspace_name nullable

* fix: clean up sync vectors

* fix: delete syntax

* fix: hash namespace

* External Vector Store Nits (#332)

* fix: Migration naming and long held connection

* chore: Comment for potential debt

* chore: update typescript core package

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-01-16 17:04:01 -05:00
doria b0dc05c909
feat: message ingestion forced-batching (#327)
* feat: implement forced batching for representation tasks and adjust max tokens

- Updated `REPRESENTATION_BATCH_MAX_TOKENS` to 1024 in `config.py`.
- Enhanced `get_and_claim_work_units` in `QueueManager` to enforce batching based on token thresholds.
- Added tests to ensure representation work units are only claimed when token counts meet or exceed the threshold.
- Introduced a synthesis prompt for tool execution to improve final response generation.

* chore: update queue-status docs to match new behavior

* fix: make representation work query efficient

* fix: Align alembic models and force dreams on for tests

* fix: clean queue between tests

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-01-14 14:59:35 -05:00
doria 2ef0589705
fix: keep backwards compatibility for message creation endpoint (#328) 2026-01-14 14:26:17 -05:00
doria 26da24c8bf
feat: add tool_choice to dialectic level settings, fix: `extra-high` label to `max` (#326)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 16:54:17 -05:00
doria 817123de95
fixes: update unified tests, bugs in dream scheduling (#322)
* fix: swap to reasoning config in unified test files, handle new representation output format

* fix: resolve bugs in dream scheduling -- message from a peer should cancel all dreams observing them, and document counts should be checked at moment of dream, not in enqueue. clean up and test
2026-01-13 12:33:59 -05:00
Vineeth Voruganti 5978c670de
Missing Commits from previous merge (#321)
* fix: Maintain this binding for pagination

* chore: fix tests to match core sdk

* fix: Upgrade Dockerfile and uv
2026-01-13 12:14:57 -05:00
Vineeth Voruganti ee12164835
fix: Maintain this binding for pagination (#289) 2026-01-13 11:48:18 -05:00
doria 59b203e357
Merge pull request #319 from plastic-labs/ben/renaming-and-api-cleanup-clean
feat: API renaming and cleanup
2026-01-13 10:14:38 -05:00
Vineeth Voruganti be3adf820e chore: re-add n8n 2026-01-13 10:06:50 -05:00
Benjamin McCormick 5d74818bed chore: code review, use new stainless releases 2026-01-12 18:41:34 -05:00
Benjamin McCormick 4b01f8754d feat: make dreamer and dialectic actually respect settings around using peer card 2026-01-12 18:25:35 -05:00
Benjamin McCormick 0eb0d6d427 chore: docstring cleanup 2026-01-12 17:52:05 -05:00
vintro 669b1886d6
README and docs updates (#308)
* docs: initial updates

* chore: fix broken link

* feat: add honcho-integration SKILL.md

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
Co-authored-by: Benjamin McCormick <docterformer@protonmail.com>
2026-01-12 17:44:02 -05:00
Benjamin McCormick 2c66813944 fix: use real stainless releases, update to match 2026-01-12 17:27:54 -05:00
Benjamin McCormick 5b7ae0d82c feat: API renaming and cleanup
- Rename API routes for consistency
- Add backwards-compatible conclusion and queue endpoints
- SDK cleanup and representation improvements
- Add reasoning_level param validation
- Fix thinking budget validation for Anthropic

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 16:50:40 -05:00
doria 578ef2c665
feat: agentic dreamer and agentic dialectic (#309)
* feat: add better params to working representation fetch in SDKs, return messages when added

* fix: working representation routes now accepting all parameters properly, with tests

* feat: add metadata/config fields to SDK objects where viable

* fix: tests

* feat: refactor SDKs to use representation config; [TEMP STAINLESS BUILD] update API

* feat: add representation object to sdks

* fix: use stainless sdk on branch

* fix: update TypeScript SDK tsconfig to use node16 module resolution

* fix: add isolatedModules = true to tsconfig

* fix: lol

* chore: coderabbit review

* feat: make delete session real

* feat: add observations routes with delete endpoints for documents. make session deletion real.

* chore: type cleanup

* fix: tests

* chore: coderabbit review

* fix: namespace by workspace

* feat: add ability to customize messages_per_summary at both workspace and session level

* chore: tests for summary config

* chore: coderabbit cleanup

* feat: make session and workspace config totally customizeable

* feat: add search by peer knowledge (#250)

* feat: search by peer perspective

* fix: enforce workspace in filters, make messages distinct in join

* fix: batch and merge migration steps

* fix: add refresh, add config to workspace, add refresh function, make fields readonly

* fix: search distinct

* fix: merge migrations

* fix: merge migrations

* fix: batch deletions, improve comments, limit consolidate dream to 100 docs at a time, auth on observations routes

* chore: review

* chore: coderabbit

* chore: review

* chore: broken comment

* feat: add set peer card route to API

* feat: create advanced configuration parameters with message>session>workspace hierarchy

* [wip] build unified testing harness

* chore: lint

* fix: cache invalidation, naming things, etc

* feat: longmem tests

* chore: peer config refactor

* feat: consolidate dream working, refactor representation

* fix: Various CR Comment Fixes

* feat: Allow configurable Redis port for harness instances and update cleanup methods to be asynchronous.

* feat: agentic ingestion task!!!

* feat: agentic deriver

* feat: dialectic agent and dreamer agent

* chore: browbeat tests into passing

* fix: nits

* chore: remove old code, update config files

* fix: simplify deriver

* feat: dialectic agent prompt updates, re-introduce non_agent deriver, eval tweaks

* feat: fast deriver, dreamer, then dialectic

* fix: tweaks across the board

* feat: add baseline tests

* feat: truncation in tools and client, tweaks for evals

* feat: add locomo, fix longmem judge!!!

* fix: locomo f1 is trash, use llm judge

* feat: trace creation

* feat: add first draft of obex benchmark, fix embedding model, fix locomo methodology

* fix: locomo session-optimized, better logging of cache usage and better cache usage

* chore: use openrouter for baselines

* fix: add test for merge migration

* chore: opus-powered cleanup

* fix: add config for vllm, better client

* chore: clean up clients.py a bit

* chore: move magic numbers to config, add tests for agent tools

* fix: wrong mock in dialectic tests, make ToolContext a dataclass

* feat: tweak prompts, make deriver explicit-only

* feat: more prompt & tool tweaks

* chore: more tweaks

* feat: dream with subagents

* fix: make dream trigger override scheduled, play around with dream agents

* chore: cleanup deriver

* chore: cleanup dialectic

* chore: cleanup orchestrator

* chore: comment out dream stuff, WIPing

* fix: inc temp on retry, typechecking

* feat: tweak dreaming

* feat: contradiction obs

* Add dream trees

* chore: preserve reasoning_details from openrouter in client

* fix: get_observation_context correct params

* fix: use correct message id in tool

* chore: cleanup longmem runner

* chore: clean up tests, remove dream tests for now as rearchitecting around trees

* chore: update stainless deps

* Update threholding mechanism

* chore: pre-commit hooks whitespace

* chore: clean up types

* feat: add explicit bench

* fix: address additional basepyright issues

* fix: adding logging as a fixture on honcho_llm_call and supporting dialectic loging. (#305)

* fix: lock on db for tool calls

* chore: clean up experimental derivers

* chore: coderabbit review cleanup

* feat: add streaming support to agentic dialectic

* feat: prometheus token tracking for deriver and dialectic

* fix: self-loops for isolated nodes

* chore: PascalCase for prometheus parameter typing

* feat: add reasoning levels to dialectic agent

* chore: delete old file, add new fake env vars in unittest.yml

* fix: all fields needed for dialectic reasoning level configs

* feat: track dreaming usage in prometheus

* chore: Create backwards compatabile conclusion and queue endpoints

* fix: remove redundant try-catch, add trace label, move .limit to end of statement

* fix: remove vignettes (for now), review fixes, remove merge migration, config cleanup

* chore: code review / cleanup

* chore: merge fixes

* chore: clean up, remove reasoning_focus, reintroduce peer cards in dreamers

* chore: code rabbit nitpicks

* fix: add unique index for pending dreams in queue

* fix: revert removal of surprisal in dreamer config

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
Co-authored-by: 3un01a <3un01a@plasticlabs.ai>
Co-authored-by: 3un01a <3un01a.labs@gmail.com>
Co-authored-by: ajspig <46900795+ajspig@users.noreply.github.com>
2026-01-12 15:12:17 -05:00
ajspig 49f6d75991
Adding n8n integration (#318)
* feat: Adding n8n integration

* fix: minor gramatical and style changes

* fix: matching the markdown with the json

* feat: adding screenshots to n8n

* fix: final modifications

* chore: code rabbit suggestions

* chore: code rabbit suggestions
2026-01-09 17:09:48 -05:00
doria 20e60cb6ba
chore: fix basedpyright warnings (#317) 2026-01-08 14:02:25 -05:00
Vineeth Voruganti 77551e70a1
fix: Upgrade Cashews to prevent NoScriptError (#316) 2026-01-07 15:02:45 -05:00
Rajat Ahuja d6e8c2143a
fix: summary creation if start_message_id > end_message_id (#312)
* fix: summary creation if start_message_id > end_message_id

* fix: change start_id calculation

* fix: use resolved configuration

* fix: use message_seq_in_session

* fix: clean up method

* fix: skip if latest summary on session covers through message_id
2026-01-07 14:28:21 -05:00
Rajat Ahuja 201b5125b3
feat: increase iam role duration. alter s3 file name and add to discord message (#315) 2026-01-07 13:04:13 -05:00
Rajat Ahuja acfade7d4e
feat: run unified tests suite in CI (#291)
* feat: run unified tests in CI

* fix: attempt use aws secrets manager

* fix: temp add verification workflow

* fix: CodeRabbit comments

* fix: remove debugging step

* fix: only run on main

* fix: add UNIFIED_TEST_LOG_LEVEL env var; default to WARNING
2026-01-06 16:03:29 -05:00
vintro 47776fcbc4
fix: error handling (#294) 2026-01-05 11:00:52 -05:00
Vineeth Voruganti c821eb7fce
Update MCP Worker (#311)
* fix: Update MCP Server to latest SDK version

* fix: Force primitive creation to avoid race conditions
2026-01-02 13:52:58 -05:00
Rajat Ahuja b31e9ac4b9
chore: bump redis (#302) 2025-12-30 16:45:00 -05:00
ajspig 75d1180ae1
fix: adding additional clarity when updating peercards (#304) 2025-12-18 17:34:10 -05:00
Rajat Ahuja a42252867f
fix message id range validation error (#300)
* fix: documents message_ids validation error

* feat: version bump to 2.5.1

* chore: bump core dependencies to 1.8.0
2025-12-16 11:13:01 -05:00
Eri Barrett 9c9c37f196
dashboard overview docs updates (#298)
* fix: update dialectic to chat

* fix: reasoning configuration

* fix: backlink get_context in summarizer

* fix: error handling

* chore: update dashboard overview

* fix: links due to other branches

* fix: proof

* fix: proof

---------

Co-authored-by: vintro <vince@plasticlabs.ai>
2025-12-12 16:19:14 -05:00
Paul Nodet adee668f37
docs: fix blog link in Dialectic endpoint documentation (#297)
Updated the blog link for the Dialectic endpoint documentation to the archived version.
2025-12-12 10:12:37 -05:00
doria 8dc324cb2d
Merge pull request #296 from plastic-labs/vince/dev-1307
fix: links
2025-12-10 17:20:51 -05:00
vintro 69f823aed1
fix: links 2025-12-10 17:18:32 -05:00
doria 1dc17afdcf
Merge pull request #295 from plastic-labs/ben/fix-docs-homepage
fix: force redirect on docs homepage
2025-12-10 16:40:12 -05:00
Benjamin McCormick b3327daf38 fix: force redirect on docs homepage 2025-12-10 16:39:45 -05:00
doria 971b183f11
Merge pull request #276 from plastic-labs/vince/dev-1259
V/C Docs Revamp
2025-12-10 13:06:42 -05:00
Benjamin McCormick 2b25edaf25 chore: move changes to v2.6.0-alpha 2025-12-10 13:03:56 -05:00
Benjamin McCormick 5bcde13d0f chore: docs TODOs cleanup 2025-12-10 12:13:44 -05:00
Benjamin McCormick 9950a1cfc4 Merge branch 'vince/dev-1259' of github.com:plastic-labs/honcho into vince/dev-1259 2025-12-10 11:19:33 -05:00
Benjamin McCormick 8eb6a1fc52 fix: upgrade to stainless core 1.7.0 2025-12-10 11:18:47 -05:00
Rajat Ahuja 24aa97a8e5 fix: links 2025-12-10 11:17:59 -05:00
Benjamin McCormick f0d24af827 Merge branch 'main' into vince/dev-1259 2025-12-10 10:57:40 -05:00
vintro dc297200ff
fix: description 2025-12-10 10:19:36 -05:00
vintro c53ba8dd6e
feat: draft ready for review 2025-12-09 21:24:00 -05:00
vintro 2cb07e4df4
fix: no local vs global 2025-12-09 15:55:22 -05:00
vintro ede73b5d2f
fix: another pass, todos 2025-12-08 23:18:27 -05:00
vintro d4b617f4c1
fix: various tweaks, another plan 2025-12-08 21:42:25 -05:00
vintro 42a8da201b
fix: update reasoning, various other tweaks 2025-12-08 18:21:30 -05:00
ajspig 5d2f4e150e docs: removing deriver ref in toggle-reasoning 2025-12-08 16:31:12 -05:00
ajspig 0975072b4b docs: adding clarity to representation and reasoning 2025-12-08 16:21:24 -05:00
ajspig a47da683a3 docs: removing deriver from queue-status 2025-12-08 14:27:32 -05:00
vintro 71b49f7906
Merge branch 'vince/dev-1259' of github.com:plastic-labs/honcho into vince/dev-1259 2025-12-06 14:44:35 -05:00
vintro a4f036ba6d
fix: various tweaks 2025-12-06 14:44:22 -05:00
Vineeth Voruganti 42a28dfda1 chore: Changelog updates 2025-12-05 12:22:44 -05:00
ajspig f3d5e7e4fe docs: minor textual changes 2025-12-05 12:15:37 -05:00
ajspig dfd64c765a
Mem0 -> Honcho migration (#281)
* docs: First draft of mem0 honcho migration

* docs: updating migration guide comparisons

* docs: simplifying structure and suggestions

* docs: Simplifying API comparsion

* docs: verifying code and simplfying

* docs: emphasizing the quality of honcho

* docs: minor textual changes

* docs: adding observation endpoint

* docs: coderabbit fixes and textual changes.
2025-12-05 11:53:54 -05:00
ajspig 508a212106 Merge branch 'vince/dev-1259' of https://github.com/plastic-labs/honcho into vince/dev-1259 2025-12-04 16:30:55 -05:00
ajspig e0be2d9adf docs: updating migration guide 2025-12-04 16:30:18 -05:00
vintro ba54e21d70
fix: feeling solid on core concepts 2025-12-04 16:27:06 -05:00
Vineeth Voruganti d63d580899
chore: (docs) Changelog Updates (#286)
* chore: (docs) Changelog Updates

* chore: update changelogs

* chore: nits
2025-12-04 16:07:14 -05:00
Vineeth Voruganti ca702cfd10
Resolve SDK Inconsistencies and Add Observation Creation Endpoints (#288)
* feat: Add Observation Creation Endpoints and SDK Cleanup

* fix: Resolve linting errors

* chore: Code Rabbit Nits

* chore: Code Rabbit Nits
2025-12-04 15:24:45 -05:00
Vineeth Voruganti 72eb0827ec
Add session cloning to Ergonomic SDKs (#285)
* feat: Add session cloning to Ergonomic SDKs

* chore: docs nit
2025-12-04 14:51:39 -05:00
vintro 67efb23502
fix: various tweaks 2025-12-04 12:19:20 -05:00
ajspig 984f92f0a3 docs: organizing guides. Added guides from main and migration branch. 2025-12-04 11:27:05 -05:00
vintro e5e44f4619
fix: update to main changes 2025-12-04 10:59:28 -05:00
vintro 77a240cfd6
fix: move perspective taking to representation 2025-12-04 10:21:43 -05:00
vintro 189b043e9a
fix: quick updates 2025-12-04 10:09:29 -05:00
vintro 2774c31020
fix: concept pages, reorg, first pass 2025-12-03 23:26:26 -05:00
doria e3d345b961
API/SDK updates: configurability, more parameters. Unified test harness (#283)
* feat: add better params to working representation fetch in SDKs, return messages when added

* fix: working representation routes now accepting all parameters properly, with tests

* feat: add metadata/config fields to SDK objects where viable

* fix: tests

* feat: refactor SDKs to use representation config; [TEMP STAINLESS BUILD] update API

* feat: add representation object to sdks

* fix: use stainless sdk on branch

* fix: update TypeScript SDK tsconfig to use node16 module resolution

* fix: add isolatedModules = true to tsconfig

* fix: lol

* chore: coderabbit review

* feat: make delete session real

* feat: add observations routes with delete endpoints for documents. make session deletion real.

* chore: type cleanup

* fix: tests

* chore: coderabbit review

* fix: namespace by workspace

* feat: add ability to customize messages_per_summary at both workspace and session level

* chore: tests for summary config

* chore: coderabbit cleanup

* feat: make session and workspace config totally customizeable

* feat: add search by peer knowledge (#250)

* feat: search by peer perspective

* fix: enforce workspace in filters, make messages distinct in join

* fix: batch and merge migration steps

* fix: add refresh, add config to workspace, add refresh function, make fields readonly

* fix: search distinct

* fix: merge migrations

* fix: merge migrations

* fix: batch deletions, improve comments, limit consolidate dream to 100 docs at a time, auth on observations routes

* chore: review

* chore: coderabbit

* chore: review

* chore: broken comment

* feat: add set peer card route to API

* feat: create advanced configuration parameters with message>session>workspace hierarchy

* [wip] build unified testing harness

* chore: lint

* fix: cache invalidation, naming things, etc

* feat: longmem tests

* chore: peer config refactor

* feat: consolidate dream working, refactor representation

* fix: Various CR Comment Fixes

* feat: Allow configurable Redis port for harness instances and update cleanup methods to be asynchronous.

* fix: version bump, api/sdk updates

* fix: observation endpoints, deletion queue, sdk observation implementation

* chore: Fix migration order

* fix: Use published stainless sdks

* chore: (docs) update api-reference

* fix: (docs) update based on api and sdk changes

* fix: Code Rabbit Comments

* fix: Code Rabbit Final Nits

* fix: dream scheduler

* fix: SDK model type consistency

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2025-12-03 16:49:30 -05:00
Rajat Ahuja b3e715d1d6
fix conditional cache invalidation (#280)
* fix: cleanup cache logs

* fix: address cache thrashing

* fix: warm cache for new session
2025-12-03 12:22:35 -05:00
vintro 4eab8c03ff
fix: reorder again 2025-12-02 00:36:25 -05:00
vintro 37e62c772c
feat: get context page 2025-12-01 16:31:31 -05:00
ajspig 34957d7885
Adding crewAI integration guide (#279)
* docs: adding crewAI integration guide

* docs: adding a honcho_crewai package

* docs: Using session.search and session summaries to enhance the honcho storage class

* docs: updating to use honcho_crewai package

* docs: Added honcho_crewAI tools. Updated honcho_crewai tests to better match the specific integration. Built out the package definition more.

* docs: Adding all the honcho sdk parameters to crewAI tools, also adding tools and a simple example.

* docs: adding logging to HonchoStorage class

* docs: updating mdx file to match examples and fixing explanations

* Docs: removing session summaries from search

* docs: adding files package

* docs: simplifying language specifically for theory-of-mind.

* chore: code rabbit suggestions.

* chore: code rabbit

* fix: removing nanoid crewai dependency

* docs: adding filtering capability to honcho crewai package and tool examples.

* fix: remove factory class in favor of direct class instantiation

* docs: adding hybrid memory example

* fix: fixing redundent calls to honcho for saving message history

* chore: code rabbit fixes
2025-12-01 14:46:14 -05:00
vintro 36a468e00a
fix: overview draft, rough reorg 2025-12-01 03:15:24 -05:00
ajspig 093444c38c
Removing honcho-ai/core references in API Reference where implemented by the honcho-ai package (#275)
* docs: removing honcho_core imports in SDK documentation where implemented by the honcho-ai package.

* docs: updating json import

* docs: removing duplicate openapi doc
2025-11-25 15:05:20 -05:00
vintro ace8558388
fix: 11.24 checkpoint 2025-11-24 12:33:15 -05:00
doria 4ee2f8bd0c
Merge pull request #270 from plastic-labs/vince/ai-96
feat: beam benchmark initial commit
2025-11-20 17:59:41 -05:00
Benjamin McCormick 298dbbdb85 chore: coderabbit review 2025-11-20 17:56:27 -05:00
Benjamin McCormick f333e64f12 chore: tuning beam bench 2025-11-20 17:19:14 -05:00
Vineeth Voruganti b1402c21fe
v2.4.3 Release (#278)
* chore: (docs) update changelog and version number

* chore: update for new downstream pr
2025-11-20 12:00:26 -05:00
Vineeth Voruganti a7520ce21d
Standardize DB Constraint Conventions (#272)
* fix: (db) Add standard naming conventions to SQLAlchemy Declarative Base

* chore: remove unnecessary relationships

* fix: (tests) handle old migrations being made before conventions were applied

* fix: wip migration to standardize naming for constraints

* fix (db): Migration wip)

* chore: rebase migration

* fix (db) Add migration tests

* chore: Code Review Comments

* fix: (db) fix remaining inconsistent index
2025-11-20 11:50:54 -05:00
Vineeth Voruganti 841e4bb808
Merge pull request #277 from plastic-labs/rajat/cache-invalidate-summary
fix: invalidate cache when creating summaries
2025-11-20 10:50:42 -05:00
Rajat Ahuja 47d2cb5ab7 fix: invalidate cache when creating summaries 2025-11-20 10:22:17 -05:00
vintro 90cd0b814f
feat: re-org, overview, quickstart drafts 2025-11-20 09:08:13 -05:00
Rajat Ahuja 0f1e1dec20
metrics for deriver / dialectic input + output tokens (#274)
* feat: separate deriver input / output tokens

* fix: track dialectic input / output tokens

* feat: track dialectic output tokens in streaming API

* fix: add component to metric ad change critical_analysis -> representation

* feat: track summary metrics in prometheus

* test: fix client streaming mocks

* fix: update tokenizer

* fix: add helper method; instrument peer card

* fix: count previous summary if not fallback
2025-11-19 21:20:34 -05:00
Benjamin McCormick 1616304b40 chore: fix up beam script 2025-11-19 17:56:12 -05:00
ajspig 2ffa7b7b30
Add LangGraph integration guide (#271) 2025-11-19 17:13:46 -05:00
vintro c8b0aa9cf2
fix: timeout transparency 2025-11-19 13:43:42 -05:00
vintro a88799ca3e
fix: update code to handle 10M 2025-11-19 09:47:49 -05:00
Rajat Ahuja 2945f6bb3c
rm: level_valid constraint (#266)
* rm: level_valid constraint

* fix: create new migration

* fix: normalize data before enforcing check constraint again
2025-11-17 12:56:37 -05:00
Rajat Ahuja 7e8cf4336c
fix: add test for message seq in session (#268) 2025-11-17 12:54:23 -05:00
ajspig 9b9fe46822
Abigail/dev 1242 (#269)
* Fix postgresql password config to match docker-compose

* Including more detailed local development setup

* fix: whitespace

* fix: clarifying deriver functionality

* fix: Adding additional clarify the impact of having more derivers.

* chore: PR Comments

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2025-11-17 12:22:05 -05:00
vintro 6fbb702ae4
feat: beam benchmark initial commit 2025-11-17 10:36:26 -05:00
doria 5bf1ae62c9
Merge pull request #255 from plastic-labs/ben/dev-1163
feat: add optional backup providers that kick in on retry
2025-11-13 14:57:14 -05:00
Benjamin McCormick 5226b84cf3 Merge branch 'main' into ben/dev-1163 2025-11-13 12:24:39 -05:00
Benjamin McCormick 090d871c42 chore: add comment about backup to docstring 2025-11-13 12:21:45 -05:00
doria c8d75c77df
Merge pull request #256 from plastic-labs/ben/better-dedup
feat: better dedup with configurability
2025-11-13 11:46:43 -05:00
Benjamin McCormick d58c36d100 chore: code review 2025-11-11 17:42:25 -05:00
Rajat Ahuja d4801d35a0
fix: pin cashews version to 7.4.1 (#267) 2025-11-11 12:48:29 -05:00
Rajat Ahuja d7bdcc3bc1
feat: codify queue columns (#254)
* feat: codify queue columns

* fix: batch with python loop control

* fix: cleanup merge

* fix: down revision

* fix: batch delete in migration

* feat: only run alembic tests for changed migration / test (#264)

* feat: only run alembic tests for changed migration / test
* fix: Run full test suite if alembic testing infra changes

* feat: codify times_derived + level on Document (#260)

* feat: codify times_derived + level on Document
* fix: CR comment

* fix: CodeRabbit comments

* fix: batch migrations; move types; remove fields from payload

* fix: rm duplicate table args

* fix: add messages.id FK
2025-11-07 13:22:24 -05:00
Rajat Ahuja 8e3f24d4f8
feat: add caching (redis) (#265)
* feat: init redis cache

* feat: initialize redis client

* fix: add fakeredis client

* fix: update docker compose

* fix: various coderabbit comments

* fix: update docker-compose.yml.example

* fix: CR comments 2

* fix: clean up client

* fix: cache key class; workspace LRU; constants -> utils

* fix: CodeRabbit comments

* feat: add redis to harness

* fix: use cashews

* fix: add locking for cache stampedes

* fix: clean up PR

* f: unify cache key templates and stop caching None

* fix: alter prefixes + prime cache on get_or_create()

* fix: re-add fakeredis

* chore: Code Rabbit nits

* CR comments; make client more resilient

* fix: BPR

* fix: set redis once but clear cache in between every test

* fix: attempt fix contextvar error

* fix: make TestClient rely on redis patch

* fix: potential working fix

* fix: namespace inheritance; address comments

* fix: namespace inheritance; address comments

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2025-11-07 13:04:28 -05:00
Vineeth Voruganti 641eb9991e chore: docs formatting 2025-11-07 12:05:07 -05:00
Vineeth Voruganti 822059b81a
Step Function Improvement on Docs (#241)
* feat: Release PR for v2.4.0

* chore: initial re-organization pass

* chore: in progress edits

* fix: WIP Restructure

* fix: docs introduction section

* fix: Populates new features pages

* fix: Code Rabbit grammatical catches

* chore: make system diagram visible by default
2025-11-05 17:40:53 -05:00
doria 71a6b80594
Merge pull request #258 from plastic-labs/ben/add-test-count-to-longmem
feat: add --test-count arg to longmem.py
2025-10-31 13:29:42 -04:00
Benjamin McCormick 9bdd8741a5 feat: add --test-count arg to longmem.py 2025-10-30 16:52:23 -04:00
Benjamin McCormick ca4a70f6fb fix: [critical] update prompt to
fix wording around deriving from multiple turns and remove bars

feat: add DEDUPLICATE config flag, when true, uses cosine+token similarly in tandem to dedup
2025-10-30 16:12:48 -04:00
Benjamin McCormick 0f8fc7f21c chore: remove old unuseful code 2025-10-30 14:57:25 -04:00
Benjamin McCormick 3186d2ce39 feat: add optional backup providers that kick in on retry 2025-10-29 16:53:28 -04:00
Benjamin McCormick a90156e113 feat: rework langfuse setup to work more cleanly; fix bug in dream scheduling 2025-10-29 16:10:11 -04:00
885 changed files with 214909 additions and 30736 deletions

1
.agents/skills Symbolic link
View File

@ -0,0 +1 @@
../skills

1
.claude/skills Symbolic link
View File

@ -0,0 +1 @@
../skills

View File

@ -8,6 +8,10 @@
# Application Settings # Application Settings
# ============================================================================= # =============================================================================
LOG_LEVEL=INFO LOG_LEVEL=INFO
PERFORMANCE_LOG_FORMAT=compact # compact|rich
# API server processes used by the Docker entrypoint (default: 1).
# Each process owns a separate pool when connection pooling is enabled.
# API_WORKERS=1
# SESSION_OBSERVERS_LIMIT=10 # SESSION_OBSERVERS_LIMIT=10
# GET_CONTEXT_MAX_TOKENS=100000 # GET_CONTEXT_MAX_TOKENS=100000
# MAX_FILE_SIZE=5242880 # Bytes # MAX_FILE_SIZE=5242880 # Bytes
@ -15,11 +19,24 @@ LOG_LEVEL=INFO
# Embedding settings # Embedding settings
# EMBED_MESSAGES=true # EMBED_MESSAGES=true
# MAX_EMBEDDING_TOKENS=8192 # EMBEDDING_VECTOR_DIMENSIONS=1536
# MAX_EMBEDDING_TOKENS_PER_REQUEST=300000 # EMBEDDING_MAX_INPUT_TOKENS=8192
# EMBEDDING_MAX_TOKENS_PER_REQUEST=300000
# EMBEDDING_MODEL_CONFIG__TRANSPORT=openai
# EMBEDDING_MODEL_CONFIG__MODEL=text-embedding-3-small
# EMBEDDING_MODEL_CONFIG__MAX_BATCH_SIZE=10
# EMBEDDING_MODEL_CONFIG__OVERRIDES__BASE_URL=
# EMBEDDING_MODEL_CONFIG__OVERRIDES__API_KEY_ENV=
# LANGFUSE_HOST= # LANGFUSE_HOST=
# LANGFUSE_PUBLIC_KEY= # LANGFUSE_PUBLIC_KEY=
# LANGFUSE_SECRET_KEY=
# COLLECT_METRICS_LOCAL=false
# LOCAL_METRICS_FILE=metrics.jsonl
# REASONING_TRACES_FILE=traces.jsonl # Path to JSONL file for reasoning traces
# NAMESPACE="honcho"
# ============================================================================= # =============================================================================
# Database Settings (REQUIRED) # Database Settings (REQUIRED)
@ -33,12 +50,15 @@ DB_CONNECTION_URI=postgresql+psycopg://postgres:postgres@localhost:5432/postgres
# DB_POOL_CLASS=default # DB_POOL_CLASS=default
# DB_POOL_SIZE=10 # DB_POOL_SIZE=10
# DB_MAX_OVERFLOW=20 # DB_MAX_OVERFLOW=20
# DB_POOL_TIMEOUT=30 # DB_POOL_TIMEOUT=5 # seconds a pooled checkout waits for a free connection (QueuePool only)
# DB_POOL_RECYCLE=300 # DB_POOL_RECYCLE=300
# DB_POOL_PRE_PING=true # DB_POOL_PRE_PING=true
# DB_POOL_USE_LIFO=true # DB_POOL_USE_LIFO=true
# DB_SQL_DEBUG=false # DB_SQL_DEBUG=false
# DB_TRACING=false # DB_TRACING=false
# Per-connection establish timeout (seconds) so a single connection attempt
# fails fast instead of hanging when the server/pooler is unreachable.
# DB_CONNECT_TIMEOUT_SECONDS=2
# ============================================================================= # =============================================================================
# Authentication Settings # Authentication Settings
@ -51,78 +71,179 @@ AUTH_USE_AUTH=false
# AUTH_JWT_SECRET=your-secret-key-here # AUTH_JWT_SECRET=your-secret-key-here
# ============================================================================= # =============================================================================
# LLM API Keys (REQUIRED for full functionality) # LLM Provider (REQUIRED)
# ============================================================================= # =============================================================================
# OpenAI API key for embeddings # Honcho uses LLMs for memory extraction, summarization, dialectic chat, and
LLM_OPENAI_API_KEY=your-openai-api-key-here # dream consolidation. The server will fail to start without a provider configured.
#
# Anthropic API key for dialectic and deriver functionality # Quick start: set LLM_OPENAI_API_KEY below to use the built-in defaults.
LLM_ANTHROPIC_API_KEY=your-anthropic-api-key-here # Text-generation features default to transport = "openai" and
# model = "gpt-5.4-mini". Embeddings default to transport = "openai" and
# Google API key for summarization (if using Gemini) # model = "text-embedding-3-small". For OpenAI-compatible proxies
# LLM_GEMINI_API_KEY=your-google-api-key-here # (OpenRouter, Together, Fireworks, vLLM, Ollama, LiteLLM), override
# MODEL_CONFIG__MODEL and MODEL_CONFIG__OVERRIDES__BASE_URL on each feature
# Groq API key for query generation (if using Groq) # section you want to route through that endpoint.
# LLM_GROQ_API_KEY=your-groq-api-key-here # Models must support tool calling (function calling).
#
# Base URL for OpenAI Compatible Requests if you want to use a different provider # Supported transports: openai, anthropic, gemini
# LLM_OPENAI_COMPATIBLE_BASE_URL= # Each transport picks up its API key from the corresponding LLM_*_API_KEY.
# LLM_OPENAI_COMPATIBLE_API_KEY= # Base URLs are set per-module via MODEL_CONFIG__OVERRIDES__BASE_URL.
#
LLM_OPENAI_API_KEY=your-api-key-here
# LLM_ANTHROPIC_API_KEY=
# LLM_GEMINI_API_KEY=
# ============================================================================= # =============================================================================
# LLM Configuration # LLM Configuration
# ============================================================================= # =============================================================================
# Global LLM settings # Global LLM settings
# LLM_DEFAULT_MAX_TOKENS=2500 # LLM_DEFAULT_MAX_TOKENS=2500
# LLM_MAX_TOOL_OUTPUT_CHARS=10000 # Max chars for tool output (~2500 tokens)
# LLM_MAX_MESSAGE_CONTENT_CHARS=2000 # Max chars per message in tool results
# ============================================================================= # =============================================================================
# Deriver (Background Worker) Settings # Deriver (Background Worker)
# ============================================================================= # =============================================================================
# DERIVER_ENABLED=true
# Defaults:
# DERIVER_MODEL_CONFIG__TRANSPORT=openai
# DERIVER_MODEL_CONFIG__MODEL=gpt-5.4-mini
# Optional overrides:
# DERIVER_MODEL_CONFIG__MODEL=your-model-here
# DERIVER_MODEL_CONFIG__OVERRIDES__BASE_URL=https://openrouter.ai/api/v1
# DERIVER_WORKERS=1 # DERIVER_WORKERS=1
# DERIVER_POLLING_SLEEP_INTERVAL_SECONDS=1.0 # DERIVER_POLLING_SLEEP_INTERVAL_SECONDS=1.0
# Adaptive polling: grows the idle/error sleep from the base toward the max by
# the multiplier each cycle, snapping back to base when work is found.
# DERIVER_POLLING_BACKOFF_ENABLED=true
# DERIVER_POLLING_SLEEP_MAX_INTERVAL_SECONDS=30.0
# DERIVER_POLLING_BACKOFF_MULTIPLIER=2.0
# Jitter so instances that start together don't poll in lockstep. Startup: sleep
# a random delay in [0, value] before the first poll (0.0 disables). Per-cycle:
# multiply every poll sleep by a random factor in [1-ratio, 1+ratio] (0.0 disables).
# DERIVER_POLLING_STARTUP_JITTER_SECONDS=30.0
# DERIVER_POLLING_JITTER_RATIO=0.5
# DERIVER_STALE_SESSION_TIMEOUT_MINUTES=5 # DERIVER_STALE_SESSION_TIMEOUT_MINUTES=5
# DERIVER_PROVIDER=google # DERIVER_QUEUE_ERROR_RETENTION_SECONDS=2592000 # 30 days
# DERIVER_MODEL=gemini-2.0-flash-lite # DERIVER_MODEL_CONFIG__TEMPERATURE=
# DERIVER_MAX_OUTPUT_TOKENS=2500 # DERIVER_MODEL_CONFIG__THINKING_EFFORT=minimal
# only applied when using Anthropic as provider # DERIVER_MODEL_CONFIG__THINKING_BUDGET_TOKENS=1024 # Gemini/Anthropic only
# DERIVER_THINKING_BUDGET_TOKENS=1024 # DERIVER_MODEL_CONFIG__STRUCTURED_OUTPUT_MODE=json_object # for providers without json_schema support
# DERIVER_DEDUPLICATE=true
# DERIVER_MODEL_CONFIG__MAX_OUTPUT_TOKENS=4096
# DERIVER_LOG_OBSERVATIONS=false
# DERIVER_MAX_INPUT_TOKENS=25000
# DERIVER_MAX_CUSTOM_INSTRUCTIONS_TOKENS=2000
# DERIVER_WORKING_REPRESENTATION_MAX_OBSERVATIONS=100 # DERIVER_WORKING_REPRESENTATION_MAX_OBSERVATIONS=100
# DERIVER_REPRESENTATION_BATCH_MAX_TOKENS=4096 # DERIVER_REPRESENTATION_BATCH_WORK_UNIT_TARGET_TOKENS=512 # Min tokens a work unit accumulates before the deriver claims it; 0 disables the gate
# DERIVER_MAX_INPUT_TOKENS=23000 # DERIVER_REPRESENTATION_BATCH_TARGET_INPUT_TOKENS=1024 # Max context-window tokens per deriver LLM call
# DERIVER_REPRESENTATION_BATCH_MAX_AGE_SECONDS=1800
# DERIVER_FLUSH_ENABLED=false # Bypass batch token threshold, process work immediately
# DERIVER_MODEL_CONFIG__FALLBACK__MODEL=
# DERIVER_MODEL_CONFIG__FALLBACK__TRANSPORT=
# DERIVER_MODEL_CONFIG__OVERRIDES__BASE_URL=
# DERIVER_MODEL_CONFIG__OVERRIDES__API_KEY_ENV=
# ============================================================================= # =============================================================================
# Peer Card Configuration # Peer Card
# ============================================================================= # =============================================================================
# ENABLED=true # PEER_CARD_ENABLED=true
# PROVIDER=openai
# MODEL=gpt-5-nano-2025-08-07
# MAX_OUTPUT_TOKENS=4000
# ============================================================================= # =============================================================================
# Dialectic Settings # Dialectic
# ============================================================================= # =============================================================================
# DIALECTIC_PROVIDER=anthropic # DIALECTIC_MAX_OUTPUT_TOKENS=8192
# DIALECTIC_MODEL=claude-sonnet-4-20250514 # DIALECTIC_MAX_INPUT_TOKENS=100000
# DIALECTIC_PERFORM_QUERY_GENERATION=false # DIALECTIC_HISTORY_TOKEN_LIMIT=8192
# DIALECTIC_QUERY_GENERATION_PROVIDER=groq # DIALECTIC_SESSION_HISTORY_MAX_TOKENS=4096
# DIALECTIC_QUERY_GENERATION_MODEL=llama-3.1-8b-instant #
# DIALECTIC_MAX_OUTPUT_TOKENS=2500 # Per-level settings (reasoning_level parameter in API)
# DIALECTIC_SEMANTIC_SEARCH_TOP_K=10 # Each level has its own nested MODEL_CONFIG, tool iterations, and max output tokens.
# DIALECTIC_SEMANTIC_SEARCH_MAX_DISTANCE=0.85 # MAX_OUTPUT_TOKENS is optional per level; if not set, uses global DIALECTIC_MAX_OUTPUT_TOKENS.
# DIALECTIC_THINKING_BUDGET_TOKENS=1024 # Defaults:
# DIALECTIC_CONTEXT_WINDOW_SIZE=100000 # DIALECTIC_LEVELS__minimal__MODEL_CONFIG__TRANSPORT=openai
# DIALECTIC_LEVELS__minimal__MODEL_CONFIG__MODEL=gpt-5.4-mini
# DIALECTIC_LEVELS__minimal__MAX_TOOL_ITERATIONS=1
# DIALECTIC_LEVELS__minimal__MAX_OUTPUT_TOKENS=250
# DIALECTIC_LEVELS__minimal__TOOL_CHOICE=auto
# DIALECTIC_LEVELS__low__MODEL_CONFIG__TRANSPORT=openai
# DIALECTIC_LEVELS__low__MODEL_CONFIG__MODEL=gpt-5.4-mini
# DIALECTIC_LEVELS__low__MAX_TOOL_ITERATIONS=5
# DIALECTIC_LEVELS__low__TOOL_CHOICE=auto
# DIALECTIC_LEVELS__medium__MODEL_CONFIG__TRANSPORT=openai
# DIALECTIC_LEVELS__medium__MODEL_CONFIG__MODEL=gpt-5.4-mini
# DIALECTIC_LEVELS__medium__MAX_TOOL_ITERATIONS=2
# DIALECTIC_LEVELS__high__MODEL_CONFIG__TRANSPORT=openai
# DIALECTIC_LEVELS__high__MODEL_CONFIG__MODEL=gpt-5.4-mini
# DIALECTIC_LEVELS__high__MAX_TOOL_ITERATIONS=4
# DIALECTIC_LEVELS__max__MODEL_CONFIG__TRANSPORT=openai
# DIALECTIC_LEVELS__max__MODEL_CONFIG__MODEL=gpt-5.4-mini
# DIALECTIC_LEVELS__max__MAX_TOOL_ITERATIONS=10
# Optional overrides (model and OpenAI-compatible base URL are per-level):
# DIALECTIC_LEVELS__minimal__MODEL_CONFIG__MODEL=your-model-here
# DIALECTIC_LEVELS__minimal__MODEL_CONFIG__OVERRIDES__BASE_URL=https://openrouter.ai/api/v1
# DIALECTIC_LEVELS__low__MODEL_CONFIG__MODEL=your-model-here
# DIALECTIC_LEVELS__low__MODEL_CONFIG__OVERRIDES__BASE_URL=https://openrouter.ai/api/v1
# DIALECTIC_LEVELS__medium__MODEL_CONFIG__MODEL=your-model-here
# DIALECTIC_LEVELS__medium__MODEL_CONFIG__OVERRIDES__BASE_URL=https://openrouter.ai/api/v1
# DIALECTIC_LEVELS__high__MODEL_CONFIG__MODEL=your-model-here
# DIALECTIC_LEVELS__high__MODEL_CONFIG__OVERRIDES__BASE_URL=https://openrouter.ai/api/v1
# DIALECTIC_LEVELS__max__MODEL_CONFIG__MODEL=your-model-here
# DIALECTIC_LEVELS__max__MODEL_CONFIG__OVERRIDES__BASE_URL=https://openrouter.ai/api/v1
# DIALECTIC_LEVELS__max__MODEL_CONFIG__THINKING_EFFORT=medium
# DIALECTIC_LEVELS__max__MODEL_CONFIG__THINKING_BUDGET_TOKENS=1024
# Optional backup per level (must set both or neither):
# DIALECTIC_LEVELS__max__MODEL_CONFIG__FALLBACK__MODEL=gemini-2.5-pro
# DIALECTIC_LEVELS__max__MODEL_CONFIG__FALLBACK__TRANSPORT=gemini
# ============================================================================= # =============================================================================
# Summary Settings # Summary
# ============================================================================= # =============================================================================
# SUMMARY_ENABLED=true # SUMMARY_ENABLED=true
# Defaults:
# SUMMARY_MODEL_CONFIG__TRANSPORT=openai
# SUMMARY_MODEL_CONFIG__MODEL=gpt-5.4-mini
# Optional overrides:
# SUMMARY_MODEL_CONFIG__MODEL=your-model-here
# SUMMARY_MODEL_CONFIG__OVERRIDES__BASE_URL=https://openrouter.ai/api/v1
# SUMMARY_MODEL_CONFIG__THINKING_EFFORT=minimal
# SUMMARY_MODEL_CONFIG__THINKING_BUDGET_TOKENS=1024 # Gemini/Anthropic only
# SUMMARY_MESSAGES_PER_SHORT_SUMMARY=20 # SUMMARY_MESSAGES_PER_SHORT_SUMMARY=20
# SUMMARY_MESSAGES_PER_LONG_SUMMARY=60 # SUMMARY_MESSAGES_PER_LONG_SUMMARY=60
# SUMMARY_PROVIDER=google
# SUMMARY_MODEL=gemini-1.5-flash-latest
# SUMMARY_MAX_TOKENS_SHORT=1000 # SUMMARY_MAX_TOKENS_SHORT=1000
# SUMMARY_MAX_TOKENS_LONG=2000 # SUMMARY_MAX_TOKENS_LONG=4000
# SUMMARY_THINKING_BUDGET_TOKENS=512 # SUMMARY_MODEL_CONFIG__FALLBACK__MODEL=
# =============================================================================
# Dream
# =============================================================================
# DREAM_ENABLED=true
# Defaults:
# DREAM_DEDUCTION_MODEL_CONFIG__TRANSPORT=openai
# DREAM_DEDUCTION_MODEL_CONFIG__MODEL=gpt-5.4-mini
# DREAM_INDUCTION_MODEL_CONFIG__TRANSPORT=openai
# DREAM_INDUCTION_MODEL_CONFIG__MODEL=gpt-5.4-mini
# Optional overrides:
# DREAM_DEDUCTION_MODEL_CONFIG__MODEL=your-model-here
# DREAM_DEDUCTION_MODEL_CONFIG__OVERRIDES__BASE_URL=https://openrouter.ai/api/v1
# DREAM_INDUCTION_MODEL_CONFIG__MODEL=your-model-here
# DREAM_INDUCTION_MODEL_CONFIG__OVERRIDES__BASE_URL=https://openrouter.ai/api/v1
# DREAM_DOCUMENT_THRESHOLD=50
# DREAM_IDLE_TIMEOUT_MINUTES=60
# DREAM_MIN_HOURS_BETWEEN_DREAMS=8
# DREAM_ENABLED_TYPES=["omni"]
# DREAM_MAX_TOOL_ITERATIONS=20
# DREAM_HISTORY_TOKEN_LIMIT=16384
# Surprisal sampling (advanced):
# DREAM_SURPRISAL__ENABLED=false
# DREAM_SURPRISAL__TREE_TYPE=kdtree
# DREAM_SURPRISAL__TREE_K=5
# DREAM_SURPRISAL__SAMPLING_STRATEGY=recent
# DREAM_SURPRISAL__SAMPLE_SIZE=200
# DREAM_SURPRISAL__TOP_PERCENT_SURPRISAL=0.10
# DREAM_SURPRISAL__MIN_HIGH_SURPRISAL_FOR_REPLACE=10
# DREAM_SURPRISAL__INCLUDE_LEVELS=["explicit","deductive"]
# ============================================================================= # =============================================================================
# Webhook Settings # Webhook Settings
@ -142,7 +263,72 @@ LLM_ANTHROPIC_API_KEY=your-anthropic-api-key-here
# SENTRY_PROFILES_SAMPLE_RATE=0.1 # SENTRY_PROFILES_SAMPLE_RATE=0.1
# ============================================================================= # =============================================================================
# Metrics (Optional) # Prometheus Metrics Settings (Pull-based metrics)
# ============================================================================= # =============================================================================
# ENABLED=false # METRICS_ENABLED=false
# NAMESPACE=honcho # METRICS_NAMESPACE=honcho # Inherits from NAMESPACE if not set
# =============================================================================
# CloudEvents Telemetry Settings (Analytics events)
# =============================================================================
# TELEMETRY_ENABLED=false
# TELEMETRY_ENDPOINT=https://telemetry.honcho.dev/v1/events
# TELEMETRY_HEADERS={"Authorization": "Bearer your-token"} # JSON string for auth headers
# TELEMETRY_BATCH_SIZE=100
# TELEMETRY_FLUSH_INTERVAL_SECONDS=1.0
# TELEMETRY_FLUSH_THRESHOLD=50
# TELEMETRY_MAX_RETRIES=3
# TELEMETRY_MAX_BUFFER_SIZE=10000
# TELEMETRY_NAMESPACE=honcho # Inherits from NAMESPACE if not set
# Full-fidelity payload tracing (llm.call.traced / trace.content). Default-off
# TELEMETRY_TRACE_PAYLOADS_ENABLED=false # Trace events ship to TELEMETRY_ENDPOINT
# TELEMETRY_TRACE_MAX_BYTES=262144 # Per-message cap; oversized content is clipped
# TELEMETRY_TRACE_PURPOSES=[] # JSON list of CallPurpose values to capture; empty = all
# =============================================================================
# Cache
# =============================================================================
# CACHE_ENABLED=false
# CACHE_URL="redis://localhost:6379/0?suppress=true"
# CACHE_CLUSTER=false # true when CACHE_URL is a Redis Cluster (e.g. Memorystore for Redis Cluster)
# CACHE_NAMESPACE="honcho" # Inherits from NAMESPACE if not set
# CACHE_DEFAULT_TTL_SECONDS=300
# CACHE_DEFAULT_LOCK_TTL_SECONDS=5
# CACHE_LOCK_WAIT_CHECK_INTERVAL_SECONDS=0.1
# =============================================================================
# CORS Settings
# =============================================================================
# JSON array of origins allowed by the FastAPI CORSMiddleware. Defaults match
# the previously hardcoded list: localhost, 127.0.0.1:8000 and api.honcho.dev.
# CORS_ORIGINS=["http://localhost","http://127.0.0.1:8000","https://api.honcho.dev"]
# =============================================================================
# Vector Store Settings
# =============================================================================
# Vector store type: "pgvector", "turbopuffer", or "lancedb"
VECTOR_STORE_TYPE=pgvector
# Migration flag: set to true when migration from pgvector is complete
VECTOR_STORE_MIGRATED=false
# Global namespace prefix for all vector namespaces
# Namespaces follow the pattern: {NAMESPACE}.{type}.{hash}
# where hash is a base64url-encoded SHA-256 of the workspace/peer names
# - Documents: {NAMESPACE}.doc.{hash(workspace, observer, observed)}
# - Messages: {NAMESPACE}.msg.{hash(workspace)}
# VECTOR_STORE_NAMESPACE=honcho # Inherits from NAMESPACE if not set
# Embedding dimensions are configured via EMBEDDING_VECTOR_DIMENSIONS (see top
# of this file). VECTOR_STORE_DIMENSIONS is deprecated and ignored.
# Turbopuffer-specific settings (required if TYPE is "turbopuffer")
# VECTOR_STORE_TURBOPUFFER_API_KEY=your-turbopuffer-api-key
# VECTOR_STORE_TURBOPUFFER_REGION=gcp-us-east4
# LanceDB-specific settings (local embedded mode)
# VECTOR_STORE_LANCEDB_PATH=./lancedb_data
# Reconciliation interval for background sync (default: 5 minutes)
# VECTOR_STORE_RECONCILIATION_INTERVAL_SECONDS=300

5
.gitattributes vendored Normal file
View File

@ -0,0 +1,5 @@
# Shell entrypoints are executed with sh/dash inside the container image. A
# Windows checkout with core.autocrlf=true rewrites them to CRLF, and dash
# then aborts with "set: Illegal option" because the carriage return becomes
# part of the "-e" flag argument (docker/entrypoint.sh).
*.sh text eol=lf

61
.github/CODEOWNERS vendored Normal file
View File

@ -0,0 +1,61 @@
# Code owners for Honcho.
#
# Beyond review routing, this file is the allowlist for manually triggering
# the live-llm-tests and unified-tests workflows (via their PR labels and
# workflow_dispatch). The gate jobs grep every @username in this file —
# regardless of which path pattern it sits on — and read it from `main`,
# never from the PR branch, so additions only take effect once merged.
#
# The workflow gates only understand individual @usernames (no @org/team
# entries).
#
# Order matters: GitHub applies the LAST matching pattern, so narrower rules
# go further down. Paths not listed here have no automatic reviewer.
# Telemetry, tracing, metrics.
/src/telemetry/ @akattelu @Rajat-Ahuja1997
# Data model, connections, configuration, LLM transport.
/src/db.py @akattelu @eisene
/src/models.py @akattelu @eisene
/src/config.py @akattelu @eisene
/src/cache/ @akattelu @eisene
/src/crud/ @akattelu @eisene
/migrations/ @akattelu @eisene
/src/llm/ @akattelu @eisene
# Client-facing surfaces and API shape.
/sdks/ @ajspig @akattelu
/mcp/ @ajspig @akattelu
/honcho-cli/ @ajspig @akattelu
/src/routers/ @ajspig @akattelu
/src/schemas/ @ajspig @akattelu
# The reasoning agents, their prompts, and shared agent tooling.
/src/deriver/ @eisene @akattelu
/src/dreamer/ @eisene @akattelu
/src/dialectic/ @eisene @akattelu
/src/utils/ @eisene @akattelu
# Deployment, and swappable storage and inference backends.
# /src/llm/backends/ must stay below /src/llm/ above — last match wins.
/docker/ @eisene @Rajat-Ahuja1997
/Dockerfile @eisene @Rajat-Ahuja1997
/docker-compose.yml.example @eisene @Rajat-Ahuja1997
/src/vector_store/ @eisene @Rajat-Ahuja1997
/src/llm/backends/ @eisene @Rajat-Ahuja1997
# Documentation and contributor-facing policy.
/docs/ @ajspig @akattelu
/README.md @ajspig @akattelu
/CONTRIBUTING.md @akattelu @ajspig
/SECURITY.md @Rajat-Ahuja1997 @ajspig
# Reviewers auto-requested on changes under .github/ (workflows, this file,
# templates).
/.github/ @akattelu @eisene @Rajat-Ahuja1997 @VVoruganti
# CI-trigger allowlist only: this path matches no real file, so these people
# are never auto-requested for review, but the workflow gates still pick
# them up.
/ci-trigger-allowlist @ajspig @courtlandleer @erosika @lowyelling @vintrocode

View File

@ -1,76 +0,0 @@
---
name: "🐞 Bug Report"
about: "Report an issue to help the project improve."
title: "[Bug] "
labels: "bug"
assignees: ""
---
# **🐞 Bug Report**
## **Describe the bug**
<!-- A clear and concise description of what the bug is. -->
*
---
### **Is this a regression?**
<!-- Did this behaviour used to work in the previous version? -->
<!-- Yes, the last version in which this bug was not present was: ... -->
---
### **To Reproduce**
<!-- Steps to reproduce the error:
(e.g.:)
1. Use x argument / navigate to
2. Fill this information
3. Go to...
4. See error -->
<!-- Write the steps here (add or remove as many steps as needed)-->
1.
2.
3.
4.
---
### **Expected behaviour**
<!-- A clear and concise description of what you expected to happen. -->
*
---
### **Media prove**
<!-- If applicable, add screenshots or videos to help explain your problem. -->
---
### **Your environment**
<!-- use all the applicable bulleted list elements for this specific issue,
and remove all the bulleted list elements that are not relevant for this issue. -->
* OS: <!--[e.g. Ubuntu 5.4.0-26-generic x86_64 / Windows 1904 ...]-->
* Browser name and version:
* Honcho Server Version: <!-- e.g. v0.0.8 -->
* Honcho Client Version: <!-- e.g. Python v0.0.8 -->
---
### **Additional context**
<!-- Add any other context or additional information about the problem here.-->
*
<!--📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛
To expedite issue processing, please search open and closed issues before submitting a new one.
📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛-->

72
.github/ISSUE_TEMPLATE/1-bug-report.yml vendored Normal file
View File

@ -0,0 +1,72 @@
name: Bug report
description: Something is broken or incorrect in Honcho (API, deriver, SDK, managed offering, etc.).
title: "[Bug] "
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Thanks for filing a bug. Please search [existing issues](https://github.com/plastic-labs/honcho/issues) first.
**Security vulnerability?** Do not use this form — report privately via [SECURITY.md](https://github.com/plastic-labs/honcho/blob/main/SECURITY.md).
**Memory / recall quality** (wrong or noisy conclusions, weak dialectic answers) with no crash? Prefer the **Memory / recall quality** template.
- type: dropdown
id: deploy_mode
attributes:
label: Deploy mode
description: Where are you running Honcho?
options:
- Managed (api.honcho.dev / app.honcho.dev)
- Self-hosted
- Unsure
validations:
required: true
- type: input
id: version
attributes:
label: Honcho version
description: Server image tag or release, and SDK version if you use one. Write "managed" if you are not self-hosting.
placeholder: e.g. server v2.4.1, honcho-ai 2.1.0
validations:
required: true
- type: textarea
id: description
attributes:
label: Describe the bug
description: Clear and concise description of what is wrong.
placeholder: When I…, Honcho…
validations:
required: true
- type: textarea
id: repro
attributes:
label: Steps to reproduce
description: Minimal steps or a short script/API sequence. Redact secrets, JWTs, and production user content.
placeholder: |
1. Create a session with …
2. POST /v3/... with body …
3. Observe …
validations:
required: true
- type: textarea
id: logs
attributes:
label: Logs and evidence
description: Relevant API or deriver logs or stack traces. Redact secrets and user content.
render: shell
validations:
required: false
- type: textarea
id: context
attributes:
label: Additional context
description: Config knobs, deployment notes, screenshots, related issues/PRs.
validations:
required: false

View File

@ -1,38 +0,0 @@
---
name: "💉 Failing Test"
about: "Report failing tests or CI jobs."
title: "[Test] "
labels: "Type: Test"
assignees: ""
---
# **💉 Failing Test**
## **Which jobs/test(s) are failing**
<!-- The CI jobs or tests that are failing -->
*
---
## **Reason for failure/description**
<!-- Try to describe why the test is failing or what we are missing to make it pass. -->
---
### **Media prove**
<!-- If applicable, add screenshots or videos to help explain your problem. -->
---
### **Additional context**
<!-- Add any other context or additional information about the problem here. -->
*
<!--📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛
To expedite issue processing, please search open and closed issues before submitting a new one.
📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛-->

View File

@ -0,0 +1,87 @@
name: Memory / recall quality
description: Conclusions, representations, or dialectic answers are wrong, noisy, missing, or low-quality — not a hard crash.
title: "[Quality] "
labels: ["quality"]
body:
- type: markdown
attributes:
value: |
Use this when Honcho runs without erroring, but **memory formation or recall quality** is off (bad conclusions, missed facts, weak chat answers, polluted representations, etc.).
For crashes, 5xxs, auth failures, or incorrect API mechanics, use the **Bug report** template instead.
**Do not paste production user content, full peer representations, or secrets.** Redact or invent a minimal synthetic example.
- type: dropdown
id: deploy_mode
attributes:
label: Deploy mode
options:
- Managed (api.honcho.dev / app.honcho.dev)
- Self-hosted
- Unsure
validations:
required: true
- type: input
id: version
attributes:
label: Honcho version
description: Server image tag or release, and SDK version if you use one. Write "managed" if you are not self-hosting.
placeholder: e.g. server v2.4.1, honcho-ai 2.1.0
validations:
required: true
- type: textarea
id: description
attributes:
label: What is wrong with the quality?
description: Describe the failure mode (noise, omission, contradiction, staleness, over/under-generalization, etc.).
placeholder: After ingesting messages about X, Honcho concludes Y / chat answers Z…
validations:
required: true
- type: textarea
id: repro
attributes:
label: Minimal scenario
description: >
Smallest synthetic message sequence or setup that triggers the issue.
Prefer invented names/facts over real user data. Include observer/observed
peer setup if relevant (self vs cross-peer).
placeholder: |
1. Peers: alice (user), bot (agent); session S
2. Messages ingested: …
3. Query / conclusion listing shows: …
validations:
required: true
- type: textarea
id: config
attributes:
label: Relevant config
description: >
Custom instructions, provider/model, deriver/dream settings, or workspace/peer
config that affects reasoning. Redact secrets.
placeholder: |
Provider/model: …
Custom instructions: (summary or redacted)
Other: …
validations:
required: false
- type: textarea
id: evidence
attributes:
label: Evidence
description: Redacted conclusion text, chat excerpts, or counts that show the failure. No production PII.
validations:
required: false
- type: textarea
id: context
attributes:
label: Additional context
description: Frequency, scale (message/conclusion counts), related issues, workarounds.
validations:
required: false

View File

@ -0,0 +1,58 @@
name: Feature request
description: Propose a new capability or an improvement to an existing one.
title: "[Feature] "
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
Tell us what problem you are trying to solve. Concrete use cases beat abstract wishlists.
Questions about how to use Honcho belong on [Discord](https://discord.gg/honcho), not here.
- type: dropdown
id: request_type
attributes:
label: Request type
options:
- New capability
- Improve an existing capability
- API / SDK surface
- Managed offering
- Docs / DX
- Other
validations:
required: true
- type: textarea
id: problem
attributes:
label: Problem
description: What is hard or impossible today? Who hits this?
placeholder: I'm always frustrated when… / My integration needs…
validations:
required: true
- type: textarea
id: solution
attributes:
label: Proposed solution
description: What you would like Honcho to support. Sketches and API shapes welcome.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives considered
description: Workarounds, other APIs, or designs you already tried or ruled out.
validations:
required: false
- type: textarea
id: context
attributes:
label: Additional context
description: Links, prior art, screenshots, related issues/PRs.
validations:
required: false

View File

@ -1,42 +0,0 @@
---
name: "🚀🆕 Feature Request"
about: "Suggest an idea or possible new feature for this project."
title: ""
labels: 'feature'
assignees: ''
---
# **🚀 Feature Request**
## **Is your feature request related to a problem? Please describe.**
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
*
---
## **Describe the solution you'd like**
<!-- A clear and concise description of what you want to happen. -->
*
---
## **Describe alternatives you've considered**
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
*
---
### **Additional context**
<!-- Add any other context or additional information about the problem here.-->
*
<!--📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛
To expedite issue processing, please search open and closed issues before submitting a new one.
📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛-->

View File

@ -0,0 +1,70 @@
name: Integration request
description: Add Honcho to an app store, agent framework, plugin marketplace, or other third-party surface — or improve an existing integration.
title: "[Integration] "
labels: ["integration"]
body:
- type: markdown
attributes:
value: |
Use this when you want Honcho available in (or better supported by) an external product surface — app stores, agent frameworks, plugin marketplaces, IDE extensions, MCP clients, etc.
For core API/SDK product features that are not about a third-party surface, use the **Feature request** template instead.
- type: dropdown
id: request_kind
attributes:
label: What kind of request is this?
options:
- New integration / listing (Honcho is not there yet)
- Improve an existing integration
- Official plugin / extension
- Marketplace or app-store listing
- Docs / guide for integrating with a specific tool
- Other
validations:
required: true
- type: input
id: target
attributes:
label: Target product or platform
description: Name of the app, framework, marketplace, or tool.
placeholder: e.g. Claude Code, Cursor, CrewAI, OpenClaw, VS Code Marketplace…
validations:
required: true
- type: input
id: target_url
attributes:
label: Link (if any)
description: Docs, marketplace page, repo, or product URL.
placeholder: https://…
validations:
required: false
- type: textarea
id: why
attributes:
label: Why does this matter?
description: Who would use it, and what does the integration unlock?
validations:
required: true
- type: textarea
id: shape
attributes:
label: What should the integration look like?
description: >
e.g. one-click install, MCP server listing, native memory backend,
SDK recipe, plugin with slash commands, env-var setup, etc.
Link to prior art or a sketch if you have one.
validations:
required: false
- type: textarea
id: context
attributes:
label: Additional context
description: Related issues, community demand, constraints, offers to help build it.
validations:
required: false

View File

@ -8,6 +8,10 @@ assignees: ""
--- ---
# **📚 Documentation Issue Report** # **📚 Documentation Issue Report**
**Security vulnerability?** Do not use this form — report privately via [SECURITY.md](https://github.com/plastic-labs/honcho/blob/main/SECURITY.md).
GitHub issues are public. Redact secrets, JWTs, and production user content.
## **Describe the bug** ## **Describe the bug**
<!-- A clear and concise description of what the bug is. --> <!-- A clear and concise description of what the bug is. -->
@ -33,8 +37,8 @@ assignees: ""
--- ---
### **Media prove** ### **Screenshots and videos**
<!-- If applicable, add screenshots or videos to help explain your problem. --> <!-- If applicable, add screenshots or videos to help explain your problem. Redact secrets and production content. -->
--- ---
@ -46,7 +50,7 @@ assignees: ""
--- ---
### **Additional context** ### **Additional context**
<!-- Add any other context or additional information about the problem here.--> <!-- Add any other context about the problem. Redact secrets and production user content. -->
* *

View File

@ -1,42 +0,0 @@
---
name: "🚀➕ Enhancement Request"
about: "Suggest an enhancement for this project. Improve an existing feature"
title: ""
labels: "Type: Enhancement"
assignees: ""
---
# **🚀 Enhancement Request**
## **Is your enhancement request related to a problem? Please describe.**
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
*
---
## **Describe the solution you'd like**
<!-- A clear and concise description of what you want to happen. -->
*
---
## **Describe alternatives you've considered**
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
*
---
### **Additional context**
<!-- Add any other context or additional information about the problem here.-->
*
<!--📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛
To expedite issue processing, please search open and closed issues before submitting a new one.
📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛-->

View File

@ -1,93 +0,0 @@
---
name: "⚠️ Security Report"
about: "Report an issue to help the project improve."
title: ""
labels: "security"
assignees: ""
---
<!--📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛
READ CAREFULLY IF YOUR ISSUE REPORT CONTAINS SENSIBLE OR PRIVATE DATA:
(data that might be leaked or subtracted from our servers due to this
security issue).
If this security report (or the guide on how to "identify the security bug") includes
certain personal information or involves personal identifiable data, or you believe
that the data that you might leak by exposing the way on how to attack the project
could be considered as a data leak or could violate the privacy of any kind of
data or sensible data, please do not post it here and directly email the developer:
(hello@plasticlabs.ai). You should post the issue with the least amount of
sensible or private data as possible to help us manage the security issue, and
with the extra data sent from your email to the developer (if any), we will deeply
analyze and try to fix it as fast as possible.
If you are in doubt about the data that you might post here (screenshots or media
also, count as data), please directly email us.
The data that must NOT be posted here:
* Legal and/or full names
* Names or usernames combined with other identifiers like phone numbers or email addresses
* Health or financial information (including insurance information, social security numbers, etc.)
* Information about political or religious affiliations
* Information about race, ethnicity, sexual orientation, gender, or other identifying information that could be used for discriminatory purposes
📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛-->
# **⚠️ Security Report**
## **Describe the security issue**
<!-- A clear and concise description of what the bug is. -->
*
---
### **To Reproduce**
<!-- Steps to reproduce the error:
(e.g.:)
1. Use x argument / navigate to
2. Fill this information
3. Go to...
4. See error -->
<!-- Write the steps here (add or remove as many steps as needed)-->
1.
2.
3.
4.
---
### **Expected behaviour**
<!-- A clear and concise description of what you expected to happen. -->
*
---
### **Media prove**
<!-- If applicable, add screenshots or videos to help explain your problem. -->
---
### **Your environment**
<!-- use all the applicable bulleted list elements for this specific issue,
and remove all the bulleted list elements that are not relevant for this issue. -->
* OS: <!--[e.g. Ubuntu 5.4.0-26-generic x86_64 / Windows 1904 ...]-->
* Browser name and version:
* Honcho Server Version: <!--[e.g. v0.0.1]-->
* Honcho Client Version: <!--[e.g. Python v0.0.1]-->
---
### **Additional context**
<!-- Add any other context or additional information about the problem here.-->
*

View File

@ -1,25 +0,0 @@
---
name: "❓ Question or Support Request"
about: "Questions and requests for support."
title: ""
labels: "question"
assignees: ""
---
# **❓ Question or Support Request**
## **Describe your question or ask for support.**
<!-- A clear and concise description of what your doubt is. -->
*
<!--📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛
Before posting any questions or asking for support, first read the project's README.md file and
(if there is any) the WIKI pages or any other additional documentation that might be listed
in the project's README.md file.
To expedite issue processing, please search open and closed issues before submitting a new one.
📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛📛-->

11
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: Report a security vulnerability
url: https://github.com/plastic-labs/honcho/security/advisories/new
about: Private vulnerability reporting only — do not file public security issues.
- name: Question or support
url: https://discord.gg/honcho
about: Ask the community and maintainers on Discord.
- name: Documentation
url: https://honcho.dev/docs
about: Guides, API reference, and self-hosting docs.

View File

@ -31,7 +31,7 @@ For more information on closing issues using keywords, please check https://docs
## **Changelog** ## **Changelog**
<!-- 📛📛📛📛 <!-- 📛📛📛📛
Log of changes introduced in this release in the style fo https://keepachangelog.com/en/1.1.0/ Log of changes introduced in this release in the style of https://keepachangelog.com/en/1.1.0/
📛📛📛📛 --> 📛📛📛📛 -->
### **Added** ### **Added**

View File

@ -0,0 +1,84 @@
name: Load staging secrets
description: >-
Resolve staging secret ids from the two newest v<major.minor.patch> git tags,
then load the newest fetchable secret's keys into the job environment from
AWS Secrets Manager. Falls back to the second-latest tag when the latest
tag's secret isn't published yet, and fails the job loudly when neither can
be fetched. Requires the repository to be checked out and AWS credentials to
be configured beforehand.
inputs:
secret-prefix:
description: >-
Secret-name prefix combined with a resolved tag version to form the full
secret id. Masked so it stays out of public CI logs.
required: true
runs:
using: composite
steps:
- name: Resolve secret ids from latest git tags
id: resolve-secret
shell: bash
env:
SECRET_PREFIX: ${{ inputs.secret-prefix }}
run: |
set -euo pipefail
: "${SECRET_PREFIX:?secret-prefix input is empty — is the STAGING_SECRET_PREFIX secret set for this environment?}"
# Keep the secret-name prefix out of public CI logs.
echo "::add-mask::${SECRET_PREFIX}"
# Two newest v<major.minor.patch> tags, highest first (tags are public).
versions="$(git ls-remote --tags origin 'v*' \
| sed -n 's#.*refs/tags/v\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\)$#\1#p' \
| sort -t. -k1,1nr -k2,2nr -k3,3nr -u)"
latest="$(printf '%s\n' "$versions" | sed -n '1p')"
second="$(printf '%s\n' "$versions" | sed -n '2p')"
if [ -z "${latest:-}" ]; then
echo "::error::No v<semver> git tags found to resolve a secret version"
exit 1
fi
latest_id="${SECRET_PREFIX}${latest}"
echo "::add-mask::${latest_id}"
echo "latest-id=${latest_id}" >> "$GITHUB_OUTPUT"
echo "Latest version: ${latest}"
if [ -n "${second:-}" ]; then
second_id="${SECRET_PREFIX}${second}"
echo "::add-mask::${second_id}"
echo "second-id=${second_id}" >> "$GITHUB_OUTPUT"
echo "Fallback version: ${second}"
fi
# Fetch the latest tag's secret. continue-on-error so a not-yet-published
# latest falls through to the second-latest instead of failing the job.
- name: Fetch staging secret (latest)
id: fetch-latest
continue-on-error: true
uses: aws-actions/aws-secretsmanager-get-secrets@v2
with:
secret-ids: |
,${{ steps.resolve-secret.outputs.latest-id }}
parse-json-secrets: true
# Runs only if the latest fetch failed; this one is NOT continue-on-error,
# so if the fallback also fails the job fails loudly.
- name: Fetch staging secret (fallback to second-latest)
id: fetch-fallback
if: steps.fetch-latest.outcome == 'failure' && steps.resolve-secret.outputs.second-id != ''
uses: aws-actions/aws-secretsmanager-get-secrets@v2
with:
secret-ids: |
,${{ steps.resolve-secret.outputs.second-id }}
parse-json-secrets: true
# If the latest fetch failed and the fallback was skipped (no second tag),
# no secret keys were loaded — fail here instead of letting the job run
# without staging config (e.g. every live LLM test would silently skip via
# require_provider_key and the run would go green).
- name: Verify staging secrets were loaded
if: steps.fetch-latest.outcome != 'success' && steps.fetch-fallback.outcome != 'success'
shell: bash
run: |
echo "::error::No staging secret could be fetched (latest failed; fallback skipped or failed)"
exit 1

13
.github/pull_request_template.md vendored Normal file
View File

@ -0,0 +1,13 @@
## Description
<!-- 2-3 sentences about what problem this PR solves and how -->
## Proofs
<!-- Add screenshots, logs, files as a proof that this change works -->
## Checklist
- [ ] This PR is correlated to an existing issue, and I understand it will be closed if that issue does not have the `maintainer-approved` label.
<!-- Fixes #XXX -->

286
.github/scripts/issue-gate.js vendored Normal file
View File

@ -0,0 +1,286 @@
'use strict';
/**
* Issue gate — shared logic for `.github/workflows/issue-gate.yml` (immediate
* feedback on pull request events) and `.github/workflows/pr-sweeper.yml`
* (deferred re-check, close, and stale-draft cleanup).
*
* Both workflows `require` this file through actions/github-script, so it must
* stay dependency-free: neither job runs an install step.
*
* See CONTRIBUTING.md for the policy this enforces.
*/
const REQUIRED_LABEL = 'maintainer-approved';
const GATE_LABEL = 'needs-approved-issue';
const EXEMPT_LABEL = 'gate-exempt';
const MARKER = '<!-- issue-gate -->';
const DISCORD = 'http://discord.gg/honcho';
// Hours a labelled pull request has before the sweeper closes it. Measured from
// the notice comment, so the clock starts when the author was actually told —
// not when the pull request was opened.
const GRACE_HOURS = 72;
// Days without activity before a draft from outside the org is closed.
const DRAFT_STALE_DAYS = 30;
const hasLabel = (pr, name) => (pr.labels || []).some((l) => l.name === name);
const isBot = (account) => Boolean(account) && account.type === 'Bot';
/**
* Why this pull request is exempt from the gate, or null if it is not.
*
* Single source of truth: every caller that acts on a pull request runs this.
*/
const exemptReason = async ({ github, owner, repo, pr }) => {
if (isBot(pr.user)) return 'author is a bot';
if (hasLabel(pr, EXEMPT_LABEL)) return `carries the ${EXEMPT_LABEL} label`;
const username = pr.user && pr.user.login;
if (!username) return null;
const permission = await repoPermission({ github, owner, repo, username });
if (WRITE_PERMISSIONS.includes(permission)) {
return `author has ${permission} permission`;
}
return null;
};
// Repo roles that skip the gate. `read` / `triage` do not.
const WRITE_PERMISSIONS = ['admin', 'maintain', 'write'];
/** Highest repo permission for `username`, or null if they are not a collaborator. */
async function repoPermission({ github, owner, repo, username }) {
try {
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
owner, repo, username,
});
return data.permission;
} catch (err) {
if (err && err.status === 404) return null;
throw err;
}
}
const CLOSING_ISSUES = `
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
closingIssuesReferences(first: 20) {
nodes {
number
state
labels(first: 50) { nodes { name } }
}
}
}
}
}
`;
/**
* Decide whether a pull request clears the gate.
*
* Reads GitHub's own resolved issue links rather than parsing the body, so both
* `Fixes #123` and the sidebar "Development" link count. A bare `#123` mention
* deliberately does not — that is a reference, not a claim to close.
*
* @returns {Promise<{passed: boolean, skipped?: string, issue?: number, reason?: string}>}
*/
async function checkGate({ github, owner, repo, pr }) {
if (pr.state !== 'open') return { passed: true, skipped: 'pull request is not open' };
if (pr.draft) return { passed: true, skipped: 'pull request is a draft' };
const exempt = await exemptReason({ github, owner, repo, pr });
if (exempt) return { passed: true, skipped: exempt };
const data = await github.graphql(CLOSING_ISSUES, { owner, repo, number: pr.number });
const issues = data.repository.pullRequest.closingIssuesReferences.nodes;
if (issues.length === 0) {
return { passed: false, reason: 'This pull request is not linked to an issue.' };
}
const approved = issues.find(
(i) => i.state === 'OPEN' && i.labels.nodes.some((l) => l.name === REQUIRED_LABEL),
);
if (approved) return { passed: true, issue: approved.number };
const detail = issues
.map((i) => `#${i.number} (${i.state === 'CLOSED' ? 'closed' : 'not approved'})`)
.join(', ');
return {
passed: false,
reason:
`The linked ${issues.length === 1 ? 'issue is' : 'issues are'} not open with the ` +
`\`${REQUIRED_LABEL}\` label: ${detail}.`,
};
}
function noticeBody({ owner, repo, reason }) {
return [
MARKER,
'Thanks for the contribution. This pull request does not clear our issue gate yet.',
'',
`**${reason}**`,
'',
`Every pull request to Honcho needs to be linked to an open issue carrying the \`${REQUIRED_LABEL}\` label. We do this so the review queue only holds work we have already agreed should be built — it means nobody spends time on a change we cannot merge.`,
'',
'To get this moving:',
'',
`1. Find or open an issue describing the change. [Approved issues are here](https://github.com/${owner}/${repo}/issues?q=is%3Aissue+is%3Aopen+label%3A${REQUIRED_LABEL}).`,
`2. Make the case for it in [Discord](${DISCORD}) — maintainers are most active there, and it is by far the fastest route to a decision.`,
`3. Once the issue has the label, link it: put \`Fixes #<number>\` in this pull request's description, or use **Development** in the sidebar.`,
'',
`**This will close automatically in ${GRACE_HOURS} hours if it is still unlinked.** Nothing is lost if that happens — link the issue, reopen, and it goes into the review queue.`,
'',
`See [CONTRIBUTING.md](https://github.com/${owner}/${repo}/blob/main/CONTRIBUTING.md) for the full process. If you think this is wrong, say so here and a maintainer will take a look.`,
].join('\n');
}
/**
* Every gate notice this bot posted on a pull request, oldest first.
*
* Authorship is part of the test, not decoration. MARKER is an invisible HTML
* comment, so anyone who can comment on a public repository can paste it. If
* user comments counted, a third party could post one on someone else's pull
* request: `runGate` posts a notice only when none exists, so the author would
* never be told, and `runSweep` would then measure the grace window from the
* stranger's timestamp and close them unwarned.
*/
async function findNotices({ github, owner, repo, number }) {
const comments = await github.paginate(github.rest.issues.listComments, {
owner, repo, issue_number: number, per_page: 100,
});
return comments.filter((c) => isBot(c.user) && (c.body || '').includes(MARKER));
}
/**
* Drop the gate label and delete the notice.
*
* Deleting matters: `runGate` posts a notice only when none exists, and the
* sweeper measures grace from the notice timestamp. A notice left behind after
* the gate clears would make a later re-block look weeks old and be closed with
* no warning.
*/
async function clearGate({ github, owner, repo, pr }) {
if (hasLabel(pr, GATE_LABEL)) {
await github.rest.issues
.removeLabel({ owner, repo, issue_number: pr.number, name: GATE_LABEL })
.catch(() => {});
}
for (const notice of await findNotices({ github, owner, repo, number: pr.number })) {
await github.rest.issues
.deleteComment({ owner, repo, comment_id: notice.id })
.catch(() => {});
}
}
/**
* Entry point for `.github/workflows/issue-gate.yml`.
* Labels and explains. Never closes — that is the sweeper's job.
*/
async function runGate({ github, core, context }) {
const pr = context.payload.pull_request;
const { owner, repo } = context.repo;
const result = await checkGate({ github, owner, repo, pr });
if (result.passed) {
core.info(
result.skipped ? `Skipping gate: ${result.skipped}` : `Gate passed via #${result.issue}`,
);
await clearGate({ github, owner, repo, pr });
return;
}
core.warning(`Gate failed: ${result.reason}`);
await github.rest.issues.addLabels({
owner, repo, issue_number: pr.number, labels: [GATE_LABEL],
});
const notices = await findNotices({ github, owner, repo, number: pr.number });
if (notices.length > 0) return;
await github.rest.issues.createComment({
owner, repo, issue_number: pr.number,
body: noticeBody({ owner, repo, reason: result.reason }),
});
}
/** Entry point for `.github/workflows/pr-sweeper.yml`. */
async function runSweep({ github, core, context, dryRun }) {
const { owner, repo } = context.repo;
const act = async (what, fn) => {
core.info(dryRun ? `[dry run] ${what}` : what);
if (!dryRun) await fn();
};
const close = (pr, body) => async () => {
await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body });
await github.rest.pulls.update({ owner, repo, pull_number: pr.number, state: 'closed' });
};
const prs = await github.paginate(github.rest.pulls.list, {
owner, repo, state: 'open', per_page: 100,
});
core.info(`${prs.length} open pull requests${dryRun ? ' (dry run)' : ''}`);
// Re-check everything wearing the gate label. Never close blind: a pull request
// linked through the sidebar fires no webhook, so the gate workflow cannot have
// noticed it — this pass is the only thing that will.
for (const pr of prs.filter((p) => hasLabel(p, GATE_LABEL))) {
const result = await checkGate({ github, owner, repo, pr });
if (result.passed) {
const why = result.skipped || `via #${result.issue}`;
await act(`#${pr.number}: gate now clear (${why})`, async () => {
await clearGate({ github, owner, repo, pr });
await github.rest.issues.createComment({
owner, repo, issue_number: pr.number,
body: 'The issue link is in place — this pull request has cleared the gate and is waiting on review.',
});
});
continue;
}
const [notice] = await findNotices({ github, owner, repo, number: pr.number });
if (!notice) {
core.info(`#${pr.number}: labelled but never notified — leaving it for the gate workflow`);
continue;
}
const hours = (Date.now() - Date.parse(notice.created_at)) / 3_600_000;
if (hours < GRACE_HOURS) {
core.info(`#${pr.number}: ${Math.round(GRACE_HOURS - hours)}h of grace left`);
continue;
}
await act(`#${pr.number}: closing — notified ${Math.round(hours)}h ago, still failing`, close(pr,
`Closing this: ${GRACE_HOURS} hours have passed and the gate is still not clear. This is not a judgement on the code. Link an approved issue and reopen — it goes straight into the review queue.`,
));
}
// Stale drafts. The gate skips drafts entirely, so they never carry the label;
// this pass keys off inactivity and applies the shared exemptions itself.
for (const pr of prs.filter((p) => p.draft)) {
const exempt = await exemptReason({ github, owner, repo, pr });
if (exempt) {
core.info(`#${pr.number}: leaving stale draft alone — ${exempt}`);
continue;
}
const days = (Date.now() - Date.parse(pr.updated_at)) / 86_400_000;
if (days < DRAFT_STALE_DAYS) continue;
await act(`#${pr.number}: closing stale draft — ${Math.round(days)}d without activity`, close(pr,
`Closing this draft after ${DRAFT_STALE_DAYS} days without activity, to keep the pull request list readable. Reopen whenever you pick it back up — nothing here is lost.`,
));
}
}
module.exports = {
checkGate, runGate, runSweep, noticeBody, findNotices, exemptReason,
REQUIRED_LABEL, GATE_LABEL, EXEMPT_LABEL, MARKER, GRACE_HOURS, DRAFT_STALE_DAYS,
};

161
.github/scripts/issue-gate.test.js vendored Normal file
View File

@ -0,0 +1,161 @@
'use strict';
// Self-check for the gate decision logic. No framework, no install:
// node .github/scripts/issue-gate.test.js
// Covers checkGate() only — the side-effecting halves (runGate/runSweep) are
// exercised against the real API via `pr-sweeper.yml`'s dry_run dispatch.
const assert = require('node:assert');
const {
checkGate, findNotices, runSweep, REQUIRED_LABEL, EXEMPT_LABEL, MARKER,
} = require('./issue-gate.js');
const pull = (over = {}) => ({
number: 1, state: 'open', draft: false,
user: { type: 'User', login: 'alice' }, labels: [],
...over,
});
const notCollaborator = () => {
const err = new Error('Not Found');
err.status = 404;
throw err;
};
// `linked` is the list of issues GitHub resolves as closing references.
const stub = (linked, permission) => ({
graphql: async () => ({
repository: { pullRequest: { closingIssuesReferences: {
nodes: linked.map((i) => ({
number: i.number, state: i.state || 'OPEN',
labels: { nodes: (i.labels || []).map((name) => ({ name })) },
})),
} } },
}),
rest: {
repos: {
getCollaboratorPermissionLevel: async () => {
if (!permission) return notCollaborator();
return { data: { permission } };
},
},
},
});
const run = (linked, over, permission) =>
checkGate({ github: stub(linked, permission), owner: 'o', repo: 'r', pr: pull(over) });
const cases = [
['no linked issue fails', () => run([]), (r) => r.passed === false],
['linked but unapproved fails', () => run([{ number: 7 }]), (r) => r.passed === false],
['linked and approved passes',
() => run([{ number: 7, labels: [REQUIRED_LABEL] }]),
(r) => r.passed === true && r.issue === 7],
['approved but closed fails',
() => run([{ number: 7, state: 'CLOSED', labels: [REQUIRED_LABEL] }]),
(r) => r.passed === false],
['picks the approved one out of several',
() => run([{ number: 7 }, { number: 8, labels: [REQUIRED_LABEL] }]),
(r) => r.passed === true && r.issue === 8],
// Exemptions.
['write permission skips', () => run([], {}, 'write'), (r) => r.passed === true],
['maintain permission skips', () => run([], {}, 'maintain'), (r) => r.passed === true],
['bot skips', () => run([], { user: { type: 'Bot' } }), (r) => r.passed === true],
['draft skips', () => run([], { draft: true }), (r) => r.passed === true],
[`${EXEMPT_LABEL} skips`, () => run([], { labels: [{ name: EXEMPT_LABEL }] }), (r) => r.passed === true],
['triage permission is still gated', () => run([], {}, 'triage'), (r) => r.passed === false],
['MEMBER association without write is still gated',
() => run([], { author_association: 'MEMBER' }),
(r) => r.passed === false],
['CONTRIBUTOR with write skips',
() => run([], { author_association: 'CONTRIBUTOR' }, 'write'),
(r) => r.passed === true],
];
// --- findNotices: only the bot's own notices count -------------------------
// A stranger pasting the invisible MARKER into a comment must not suppress the
// notice or become the grace-window clock.
const commentsStub = (comments) => ({
paginate: async () => comments,
rest: { issues: { listComments: null } },
});
const noticeCases = [
['a user comment carrying MARKER is not a notice',
[{ id: 1, user: { type: 'User' }, body: `sneaky ${MARKER}`, created_at: 'x' }], 0],
['a bot comment carrying MARKER is a notice',
[{ id: 2, user: { type: 'Bot' }, body: `${MARKER}\nnotice`, created_at: 'x' }], 1],
['a bot comment without MARKER is not a notice',
[{ id: 3, user: { type: 'Bot' }, body: 'unrelated', created_at: 'x' }], 0],
['a user MARKER does not mask the real bot notice',
[{ id: 4, user: { type: 'User' }, body: MARKER, created_at: 'x' },
{ id: 5, user: { type: 'Bot' }, body: MARKER, created_at: 'y' }], 1],
];
// --- runSweep: the stale-draft pass must honour every exemption ------------
const draft = (over) => ({
number: 9, draft: true, state: 'open', labels: [],
user: { type: 'User', login: 'alice' },
updated_at: new Date(Date.now() - 400 * 86400_000).toISOString(),
...over,
});
async function sweepClosed(pr, permission) {
const closed = [];
const github = {
paginate: async (route) => (route === 'pulls' ? [pr] : []),
rest: {
pulls: {
list: 'pulls',
update: async ({ pull_number }) => closed.push(pull_number),
},
issues: { listComments: 'comments', createComment: async () => {} },
repos: {
getCollaboratorPermissionLevel: async () => {
if (!permission) return notCollaborator();
return { data: { permission } };
},
},
},
};
await runSweep({
github, core: { info() {}, warning() {} },
context: { repo: { owner: 'o', repo: 'r' } }, dryRun: false,
});
return closed;
}
const sweepCases = [
['stale draft from an outside author closes', draft({}), 1],
['stale draft from a bot is left alone', draft({ user: { type: 'Bot' } }), 0],
['stale draft from a writer is left alone', draft({}), 0, 'write'],
[`stale draft with ${EXEMPT_LABEL} is left alone`, draft({ labels: [{ name: EXEMPT_LABEL }] }), 0],
['recent draft is left alone', draft({ updated_at: new Date().toISOString() }), 0],
];
(async () => {
let failed = 0;
for (const [name, comments, want] of noticeCases) {
const got = (await findNotices({ github: commentsStub(comments), owner: 'o', repo: 'r', number: 1 })).length;
if (got === want) console.log(` ok ${name}`);
else { failed++; console.log(` FAIL ${name} -> ${got} notices, wanted ${want}`); }
}
for (const [name, pr, want, permission] of sweepCases) {
const got = (await sweepClosed(pr, permission)).length;
if (got === want) console.log(` ok ${name}`);
else { failed++; console.log(` FAIL ${name} -> closed ${got}, wanted ${want}`); }
}
for (const [name, thunk, ok] of cases) {
const result = await thunk();
if (ok(result)) {
console.log(` ok ${name}`);
} else {
failed++;
console.log(` FAIL ${name} -> ${JSON.stringify(result)}`);
}
}
assert.strictEqual(failed, 0, `${failed} case(s) failed`);
console.log(`\n${cases.length + noticeCases.length + sweepCases.length} passed`);
})();

View File

@ -1,61 +0,0 @@
# See https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/
name: Fly Deploy (Production Environment)
permissions:
contents: read
on:
push:
tags:
- v*
workflow_dispatch:
inputs:
version:
description: "Version to deploy (without v prefix)"
required: true
type: string
default: 'manual'
jobs:
deploy-honcho-prod-image:
name: Deploy Honcho Image (Production Environment)
runs-on: ubuntu-latest
concurrency:
group: deploy-prod-group
cancel-in-progress: true
steps:
- uses: actions/checkout@v4
- uses: superfly/flyctl-actions/setup-flyctl@1.5
- run: |
# Determine the image label based on trigger type
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
IMAGE_LABEL="deployment-${{ github.event.inputs.version }}"
else
IMAGE_LABEL="deployment-${{ github.ref_name }}"
fi
flyctl deploy -a honcho-prod-image --remote-only --build-only --push --no-cache --image-label "$IMAGE_LABEL"
env:
FLY_API_TOKEN: ${{ secrets.FLY_PROD_API_TOKEN }}
prompt-service:
name: Push to Service (Production Environment)
needs: deploy-honcho-prod-image
runs-on: ubuntu-latest
steps:
- name: Send POST request
env:
GITHUB_REF_NAME: ${{ github.ref_name }}
run: |
# Determine version and image label based on trigger type
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
TAG="${{ github.event.inputs.version }}"
IMAGE_LABEL="honcho-prod-image:deployment-${{ github.event.inputs.version }}"
else
TAG=${GITHUB_REF_NAME#v}
IMAGE_LABEL="honcho-prod-image:deployment-${GITHUB_REF_NAME}"
fi
curl --fail -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${{ secrets.PROD_ENV_WEBHOOK_SECRET }}" \
-d "{\"version\":\"$TAG\",\"image_label\":\"$IMAGE_LABEL\"}" \
"${{ secrets.PROD_ENV_URL }}/webhooks/v1/add_honcho_version"

View File

@ -1,61 +0,0 @@
# See https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/
name: Fly Deploy (Test Environment)
permissions:
contents: read
on:
push:
tags:
- v*
workflow_dispatch:
inputs:
version:
description: "Version to deploy (without v prefix)"
required: true
type: string
default: 'manual'
jobs:
deploy-honcho-image:
name: Deploy Honcho Image (Test Environment)
runs-on: ubuntu-latest
concurrency:
group: deploy-test-group
cancel-in-progress: true
steps:
- uses: actions/checkout@v4
- uses: superfly/flyctl-actions/setup-flyctl@1.5
- run: |
# Determine the image label based on trigger type
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
IMAGE_LABEL="deployment-${{ github.event.inputs.version }}"
else
IMAGE_LABEL="deployment-${{ github.ref_name }}"
fi
flyctl deploy -a honcho-image --remote-only --build-only --push --no-cache --image-label "$IMAGE_LABEL"
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
prompt-service:
name: Push to Service (Test Environment)
runs-on: ubuntu-latest
needs: deploy-honcho-image
steps:
- name: Send POST request
env:
GITHUB_REF_NAME: ${{ github.ref_name }}
run: |
# Determine version and image label based on trigger type
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
TAG="${{ github.event.inputs.version }}"
IMAGE_LABEL="honcho-image:deployment-${{ github.event.inputs.version }}"
else
TAG=${GITHUB_REF_NAME#v}
IMAGE_LABEL="honcho-image:deployment-${GITHUB_REF_NAME}"
fi
curl --fail -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${{ secrets.TEST_ENV_WEBHOOK_SECRET }}" \
-d "{\"version\":\"$TAG\",\"image_label\":\"$IMAGE_LABEL\"}" \
"${{ secrets.TEST_ENV_URL }}/webhooks/v1/add_honcho_version"

37
.github/workflows/issue-gate.yml vendored Normal file
View File

@ -0,0 +1,37 @@
name: Issue Gate
# Labels pull requests that are not linked to an issue carrying the
# `maintainer-approved` label, and comments explaining how to fix it.
#
# This workflow never closes anything. `pr-sweeper.yml` re-checks later and closes
# only after the grace period — that gives contributors time to link an issue, and
# gives maintainers time to wave through a one-line fix. It is also the only thing
# that can notice a sidebar issue link, which fires no webhook of its own.
#
# `pull_request_target` is required so the job has write access on pull requests
# from forks. It must therefore NEVER run code from the pull request. The checkout
# below is safe because on `pull_request_target` actions/checkout defaults to the
# BASE ref, which is repo-trusted code. Never point it at `pr.head.sha`.
#
# Not triggered on `synchronize`: re-running on every push would be noise.
# Drafts are ignored until marked ready.
on:
pull_request_target:
types: [opened, edited, reopened, ready_for_review]
permissions:
contents: read
issues: write
pull-requests: write
jobs:
gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/github-script@v7
with:
script: |
const gate = require(`${process.env.GITHUB_WORKSPACE}/.github/scripts/issue-gate.js`);
await gate.runGate({ github, core, context });

124
.github/workflows/live-llm-tests.yml vendored Normal file
View File

@ -0,0 +1,124 @@
name: Live LLM Tests
on:
# Runs on main pushes that can affect the LLM transport (narrower than
# unified-tests' src/** — live provider calls aren't worth burning on
# changes that can't reach the backends).
push:
branches: [main]
paths:
- 'src/llm/**'
- 'src/config.py'
- 'tests/live_llm/**'
- 'pyproject.toml'
- 'uv.lock'
- '.python-version'
- '.github/workflows/live-llm-tests.yml'
# Manual trigger for PRs: add the `run-live-llm` label to run the suite
# against the PR's merge commit. The label is purged as soon as the run
# starts so it can be re-added to trigger another run.
pull_request:
types: [labeled]
workflow_dispatch:
# Cap spend: at most one active run per PR (per ref for push/dispatch).
# Re-triggering a PR run cancels the in-flight one instead of stacking live
# provider calls; pushes to main queue instead of cancelling so main CI
# results aren't lost.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name != 'push' }}
permissions:
contents: read
jobs:
# Only code owners (.github/CODEOWNERS) may trigger the suite manually via
# the label or workflow_dispatch; the gate also purges the trigger label so
# it can be re-added to trigger another run.
gate:
name: Gate manual trigger
permissions:
contents: read
pull-requests: write
uses: ./.github/workflows/manual-trigger-gate.yml
with:
label: run-live-llm
allow-workflow-dispatch: true
live-llm-tests:
name: Run Live LLM Tests
needs: gate
# always() lets this run on push events, where the gate's jobs are skipped.
# Manual triggers (label / workflow_dispatch) additionally require the
# gate's CODEOWNERS check to have passed.
if: >-
always() &&
(github.event_name == 'push' ||
((github.event_name == 'workflow_dispatch' ||
github.event.label.name == 'run-live-llm') &&
needs.gate.outputs.authorized == 'true'))
runs-on: ubuntu-latest
timeout-minutes: 20
environment: unified-tests
permissions:
id-token: write # Required for OIDC authentication with AWS
contents: read
env:
PYTHONUNBUFFERED: "1"
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.sha }}
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ vars.AWS_OIDC_ROLE_ARN }}
aws-region: us-east-1
role-duration-seconds: 3600
# Resolves secret ids from the newest release tags, fetches the newest
# available staging secret into the job env, and fails if none loaded.
- name: Load staging secrets
uses: ./.github/actions/load-staging-secrets
with:
secret-prefix: ${{ secrets.STAGING_SECRET_PREFIX }}
# Configure the test environment. Sentry/CloudEvents endpoints aren't
# reachable from CI. LIVE_LLM_ANTHROPIC_45_PLUS_MODELS must be set for
# the Anthropic tests to materialize — the claude_4_5_plus family has no
# default models, so with only the API key they'd silently collect as
# empty parameter sets.
- name: Configure test environment
run: |
{
# The staging dotenv carries AUTH_USE_AUTH=true without a usable
# JWT secret; src/config.py validates the pair at import time, so
# disable auth (this suite never runs the API server anyway).
echo "AUTH_USE_AUTH=false"
echo "SENTRY_ENABLED=false"
echo "TELEMETRY_ENABLED=false"
echo "LIVE_LLM_ANTHROPIC_45_PLUS_MODELS=claude-sonnet-4-5"
} >> "$GITHUB_ENV"
- name: Install uv
uses: astral-sh/setup-uv@v2
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"
- name: Install the project
run: uv sync --all-extras
# -n 0 overrides the `-n auto` xdist default from pyproject: ~15 short
# tests gain nothing from parallelism, serial execution avoids bursting
# every provider at once, and flake diagnosis gets ordered output.
- name: Run live LLM tests
run: uv run --frozen pytest tests/live_llm/ --live-llm -n 0 -v

View File

@ -0,0 +1,81 @@
name: Manual Trigger Gate
# Shared gate for workflows that can be triggered manually on PRs by adding a
# label (and optionally via workflow_dispatch): verifies the actor is a code
# owner and purges the trigger label so it can be re-added for another run.
#
# Callers must grant `pull-requests: write` on the calling job so the
# remove-label job can delete the label, and should gate downstream jobs on
# the `authorized` output rather than this workflow's conclusion.
on:
workflow_call:
inputs:
label:
description: PR label that triggers the calling workflow
required: true
type: string
allow-workflow-dispatch:
description: Whether workflow_dispatch events may pass the gate
required: false
default: false
type: boolean
outputs:
authorized:
description: >-
'true' when the manual trigger's actor passed the CODEOWNERS check.
Empty on events where the check did not run (e.g. push).
value: ${{ jobs.check-actor.outputs.authorized }}
jobs:
# Only code owners (.github/CODEOWNERS) may trigger the calling workflow
# manually.
check-actor:
name: Verify actor is a code owner
if: >-
(inputs.allow-workflow-dispatch && github.event_name == 'workflow_dispatch') ||
(github.event_name == 'pull_request' && github.event.label.name == inputs.label)
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
authorized: ${{ steps.codeowners.outputs.authorized }}
steps:
- name: Check actor against CODEOWNERS on main
id: codeowners
env:
GH_TOKEN: ${{ github.token }}
ACTOR: ${{ github.actor }}
run: |
set -euo pipefail
# Usernames are case-insensitive on GitHub; compare lowercased.
owners="$(gh api -H "Accept: application/vnd.github.raw" \
"repos/${{ github.repository }}/contents/.github/CODEOWNERS?ref=main" \
| sed 's/#.*//' | grep -oE '@[A-Za-z0-9-]+' | tr -d '@' \
| tr '[:upper:]' '[:lower:]' | sort -u)"
actor_lc="$(printf '%s' "$ACTOR" | tr '[:upper:]' '[:lower:]')"
if printf '%s\n' "$owners" | grep -qxF "$actor_lc"; then
echo "@${ACTOR} is a code owner; proceeding"
echo "authorized=true" >> "$GITHUB_OUTPUT"
else
echo "::error::@${ACTOR} is not listed in .github/CODEOWNERS on main — only code owners may trigger this workflow manually"
exit 1
fi
# Purge the trigger label first thing. Best-effort: failing to remove the
# label (e.g. read-only token on a fork PR) doesn't block the tests.
remove-label:
name: Remove trigger label
if: github.event_name == 'pull_request' && github.event.label.name == inputs.label
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Remove trigger label
env:
GH_TOKEN: ${{ github.token }}
run: |
if ! gh api --method DELETE \
"repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels/${{ inputs.label }}"; then
echo "::warning::Could not remove the ${{ inputs.label }} label (it may have been removed already)"
fi

41
.github/workflows/pr-sweeper.yml vendored Normal file
View File

@ -0,0 +1,41 @@
name: PR Sweeper
# Deferred half of the issue gate. Every six hours:
#
# 1. Re-check every pull request carrying `needs-approved-issue`. Clear the ones
# that now link an approved issue; close the ones still failing 72h after they
# were told. The re-check is the point — linking an issue through the sidebar
# fires no webhook, so `issue-gate.yml` never sees it.
# 2. Close drafts from outside the org after 30 days without activity.
#
# Runs on `schedule`, so it never touches pull request code and needs none of the
# `pull_request_target` precautions. Dispatch manually with dry_run to see what it
# would do before it does it.
on:
schedule:
- cron: '17 */6 * * *'
workflow_dispatch:
inputs:
dry_run:
description: 'Log intended actions without closing anything'
type: boolean
default: true
permissions:
contents: read
issues: write
pull-requests: write
jobs:
sweep:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/github-script@v7
env:
DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || 'false' }}
with:
script: |
const gate = require(`${process.env.GITHUB_WORKSPACE}/.github/scripts/issue-gate.js`);
await gate.runSweep({ github, core, context, dryRun: process.env.DRY_RUN === 'true' });

View File

@ -0,0 +1,78 @@
name: Build and Push to GCP Artifact Registry (production)
permissions:
contents: read
on:
push:
tags:
- v*
env:
GCP_PROJECT_ID: ${{ secrets.PROD_GCP_PROJECT_ID }}
GCP_AR_LOCATION: ${{ secrets.PROD_GCP_AR_LOCATION }}
GCP_AR_REPO: ${{ secrets.PROD_GCP_AR_REPO }}
IMAGE_NAME: ${{ secrets.PROD_IMAGE_NAME }}
GCP_SA_KEY: ${{ secrets.PROD_GCP_SA_KEY }}
jobs:
build-and-push:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Resolve and verify version
id: version
run: |
VERSION="${GITHUB_REF_NAME#v}"
# A running instance serves this version at /openapi.json, so it must
# match the version being deployed.
PYPROJECT_VERSION="$(grep -m1 '^version = ' pyproject.toml | cut -d'"' -f2)"
if [[ "$VERSION" != "$PYPROJECT_VERSION" ]]; then
echo "::error::pyproject.toml version '$PYPROJECT_VERSION' does not match tag '$GITHUB_REF_NAME'. Bump pyproject.toml before tagging."
exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Authenticate to GCP
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ env.GCP_SA_KEY }}
- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v2
- name: Configure Docker for Artifact Registry
run: gcloud auth configure-docker ${{ env.GCP_AR_LOCATION }}-docker.pkg.dev --quiet
- name: Build and push image
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
BASE="${{ env.GCP_AR_LOCATION }}-docker.pkg.dev/${{ env.GCP_PROJECT_ID }}/${{ env.GCP_AR_REPO }}/${{ env.IMAGE_NAME }}"
TAG="$BASE:deployment-v${VERSION}"
docker build -t "$TAG" .
docker push "$TAG"
prompt-service:
name: Push to Service (Production Environment)
runs-on: ubuntu-latest
needs: build-and-push
steps:
- name: Send POST request
env:
VERSION: ${{ needs.build-and-push.outputs.version }}
run: |
# Name and tag only; the registry path is supplied downstream.
IMAGE_LABEL="${{ env.IMAGE_NAME }}:deployment-v${VERSION}"
curl --fail --connect-timeout 10 --max-time 60 -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${{ secrets.PROD_ENV_WEBHOOK_SECRET }}" \
-d "{\"version\":\"$VERSION\",\"image_label\":\"$IMAGE_LABEL\"}" \
"${{ secrets.PROD_ENV_URL }}/webhooks/v1/add_honcho_version"

View File

@ -0,0 +1,78 @@
name: Build and Push to GCP Artifact Registry (Staging)
permissions:
contents: read
on:
push:
tags:
- v*
env:
GCP_PROJECT_ID: ${{ secrets.STAGING_GCP_PROJECT_ID }}
GCP_AR_LOCATION: ${{ secrets.STAGING_GCP_AR_LOCATION }}
GCP_AR_REPO: ${{ secrets.STAGING_GCP_AR_REPO }}
IMAGE_NAME: ${{ secrets.STAGING_IMAGE_NAME }}
GCP_SA_KEY: ${{ secrets.STAGING_GCP_SA_KEY }}
jobs:
build-and-push:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Resolve and verify version
id: version
run: |
VERSION="${GITHUB_REF_NAME#v}"
# A running instance serves this version at /openapi.json, so it must
# match the version being deployed.
PYPROJECT_VERSION="$(grep -m1 '^version = ' pyproject.toml | cut -d'"' -f2)"
if [[ "$VERSION" != "$PYPROJECT_VERSION" ]]; then
echo "::error::pyproject.toml version '$PYPROJECT_VERSION' does not match tag '$GITHUB_REF_NAME'. Bump pyproject.toml before tagging."
exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Authenticate to GCP
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ env.GCP_SA_KEY }}
- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v2
- name: Configure Docker for Artifact Registry
run: gcloud auth configure-docker ${{ env.GCP_AR_LOCATION }}-docker.pkg.dev --quiet
- name: Build and push image
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
BASE="${{ env.GCP_AR_LOCATION }}-docker.pkg.dev/${{ env.GCP_PROJECT_ID }}/${{ env.GCP_AR_REPO }}/${{ env.IMAGE_NAME }}"
TAG="$BASE:deployment-v${VERSION}"
docker build -t "$TAG" .
docker push "$TAG"
prompt-service:
name: Push to Service (Staging Environment)
runs-on: ubuntu-latest
needs: build-and-push
steps:
- name: Send POST request
env:
VERSION: ${{ needs.build-and-push.outputs.version }}
run: |
# Name and tag only; the registry path is supplied downstream.
IMAGE_LABEL="${{ env.IMAGE_NAME }}:deployment-v${VERSION}"
curl --fail --connect-timeout 10 --max-time 60 -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${{ secrets.TEST_ENV_WEBHOOK_SECRET }}" \
-d "{\"version\":\"$VERSION\",\"image_label\":\"$IMAGE_LABEL\"}" \
"${{ secrets.TEST_ENV_URL }}/webhooks/v1/add_honcho_version"

132
.github/workflows/start-fly-runner.yml vendored Normal file
View File

@ -0,0 +1,132 @@
name: Start Fly Runner
on:
workflow_call:
outputs:
runner-ready:
description: "Whether the runner is ready"
value: ${{ jobs.start-runner.outputs.runner-ready }}
machine-id:
description: "The Fly machine ID that was started"
value: ${{ jobs.start-runner.outputs.machine-id }}
runner-labels:
description: "Labels to target the self-hosted runner"
value: ${{ jobs.start-runner.outputs.runner-labels }}
runner-name:
description: "Resolved GitHub runner name"
value: ${{ jobs.start-runner.outputs.runner-name }}
env:
FLY_RUNNER_APP: ivysaur
FLY_RUNNER_REGION: iad
FLY_RUNNER_IMAGE: registry.fly.io/ivysaur:latest
jobs:
start-runner:
name: Start Fly Runner
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
outputs:
runner-ready: ${{ steps.wait-for-runner.outputs.ready }}
machine-id: ${{ steps.machine-management.outputs.machine-id }}
runner-labels: ${{ steps.generate-labels.outputs.labels }}
runner-name: ${{ steps.wait-for-runner.outputs.runner-name }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Generate unique runner labels
id: generate-labels
run: |
UNIQUE_LABELS='"self-hosted","${{ github.run_id }}"'
echo "Generated unique labels: $UNIQUE_LABELS"
echo "labels=$UNIQUE_LABELS" >> "$GITHUB_OUTPUT"
- name: Setup Fly CLI
uses: superfly/flyctl-actions/setup-flyctl@master
- name: Get Fly app info
id: get-app-info
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN_TESTING }}
run: |
echo "Getting app info for ${FLY_RUNNER_APP}..."
flyctl status -a "${FLY_RUNNER_APP}"
- name: Set GH_TOKEN in Fly secrets
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN_TESTING }}
run: |
echo "Setting GH_TOKEN in Fly secrets..."
flyctl secrets set GH_TOKEN="${{ secrets.GH_TOKEN_ACTIONS }}" -a "${FLY_RUNNER_APP}"
- name: Create Fly machine
id: machine-management
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN_TESTING }}
run: |
set -euo pipefail
# Always create a fresh ephemeral machine
FULL_OUTPUT=$(flyctl machines run "${FLY_RUNNER_IMAGE}" \
-a "${FLY_RUNNER_APP}" \
--region "${FLY_RUNNER_REGION}" \
--env RUN_ID=${{ github.run_id }} \
--env TEST_TYPE="honcho-unified-runner" \
--vm-size shared-cpu-8x \
--vm-memory 8192 )
MACHINE_ID=$(echo "$FULL_OUTPUT" | grep "Machine ID:" | awk '{print $3}')
echo "Created machine: $MACHINE_ID"
echo "machine-id=$MACHINE_ID" >> "$GITHUB_OUTPUT"
- name: Wait for runner to be online
id: wait-for-runner
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN_ACTIONS }}
MAX_WAIT: 420
run: |
set -euo pipefail
if [ -z "${GITHUB_TOKEN}" ]; then
echo "GH_TOKEN secret is required to poll the Actions runner API."
exit 1
fi
EXPECTED_RUNNER_NAME="honcho-unified-runner-${{ github.run_id }}"
echo "Waiting for runner named ${EXPECTED_RUNNER_NAME} to come online..."
WAITED=0
RUNNER_NAME=""
while [ $WAITED -lt $MAX_WAIT ]; do
RESPONSE=$(curl -s \
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/${{ github.repository }}/actions/runners")
if echo "$RESPONSE" | grep -q '"message"'; then
echo "API Error: $(echo "$RESPONSE" | jq -r '.message')"
exit 1
fi
# Find runner with exact name and is online and not busy
RUNNER_LINE=$(echo "$RESPONSE" | jq -r --arg runner_name "$EXPECTED_RUNNER_NAME" '.runners[]? | select(.name == $runner_name) | select(.status == "online") | select(.busy == false) | "\(.name)|\(.id)"' | head -n 1)
if [ -n "$RUNNER_LINE" ]; then
RUNNER_NAME=$(echo "$RUNNER_LINE" | cut -d'|' -f1)
echo "✅ Found runner: ${RUNNER_NAME}"
echo "ready=true" >> "$GITHUB_OUTPUT"
echo "runner-name=${RUNNER_NAME}" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "⏳ Waiting for runner... (${WAITED}s elapsed)"
sleep 15
WAITED=$((WAITED + 15))
done
echo "Runner failed to come online within ${MAX_WAIT} seconds"
echo "ready=false" >> "$GITHUB_OUTPUT"
exit 1

View File

@ -1,5 +1,9 @@
name: Static Analysis name: Static Analysis
on: [push] on:
push:
branches: [main]
pull_request:
branches: [main]
permissions: permissions:
contents: read contents: read
@ -12,7 +16,7 @@ jobs:
- name: "Set up Python" - name: "Set up Python"
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version-file: "pyproject.toml" python-version-file: ".python-version"
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@v2 uses: astral-sh/setup-uv@v2
with: with:
@ -22,3 +26,11 @@ jobs:
run: uv sync --all-extras --dev run: uv sync --all-extras --dev
- name: run basedpyright - name: run basedpyright
run: uv run basedpyright run: uv run basedpyright
issue-gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# The gate runs from `pull_request_target`, where a crash is invisible until
# a contributor's PR is silently ungated. Check it here instead.
- run: node .github/scripts/issue-gate.test.js

233
.github/workflows/unified-tests.yml vendored Normal file
View File

@ -0,0 +1,233 @@
name: Unified Tests (Fly Runner)
on:
push:
branches: [main]
paths:
- 'src/**'
- 'tests/**'
# Manual trigger for PRs: add the `run-unified-tests` label to run the suite
# against the PR's merge commit. The label is purged as soon as the run
# starts so it can be re-added to trigger another run.
pull_request:
types: [labeled]
# Cap spend: at most one active run per PR (per ref for push). Re-triggering
# a PR run cancels the in-flight one instead of stacking Fly machines; pushes
# to main queue instead of cancelling so main CI results aren't lost. The
# cleanup-machine job runs `if: always()`, which still executes on cancelled
# runs, so a cancelled run's Fly machine and runner are still torn down.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name != 'push' }}
permissions:
contents: read
actions: read
jobs:
# Only code owners (.github/CODEOWNERS) may trigger the suite manually via
# the label; the gate also purges the trigger label so it can be re-added
# to trigger another run.
gate:
name: Gate manual trigger
permissions:
contents: read
pull-requests: write
uses: ./.github/workflows/manual-trigger-gate.yml
with:
label: run-unified-tests
start-runner:
name: Start Fly Runner
needs: gate
# always() lets this run on push events, where the gate's jobs are skipped.
# Label adds other than run-unified-tests trigger the workflow but skip
# every job here; the run-unified-tests label additionally requires the
# gate's CODEOWNERS check to have passed.
if: >-
always() &&
(github.event_name == 'push' ||
(github.event.label.name == 'run-unified-tests' &&
needs.gate.outputs.authorized == 'true'))
uses: ./.github/workflows/start-fly-runner.yml
secrets: inherit
unified-tests:
name: Run Unified Tests
runs-on: ${{ fromJSON(format('[{0}]', needs.start-runner.outputs.runner-labels)) }}
needs: start-runner
# !cancelled() so this doesn't inherit gate's skip on push events.
if: >-
!cancelled() &&
needs.start-runner.outputs.runner-ready == 'true'
timeout-minutes: 90
environment: unified-tests
permissions:
id-token: write # Required for OIDC authentication with AWS
contents: read
env:
PYTHONUNBUFFERED: "1"
TEST_DISCORD_WEBHOOK_URL: ${{ secrets.TEST_DISCORD_WEBHOOK_URL }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.sha }}
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ vars.AWS_OIDC_ROLE_ARN }}
aws-region: us-east-1
role-duration-seconds: 43200 # 12 hours
# Resolves secret ids from the newest release tags, fetches the newest
# available staging secret into the job env, and fails if none loaded.
- name: Load staging secrets
uses: ./.github/actions/load-staging-secrets
with:
secret-prefix: ${{ secrets.STAGING_SECRET_PREFIX }}
# Layer test-specific overrides on top of the staging secret. The staging
# dotenv tracks the deployed release and can drift from what main's config
# expects; the TESTING_SECRET_ID secret holds only the keys (flat JSON,
# exact env var names) the unified tests need to pin. The get-secrets
# action refuses to inject an env var that already exists, so the
# overrides are fetched under a prefix alias here and promoted over the
# staging values in the next step.
- name: Fetch testing secret overrides
uses: aws-actions/aws-secretsmanager-get-secrets@v2
with:
secret-ids: |
HONCHO_TEST_OVERRIDE,${{ secrets.TESTING_SECRET_ID }}
parse-json-secrets: true
# Re-export each HONCHO_TEST_OVERRIDE_* var under its real name; the
# later $GITHUB_ENV write wins over the value loaded from the staging
# secret. Values are already masked by the fetch step above.
- name: Apply testing secret overrides
run: |
set -euo pipefail
applied=0
while IFS= read -r -d '' entry; do
name="${entry%%=*}"
value="${entry#*=}"
case "$name" in
HONCHO_TEST_OVERRIDE_*)
target="${name#HONCHO_TEST_OVERRIDE_}"
{
echo "${target}<<__HONCHO_OVERRIDE_EOF__"
printf '%s\n' "$value"
echo "__HONCHO_OVERRIDE_EOF__"
} >> "$GITHUB_ENV"
echo "Overriding ${target}"
applied=$((applied + 1))
;;
esac
done < <(env -0)
echo "Applied ${applied} override(s)"
# Configure the test environment. Disables auth/Sentry/CloudEvents telemetry
# (their endpoints aren't reachable from CI), and points REASONING_TRACES_FILE
# at a shared path so the API + deriver record full LLM I/O for auditing — the
# runner uploads it to S3. Written after the fetch steps so these win over the
# values loaded from Secrets Manager (last $GITHUB_ENV write wins). Stale
# config keys loaded from the staging secret (e.g. settings that have since
# been renamed or removed on main) must always be ignored by the app config.
- name: Configure test environment
run: |
{
echo "AUTH_USE_AUTH=false"
echo "SENTRY_ENABLED=false"
echo "TELEMETRY_ENABLED=false"
echo "REASONING_TRACES_FILE=unified-reasoning-traces.jsonl"
} >> "$GITHUB_ENV"
- name: Verify Docker is available
run: docker info
- name: Verify uv and Python
run: |
uv --version
python3.13 --version
which python3.13
- name: Install the project
run: uv sync --all-extras
- name: Run unified tests
run: uv run python -m tests.unified.run
cleanup-machine:
name: Cleanup Fly Machine and Runner
runs-on: ubuntu-latest
needs: [start-runner, unified-tests]
if: always() && needs.start-runner.outputs.machine-id != ''
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN_TESTING }}
GITHUB_TOKEN: ${{ secrets.GH_TOKEN_ACTIONS }}
FLY_RUNNER_APP: ivysaur
steps:
- name: Setup Fly CLI
uses: superfly/flyctl-actions/setup-flyctl@1.5
- name: Cleanup fly machine
run: |
set -euo pipefail
MACHINE_ID="${{ needs.start-runner.outputs.machine-id }}"
if [ -z "$MACHINE_ID" ]; then
echo "No machine ID provided, skipping Fly cleanup."
exit 0
fi
echo "🧹 Cleaning up machine: $MACHINE_ID"
flyctl machines stop "$MACHINE_ID" -a "$FLY_RUNNER_APP" || echo "Machine may already be stopped"
flyctl machines destroy "$MACHINE_ID" -a "$FLY_RUNNER_APP" --force || echo "Failed to destroy machine"
- name: Cleanup GitHub runner
run: |
set -euo pipefail
RUNNER_NAME="${{ needs.start-runner.outputs.runner-name }}"
FALLBACK_LABEL="${{ github.run_id }}"
echo "🗑️ Cleaning up GitHub runner (name: ${RUNNER_NAME:-unknown}, label: ${FALLBACK_LABEL})"
RUNNERS_RESPONSE=$(curl -s \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/${{ github.repository }}/actions/runners")
if echo "$RUNNERS_RESPONSE" | grep -q '"message"'; then
echo "⚠️ Failed to fetch runners: $(echo "$RUNNERS_RESPONSE" | jq -r '.message')"
exit 0
fi
RUNNER_ID=""
if [ -n "$RUNNER_NAME" ]; then
RUNNER_ID=$(echo "$RUNNERS_RESPONSE" | jq -r --arg name "$RUNNER_NAME" '.runners[]? | select(.name == $name) | .id')
fi
if [ -z "$RUNNER_ID" ]; then
RUNNER_ID=$(echo "$RUNNERS_RESPONSE" | jq -r --arg label "$FALLBACK_LABEL" '.runners[]? | select([.labels[].name] | index($label)) | .id' | head -n 1)
fi
if [ -z "$RUNNER_ID" ] || [ "$RUNNER_ID" = "null" ]; then
echo "⚠️ Runner not found, nothing to delete."
exit 0
fi
DELETE_RESPONSE=$(curl -s -w "%{http_code}" \
-X DELETE \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GITHUB_TOKEN" \
"https://api.github.com/repos/${{ github.repository }}/actions/runners/$RUNNER_ID")
HTTP_CODE="${DELETE_RESPONSE: -3}"
if [ "$HTTP_CODE" = "204" ]; then
echo "✅ Successfully deleted runner."
else
echo "⚠️ Failed to delete runner. HTTP code: $HTTP_CODE"
echo "Response: ${DELETE_RESPONSE%???}"
fi

View File

@ -11,6 +11,7 @@ on:
- '**.jsx' - '**.jsx'
- 'pyproject.toml' - 'pyproject.toml'
- 'uv.lock' - 'uv.lock'
- '.python-version'
- 'sdks/typescript/package.json' - 'sdks/typescript/package.json'
- 'sdks/typescript/bun.lock' - 'sdks/typescript/bun.lock'
- '.github/workflows/unittest.yml' - '.github/workflows/unittest.yml'
@ -24,6 +25,7 @@ on:
- '**.jsx' - '**.jsx'
- 'pyproject.toml' - 'pyproject.toml'
- 'uv.lock' - 'uv.lock'
- '.python-version'
- 'sdks/typescript/package.json' - 'sdks/typescript/package.json'
- 'sdks/typescript/bun.lock' - 'sdks/typescript/bun.lock'
- '.github/workflows/unittest.yml' - '.github/workflows/unittest.yml'
@ -38,7 +40,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs: outputs:
python: ${{ steps.filter.outputs.python }} python: ${{ steps.filter.outputs.python }}
typescript: ${{ steps.filter.outputs.typescript }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: dorny/paths-filter@v3 - uses: dorny/paths-filter@v3
@ -49,9 +50,8 @@ jobs:
- '**.py' - '**.py'
- 'pyproject.toml' - 'pyproject.toml'
- 'uv.lock' - 'uv.lock'
- '.python-version'
- 'migrations/**' - 'migrations/**'
- '.github/workflows/unittest.yml'
typescript:
- 'sdks/typescript/**' - 'sdks/typescript/**'
- '.github/workflows/unittest.yml' - '.github/workflows/unittest.yml'
@ -88,7 +88,14 @@ jobs:
- name: "Set up Python" - name: "Set up Python"
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version-file: "pyproject.toml" python-version-file: ".python-version"
- name: Install bun
uses: oven-sh/setup-bun@v2
- name: Install TypeScript SDK dependencies
run: bun install
working-directory: sdks/typescript
- name: Install the project - name: Install the project
run: uv sync --all-extras --dev run: uv sync --all-extras --dev
@ -101,46 +108,40 @@ jobs:
SENTRY_ENABLED: false SENTRY_ENABLED: false
LLM_OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} LLM_OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
LLM_ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} LLM_ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
LLM_OPENAI_COMPATIBLE_API_KEY: test-key
LLM_OPENAI_COMPATIBLE_BASE_URL: http://localhost:8000
DERIVER_PROVIDER: openai DERIVER_PROVIDER: openai
DERIVER_MODEL: test DERIVER_MODEL: test
DIALECTIC_PROVIDER: openai DIALECTIC_LEVELS__minimal__PROVIDER: openai
DIALECTIC_MODEL: test DIALECTIC_LEVELS__minimal__MODEL: test
DIALECTIC_LEVELS__minimal__THINKING_BUDGET_TOKENS: 0
DIALECTIC_LEVELS__minimal__MAX_TOOL_ITERATIONS: 2
DIALECTIC_LEVELS__low__PROVIDER: openai
DIALECTIC_LEVELS__low__MODEL: test
DIALECTIC_LEVELS__low__THINKING_BUDGET_TOKENS: 0
DIALECTIC_LEVELS__low__MAX_TOOL_ITERATIONS: 5
DIALECTIC_LEVELS__medium__PROVIDER: openai
DIALECTIC_LEVELS__medium__MODEL: test
DIALECTIC_LEVELS__medium__THINKING_BUDGET_TOKENS: 0
DIALECTIC_LEVELS__medium__MAX_TOOL_ITERATIONS: 4
DIALECTIC_LEVELS__high__PROVIDER: openai
DIALECTIC_LEVELS__high__MODEL: test
DIALECTIC_LEVELS__high__THINKING_BUDGET_TOKENS: 0
DIALECTIC_LEVELS__high__MAX_TOOL_ITERATIONS: 4
DIALECTIC_LEVELS__max__PROVIDER: openai
DIALECTIC_LEVELS__max__MODEL: test
DIALECTIC_LEVELS__max__THINKING_BUDGET_TOKENS: 0
DIALECTIC_LEVELS__max__MAX_TOOL_ITERATIONS: 10
DIALECTIC_QUERY_GENERATION_PROVIDER: openai DIALECTIC_QUERY_GENERATION_PROVIDER: openai
DIALECTIC_QUERY_GENERATION_MODEL: test DIALECTIC_QUERY_GENERATION_MODEL: test
SUMMARY_PROVIDER: openai SUMMARY_PROVIDER: openai
SUMMARY_MODEL: test SUMMARY_MODEL: test
test-typescript:
needs: changes
if: ${{ needs.changes.outputs.typescript == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Install TypeScript SDK dependencies
run: |
cd sdks/typescript
bun install
- name: Run TypeScript SDK tests
run: |
cd sdks/typescript
bun run test
env:
HONCHO_API_KEY: test-key
HONCHO_BASE_URL: http://localhost:8000
# Status check for branch protection rules # Status check for branch protection rules
# This job always runs and reports success only if all required jobs pass # This job always runs and reports success only if all required jobs pass
test-status: test-status:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [changes, test-python, test-typescript] needs: [changes, test-python]
if: always() if: always()
steps: steps:
- name: Check test results - name: Check test results
@ -149,8 +150,4 @@ jobs:
echo "Python tests failed or were cancelled" echo "Python tests failed or were cancelled"
exit 1 exit 1
fi fi
if [[ "${{ needs.changes.outputs.typescript }}" == "true" && "${{ needs.test-typescript.result }}" != "success" && "${{ needs.test-typescript.result }}" != "skipped" ]]; then
echo "TypeScript tests failed or were cancelled"
exit 1
fi
echo "All required tests passed!" echo "All required tests passed!"

12
.gitignore vendored
View File

@ -1,11 +1,13 @@
.worktrees/
api/**/*.db api/**/*.db
api/data api/data
api/docker-compose.yml api/docker-compose.yml
*.db *.db
data data
docker-compose.yml redis-data
compose.yml /docker-compose.yml
/compose.yml
@ -180,6 +182,7 @@ docs/node_modules
timing_logs.csv timing_logs.csv
config.json
config.toml config.toml
.aider* .aider*
@ -188,3 +191,8 @@ CRUSH.md
metrics.jsonl metrics.jsonl
AGENTS.md AGENTS.md
lancedb_data/
grafana-data/
# Claude Code addon stuff
.omc

View File

@ -60,12 +60,12 @@ repos:
language: system language: system
files: ^(src/|tests/|sdks/python/|scripts/).*\.py$ files: ^(src/|tests/|sdks/python/|scripts/).*\.py$
require_serial: true require_serial: true
pass_filenames: false pass_filenames: true
# Run main application tests # Run main application tests
- id: pytest-main - id: pytest-main
name: pytest (main app) name: pytest (main app)
entry: uv run pytest tests/ --ignore=tests/alembic/ entry: uv run pytest -x tests/ --ignore=tests/alembic/
language: system language: system
files: ^(src/|tests/).*\.py$ files: ^(src/|tests/).*\.py$
stages: [pre-push] stages: [pre-push]
@ -74,11 +74,11 @@ repos:
# Run Alembic tests only when migrations change # Run Alembic tests only when migrations change
- id: pytest-alembic - id: pytest-alembic
name: pytest (alembic migrations) name: pytest (alembic migrations)
entry: uv run pytest tests/alembic/ entry: uv run python scripts/run_alembic_tests.py
language: system language: system
files: ^(migrations/.*\.py|tests/alembic/.*\.py)$ files: ^(migrations/versions/.*\.py|tests/alembic/.*\.py)$
stages: [pre-push] stages: [pre-push]
pass_filenames: false pass_filenames: true
require_serial: true require_serial: true
# Ensure each alembic migration revision has a corresponding test file # Ensure each alembic migration revision has a corresponding test file
@ -99,10 +99,10 @@ repos:
stages: [pre-push] stages: [pre-push]
pass_filenames: false pass_filenames: false
# TypeScript build/test with bun # TypeScript build with bun (tests run via pytest)
- id: typescript-check - id: typescript-check
name: TypeScript build and test name: TypeScript build
entry: bash -c 'if [ -f "sdks/typescript/package.json" ]; then cd sdks/typescript && bun run build && bun run test; fi' entry: bash -c 'if [ -f "sdks/typescript/package.json" ]; then cd sdks/typescript && bun run build; fi'
language: system language: system
files: ^sdks/typescript/.*\.(js|ts|jsx|tsx|json)$ files: ^sdks/typescript/.*\.(js|ts|jsx|tsx|json)$
stages: [pre-push] stages: [pre-push]

View File

@ -1 +1 @@
3.11 3.13

View File

@ -5,6 +5,459 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/) The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/). and this project adheres to [Semantic Versioning](http://semver.org/).
## [3.1.1] - 2026-09-02
### Changed
- Server `requires-python` is `>=3.13`, matching the production image. Self-hosters on 3.10–3.12 need to upgrade; SDK and CLI floors are unchanged (#1090)
### Fixed
- Concurrent `create_documents` writers to the same collection deadlocked on `times_derived` reinforcement UPDATEs issued in batch order; the error was swallowed per-document, the batch was lost, and the queue item was marked processed. Writers now lock target rows with `SELECT ... ORDER BY id FOR UPDATE` before applying, abort the batch on `SQLAlchemyError` instead of continuing through a dead session, and retry transient errors (deadlock, serialization failure, lock/statement timeout, lost connection) up to `MAX_RETRYABLE_ATTEMPTS` instead of burning the item (#1033)
- Scope backfill no longer embeds, writes, and syncs every planned copy at once. A 14k-document session is ~580MB of vectors; several concurrent backfills OOM-killed the deriver at its 1000Mi limit and crash-looped because the work units never completed. Phases 2–4 now run per chunk of 500 specs, reload source embeddings per chunk, and drop them once synced. Membership is locked across chunk writes so a concurrent leave cannot commit between the check and the inserts (#1104)
- Model-generated observations with NUL bytes (`\u0000`) no longer fail the exact-content dedup pre-fetch with a Postgres `DataError` that dropped the whole observer batch. Ingress already stripped NUL from user content; the deriver now strips it so stored text matches embedded text. All-NUL content is dropped rather than stored empty (#1095)
- `search_messages` no longer forwards `top_k=0` to Turbopuffer (which requires 1..10000). Zero/negative limits short-circuit to empty results; tool limits are floored at 1. The documents path was already guarded (#970); this closes the message path (#1084)
- OpenAI-compatible tool-call turns with `content=null` keep null through history replay instead of being coerced to `""`. Providers that bind reasoning state to the exact assistant message shape were breaking on the empty string. Tool-less null still becomes `""` (#1064)
- The production image now ships `pyproject.toml` in the runtime stage, so the service reports its real version instead of `unknown` in OpenAPI and telemetry (#1074)
## [3.1.0] - 2026-08-25
### Added
- Scopes: a named grouping of sessions that acts as a visibility boundary on recall, implemented as a facade over an observer peer (`scope.{name}` with `{"kind": "scope"}`). Developers manage them exclusively through `/v3/workspaces/{workspace_id}/scopes` (create-or-get, list, get, add/list/remove session membership) and an optional `scopes` field on session create — never through the observer/observed mechanics. Scope peers cannot author messages, cannot be a chat or representation `target`, are excluded from `peers.list` by default (`PeerGet.kind` = `"scope"` / `"all"` switches the view), and are rejected on the generic session-peer routes. Workspace-level key required; peer- and session-scoped keys get 401. Legacy peers occupying a reserved `scope.` name without the kind flag are refused with 409, never adopted (#884)
- `scope` read option on chat, representation, session context, and workspace search. A single scope swaps the observer to the backing scope peer so conclusion recall, peer cards, and message tools stay inside that scope's membership. A list of scopes takes the union of member sessions (capped at `MAX_SESSION_ALLOWLIST_ENTRIES`) and executes via the session-allowlist path. Empty scopes fail closed. `scope` is mutually exclusive with `filters` and `session_id`. Workspace- or admin-level key required (403 otherwise). Scope peers are also rejected as `peer_target` / `peer_perspective` on session context and as the path peer or `target` on `GET /peers/{id}/context` (#897)
- Scope backfill-by-copy and removal reconciliation. Adding a session that already has messages copies its explicit-level documents into the scope's collections (no LLM re-derivation; idempotent via `copied_from`). Removing a session soft-deletes those copies and fail-closed cascades to derived documents whose `source_ids` intersect anything removed, then enqueues a `card_refresh` dream with `rebuild=True` plus an omni dream. `GET /v3/workspaces/{workspace_id}/scopes/{scope_id}/status` reports per-session backfill state (`pending` / `completed` / `failed`, plus `docs_copied`) (#904)
- Workspace-level chat at `POST /v3/workspaces/{workspace_id}/chat`: agentic dialectic over the whole workspace instead of a single (observer, observed) pair. Prefetches workspace stats and the top active peers' self cards, then searches pair-scoped memory with `[observer->observed]` attribution. Supports `session_id`, `scope`, `reasoning_level`, `response_format`, and SSE streaming (#931)
- MCP workspace discovery: tools accept `workspace_id`, the worker honors an optional `X-Honcho-Workspace-ID` connection header, and `list_workspace` / `create_workspace` tools let clients pick or create a workspace instead of relying on the SDK default (#1020)
- MCP `search` also queries conclusions in parallel with messages when `peer_id` is given, returning `{messages, conclusions}`. The conclusions leg degrades to `[]` on error so search never gets worse than before (#974)
- Prometheus metrics for physical DB connections, visible even under `DB_POOL_CLASS=null`: `db_connections_open` (gauge) and `db_connections_established` (counter), hooked to SQLAlchemy connection-lifecycle events and registered on both the API and the deriver (#1055)
- Bounded-label Prometheus series are zero-initialized at process start so an absent series means a broken scrape rather than "nothing happened" (#927)
### Changed
- Workspace and pair chat system prompts now describe Honcho, peers, and the harness on their own terms, and render only the tools the request actually offers. The pair prompt no longer advertises a write tool that is not in the loadout (#1066)
- Deriver idle polling backoff is longer and no longer reset by periodic reconciler work, so downstream connection pools can cull idle DB connections (#1015)
- LLM provider SDKs are lazy-loaded so idle API and deriver processes no longer pay for every provider at import time (#1011)
- Production image is a multi-stage build: LanceDB/PyArrow move behind an optional `lancedb` extra (`INSTALL_LANCEDB=true` to restore them), FastAPI's unused cloud CLI is dropped, and the venv is copied into the runtime image with final ownership so Docker does not double the layer. Default unpacked image is about 663 MB (was 1.7 GB) (#1014)
- Redis Cluster cache keys hash-tag the namespace so one deployment's keys land on a single shard instead of opening a connection to every node. No behaviour change on a non-cluster backend; existing keys age out by TTL (#1058)
- Deriver extraction prompt no longer leaks its own few-shot examples into extracted conclusions (#1028)
### Fixed
- Observer-scoped `get_observation_context` no longer materializes every session the observer has ever joined into a `session_name IN (...)` list (twice in one statement). Past ~32k sessions that hit psycopg's bind-parameter ceiling and 500'd. The observer half is now a correlated `EXISTS` over `session_peers`, two bind parameters regardless of membership size (#1065)
- Re-adding an already-active session peer no longer advances `joined_at`, so `peer_perspective` search keeps messages from the original join. Genuine leave-and-rejoin still starts a new window (#1059)
- Transient embedding-provider errors (for example an OpenAI-compatible 200 with empty `data: []`) were relabeled as token-limit errors. Only genuine oversize input raises `EmbeddingTokenLimitError`; other provider errors propagate unchanged (#791)
- The filter DSL now fails closed with a 422 instead of a 500 on bad shapes, coerces operands by column type (so `{"session_id": {"ne": "abc"}}` is a string inequality rather than "invalid numeric"), and treats `NOT` / `ne` as null-safe (`IS NOT TRUE` / `IS DISTINCT FROM`) so negation no longer drops rows whose field is unset. Closed-set columns like `level` reject unknown values. Session-allowlist entries must be well-formed ids (`*` is 422, not a silent widen) (#947)
- `ne` on JSONB metadata keys is null-safe: a missing key is not equal to the compared value, so `{"metadata": {"foo": {"ne": "bar"}}}` includes rows where `foo` is unset (#1036)
- Oversized texts in `simple_batch_embed` are truncated to the embedding token cap instead of failing the whole batch. Representation processing reports failed observer saves in `RepresentationCompletedEvent` and raises when every observer save fails (#1019)
- Assistant `reasoning_content` (DeepSeek / some OpenRouter models) is preserved across tool-loop turns. Previously the tool loop dropped thinking content before building the next assistant history message, so continuation requests failed. `reasoning_details` still takes precedence when both are present (#1034)
- `create_observations` now honors `DERIVER_DEDUPLICATE` instead of hardcoding `deduplicate=True`, matching the representation write path (#1018)
- `provider_params.timeout` is forwarded to the OpenAI-compatible embedding client, not just the LLM client (#1024)
- Conclusions semantic-search validation errors name the field and the constraint instead of returning a generic 422 (#960)
- OpenAI-compatible embedding calls request `encoding_format=float` so providers that default to base64 do not break pgvector inserts (#938)
- Gemini batch embedding works for `gemini-embedding-2*` models, which rejected the previous request shape (#745)
- MCP OAuth with no advertised scopes no longer defaults to read-only (which 403'd chat and search POSTs). Protected-resource metadata advertises read and write (#1004)
## [3.0.12] - 2026-08-10
### Added
- Session allowlist on the Dialectic and representation via a constrained `filters` body on `POST /peers/{peer_id}/chat` and `/representation`, supporting only the `session_id` key (a session id, a bare list, or `{"in": [...]}`). Unsupported keys and shapes are rejected with 422 rather than silently ignored, it composes with `session_id` (which must be included in the allowlist when both are given), and it is capped at 1,000 sessions per request. Enforcement is uniform and fail-closed at every recall chokepoint: scoped conclusion recall is restricted to `level == "explicit"` (dream-derived conclusions carry a single `session_name` but are synthesized across all sessions, so that stamp can't be scoped on), `get_reasoning_chain` is unavailable under an allowlist, and an empty allowlist short-circuits to empty results everywhere. Workspace keys pass the allowlist as-given; peer-scoped JWTs must be an active member of every allowlisted session (401 otherwise) (#882)
- Bare-list membership sugar in the filter DSL: `{"session_id": ["s1", "s2"]}` is now shorthand for `{"session_id": {"in": [...]}}` on regular columns generically. JSONB metadata columns are excluded and keep containment semantics. Strictly additive, since a bare list on a regular column previously compiled to a type-mismatched equality that matched nothing (#881)
- Optional structured outputs on the Dialectic: `response_format` (a JSON Schema with root type `object`) on peer chat makes `content` a JSON string conforming to that schema. Only a conservative subset of JSON Schema is supported, with DoS guards and non-recursive `$ref` support (#896)
- Combined tool calling and structured output in the LLM transport layer, with per-backend request shaping: OpenAI routes tool-carrying structured requests through `create()` with an explicit `json_schema` response format (`parse()` 500s on non-strict function tools), Anthropic skips the `{` JSON prefill when tools are present so `tool_use` blocks stay reachable, and Gemini injects a schema instruction into the final turn instead of using native `response_schema` (rejected alongside function calling before Gemini 3). All backends skip structured-output parsing on tool-call turns, which carry no consumable content (#907)
- `card_refresh` dream type: a lightweight dream that runs only the peer-card update, for event-driven refreshes such as membership changes and cold starts. Handled by a new `CardRefreshSpecialist` restricted to `get_recent_observations`, `search_memory`, and `update_peer_card` (no observation-mutating tools) with a tool-iteration cap of `min(6, DREAM.MAX_TOOL_ITERATIONS)`. `POST /v3/workspaces/{workspace_id}/schedule_dream` accepts `dream_type=card_refresh` plus a `rebuild` flag, which omits the existing card from the prompt so the specialist rebuilds it solely from observations present in the collection. Card refreshes never advance the omni dream guard pair (`last_dream_at` / `last_dream_document_count`) (#883)
- Full-fidelity LLM trace stream, with Langfuse as one projection over it: each call is captured once (`CapturedLLMCall`) and fanned out to a CloudEvents trace stream (`llm.call.traced` / `trace.content`) and a Langfuse exporter, both reconstructing trace → run → step → generation from the same source of truth. Adds `TELEMETRY_TRACE_PAYLOADS_ENABLED` (default `false`), `TELEMETRY_TRACE_MAX_BYTES` (default 262144, per-message cap with oversized content clipped), `TELEMETRY_TRACE_PURPOSES` (JSON list of `CallPurpose` values; empty means all), and `LANGFUSE_EXPORTER_MODE` (`exporter` by default; `inline` is kept for one release for side-by-side validation). Embedding calls are traced, dreamer branches nest under one dream trace, tool calls become spans under their step, and high-volume events are sampled deterministically. `TRACE_ENDPOINT` is dropped (#845)
- Redis Cluster support via `CACHE_CLUSTER` (for example GCP Memorystore for Redis Cluster), alongside a new `CACHE_LOCK_WAIT_CHECK_INTERVAL_SECONDS` (#905)
- `EMBEDDING_MODEL_CONFIG__MAX_BATCH_SIZE` caps texts per embedding request for OpenAI-compatible providers with smaller limits than OpenAI's, such as DashScope `text-embedding-v4` (10) and Alibaba Bailian `qwen3.7-text-embedding` (20). When unset, native provider defaults are preserved (OpenAI 2048, Gemini 100) (#983)
- Per-request provider timeouts via `provider_params.timeout` on any model config, validated at config load so a bad value fails at startup with the exact config path instead of surfacing per-request as a retried 500. Good values normalize to float seconds; Gemini's is converted to milliseconds (#832)
- `RepresentationCompletedEvent` now reports deduplication counts: `exact_dup_in_batch_count`, `exact_dup_existing_count`, `semantic_dup_rejected_count`, and `semantic_dup_replaced_count` (#910)
- OAuth discovery for MCP clients: the MCP worker serves `/.well-known/oauth-protected-resource` (RFC 9728) without auth so clients can discover the authorization server, and a 401 now carries `WWW-Authenticate: Bearer resource_metadata="..."` (exposed cross-origin) to start the flow (#923)
- Prometheus metrics for the immediate-embed fast path: tasks shed because `EMBEDDING_MAX_PENDING_EMBED_TASKS` was reached, and the current in-flight task count (#892)
- Docs: a detailed system architecture diagram, a Codex integration guide (#879), a structured-outputs page (#896), a section on filtering conclusions by reasoning level (#851), a health-check endpoint reference, and SDK updates (#867)
### Changed
- **Breaking config change:** `DERIVER_REPRESENTATION_BATCH_MAX_TOKENS` is split into two settings that were previously conflated — `DERIVER_REPRESENTATION_BATCH_WORK_UNIT_TARGET_TOKENS` (default 512), the producer-side minimum a work unit accumulates before the deriver claims it, where `0` disables the gate; and `DERIVER_REPRESENTATION_BATCH_TARGET_INPUT_TOKENS` (default 1024), the consumer-side maximum context-window tokens per deriver LLM call. Deployments setting the old name must migrate (#889)
- The immediate-embed fast path now applies backpressure: `EMBEDDING_MAX_PENDING_EMBED_TASKS` (default 50) caps in-flight embed tasks, and once saturated, message creation skips the fast path entirely and the reconciler embeds on its next cycle. `0` disables the fast path (#892)
- Explicit-level documents are now kept session-pure, so memory can be built by copying explicit documents between collections. Enforcement refuses rather than rewrites: `create_documents` rejects explicit documents with a null `session_name`, exact dedup keys on (content, level, session-for-explicit), semantic dedup scopes candidate search to the same level and — for explicit documents — the same session, and the generic `create_observations` tool rejects `level='explicit'` outside message-ingestion (deriver) context. Derived levels keep cross-session consolidation (#883)
- Sentry's `before_send` filter is centralized as `default_before_send` in `src/telemetry/sentry.py` instead of living only in the API's `main.py`, so the deriver gets the same non-actionable-exception filtering. All Sentry events also carry a `namespace` tag for correlation (#934, #870)
- The minimal deriver's extraction examples no longer teach inferences its own output schema forbids. The `EXAMPLES` block demonstrated deriving a specific birthday from "I just had my 25th birthday last Saturday", deriving residence from a single visit ("I took my dog for a walk in NYC" → "alice lives in NYC"), and a "+ general knowledge" deductive output the deriver has no channel for. The replacements stay inside the schema's contract and teach the boundary: the dog/NYC message is kept and shown extracting correctly, and a separate example shows "lives in NYC" is valid when actually stated (#985)
- Dreamer specialists are instructed not to output summaries (#894)
- `session_name` is deprecated for scoping in favor of the session allowlist. It is not removed and not aliased: it also pins the query to one session, bypasses observer scoping, and drives session-history injection into the dialectic prompt, so it has no drop-in replacement (#882)
- The MCP worker no longer requires the `X-Honcho-User-Name` or `X-Honcho-Assistant-Name` headers (#923)
### Fixed
- Session scoping was applied to only one of the three working-representation query paths: `session_name` reached the recent-documents query, but the semantic and most-derived paths ignored it, so `limit_to_session` leaked cross-session conclusions into perspectives. The allowlist is now threaded uniformly through all three paths and pushed down to pgvector and external vector stores (#881)
- Empty membership lists failed open in the vector-store filter builders, silently widening scope: LanceDB dropped empty `IN` clauses and Turbopuffer emitted a bare `In []` with undocumented semantics. Both now emit an explicit always-false predicate, and `_build_filter_conditions` checks `is not None` rather than truthiness so an empty list is no longer treated like `None` (#881, #882)
- Session-scoped CRUD helpers ignored the session allowlist entirely, so a caller could read a session the allowlist forbids. The API routes guarded this with a 422, but the dialectic tools call these CRUD functions directly and bypassed it. `_semantic_search_messages` (covering `search_messages` and `search_messages_temporal`), `grep_messages`, `get_messages_by_date_range`, `get_recent_history`, and `get_observation_context` now return `[]` when `session_name` is set and outside the allowlist (#882)
- The cache client logged the full Redis URL — including the password — at INFO and WARNING on every connection attempt and failure, exposing the live credential in container logs and downstream aggregation. Credentials are now redacted across userinfo, the `?password=` (redis-py) and `?secret=` (cashews) query params, scheme-less URLs whose password is invisible to `.port`/`.password` parsing, and malformed URLs, whose fallback previously echoed the raw input verbatim (#869)
- A `top_k` of `0` reached the vector store, where Turbopuffer rejects it with a 400 (`top_k must be between 1 and 10000`). A non-positive `top_k` now returns `[]` before the embedding call, and the semantic budget floors at 1 so an explicitly requested search isn't silently allocated zero (#970)
- Gemini clients had no HTTP timeout, so a stalled socket wedged the deriver worker's uvloop event loop, which the in-process reconciler shares. A 10-minute timeout is now set on both the Gemini LLM client and the Gemini embedding client (#903)
- Dreamer conclusions were dated to ingestion time rather than their latest source observation, and their timestamps are now normalized (#890)
- Langfuse I/O annotation was gated on `LANGFUSE_PUBLIC_KEY` instead of `langfuse_inline_enabled`, so in the default `exporter` mode it called `update_current_generation()` with no active span — logging "No active span in current context" roughly 14 times per dialectic run and building throwaway `model_dump` payloads on every LLM call. Separately, `AgentToolSummaryCreatedEvent` hardcoded `run_id="deriver"` / `iteration=0`, polluting `run_id` grouping in the CloudEvents stream with a phantom run; both fields are now optional and the resource id is keyed on `message_id:summary_type` (schema_version 2 → 3) (#845)
- Assistant tool calls were dropped from the captured trace stream for OpenAI and Gemini: `build_captured_messages` read only `{role, content, tool_call_id}`, but those providers keep tool calls outside `content`, so replayed tool-call turns landed as empty content and Gemini lost its text and tool results entirely. Tool calls are now normalized per provider into a unified `tool_calls` field and folded into the content hash. Gemini's `thought_signature` is bytes, so `model_dump(mode="json")` raised `UnicodeDecodeError` inside `emit_trace`, silently dropping whole tool-calling iterations from the trace stream (billing and Langfuse were unaffected); it is now base64-encoded on the telemetry path while replay keeps the raw bytes (#845)
- `EmbeddingClient.encoding` forced full client construction, raising "OpenAI API key is required" even though tiktoken needs no credentials. The document dedup tie-break only needs `.encoding` for token counting, so any test hitting that path failed in environments without embedding keys — notably CI for pull requests from forks. The encoding is now resolved from the configured model directly, falling back to `cl100k_base`, and the underlying client's encoding is reused only when it has already been constructed (#955)
- The Docker build failed under Podman because the uv build inputs weren't copied (#878)
- LanceDB was installed on macOS Intel, where it doesn't work. A PEP 508 marker excludes `darwin/x86_64` and the LanceDB vector-store import is wrapped so a misconfiguration surfaces as a clear config error (#496)
- Prompt checks requiring the literal token "json" for `json_object` mode are now satisfied in lowercase (#887)
- Reverted an unintended `RepresentationCompletedEvent` schema-version increment
- Documented preinstalling pgvector as a privileged role for deployments where the `DB_CONNECTION_URI` role deliberately cannot create extensions (managed Postgres, Kubernetes operators, NixOS). `CREATE EXTENSION IF NOT EXISTS vector` does not help there, because Postgres checks the privilege before checking whether the extension exists. Docker Compose is unaffected, since the bundled stack connects as the `postgres` superuser (#984)
## [3.0.11] - 2026-06-24
### 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)
- Conclusion responses now include a `level` field (`explicit`, `deductive`, `inductive`, `contradiction`); list/query endpoints support filtering by `level` via `filters`, with reserved filter keys protected from being overridden by user-supplied filters (#851)
### 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)
- Document creation now performs exact (case-insensitive, whitespace-trimmed) content deduplication before the existing semantic dedup step: exact duplicates within a batch collapse to a single insert, and an exact match against a live document reinforces it (atomic `times_derived` increment) instead of creating a new row (#861)
- The OpenAI backend passed `tool_choice` through raw while the Anthropic and Gemini backends translate Honcho's canonical vocabulary to their native form, so on a mixed-provider fallback chain (for example Gemini primary → OpenAI backup) a canonical `"any"` reached OpenAI unchanged and was rejected as an invalid param. The OpenAI backend now converts it, mirroring the others: `any`/`required` → `required`, `auto`/`none` pass through, and a tool-name string or `{"name": ...}` dict becomes a function selection (#850)
- Langfuse `@observe` auto-capture serialized every argument of `honcho_llm_call_inner` into the generation span input, including `client_override` (a live `AsyncOpenAI`/`genai` client) and `selected_config` (which carries `api_key`). Auto-capture deep-copied the client into a half-constructed object whose teardown raised (`AsyncHttpxClientWrapper ... no attribute '_state'` on OpenAI, flooding stderr; `BaseApiClient ... no attribute '_http_options'` on Gemini), and it leaked `ModelConfig.api_key` into traces. Capture is now an explicit allowlist: `capture_input`/`capture_output` are disabled and curated, serializable input and output are stamped instead, with tuning knobs surfaced as `model_parameters` via a secret-bearing denylist and per-call token usage mirrored as `usage_details` (#849)
## [3.0.10] - 2026-06-15
### Added
- Messages are now embedded via a background task rather than blocking API request
- Read-only DB session mode (`get_read_db` / `tracked_db(..., read_only=True)`) so reads don't hold a transaction open across the work
- `CORS_ORIGINS` env var to configure CORS allowed origins without editing source; defaults match the prior hardcoded list, so self-hosted deployments behind custom domains can whitelist their frontend (#697)
- `scripts/generate_jwt.py` — utility for minting scoped or admin Honcho JWTs (`--admin`, `--workspace`/`--peer`/`--session`, `--expires` with human-friendly durations, `--print-only`) without calling the keys API (#757)
- `STALE_WORK_UNIT_CLEANUP_INTERVAL_SECONDS` (default 60s) — minimum jittered spacing between deriver stale-work-unit cleanup runs, so cleanup no longer runs on every seconds-scale poll (`0.0` keeps the legacy every-poll behavior) (#773)
### Changed
- Optimized the deriver and dreamer prompt cache prefixes to improve prompt-cache hit rates (#806)
### Fixed
- `times_derived` is now properly reinforced when a duplicate conclusion is detected. It had been pinned at 1 for nearly every conclusion (the reject-new branch dropped the increment and the new-wins branch reset the count to 1), so `ORDER BY times_derived DESC` fell back to arbitrary heap order and froze stale conclusions to the front of injected context. Reinforcement is now an atomic increment and both most-derived queries gained a `created_at DESC` recency tiebreaker (#768)
- Webhook creation now correctly rejects private/internal IP addresses (#793)
## [3.0.9] - 2026-06-02
### Changed
- Connection acquisition is now a single attempt with no server-side retry, on a vanilla `AsyncSession`. A new `DB_CONNECT_TIMEOUT_SECONDS` (default 2s) bounds the attempt so a saturated or unreachable pooler fails fast instead of holding a client connection open to re-knock. A saturated DB now surfaces to the caller — the API returns an error and the deriver backs off and retries on a later poll — which lets the pooler drain rather than amplifying saturation.
### Added
- Deriver poll jitter so instances that start together don't poll in lockstep: `DERIVER_POLLING_STARTUP_JITTER_SECONDS` (random delay before the first poll, default 30s) and `DERIVER_POLLING_JITTER_RATIO` (±fraction applied to every poll sleep, default 0.5). Both disable at `0.0`; the underlying backoff schedule is unchanged.
### Removed
- Reverted the connection-checkout retry and `HonchoAsyncSession` custom session introduced in 3.0.8. Removed the `DB_CONNECTION_RETRY_ENABLED` / `DB_CONNECTION_RETRY_MAX_DELAY_SECONDS` / `DB_CONNECTION_RETRY_BACKOFF_INITIAL_SECONDS` / `DB_CONNECTION_RETRY_BACKOFF_MAX_SECONDS` settings, the `db_connection_acquisitions{outcome=...}` Prometheus counter, and the `db.pool.acquire` Sentry span. Alerting built on `db_connection_acquisitions` should migrate to `db_pool_connections` / `db_queries_in_flight`.
## [3.0.8] - 2026-06-01
### Added
- Connection-checkout retry with bounded exponential backoff (tenacity) on `get_db`/`tracked_db`: transient transaction-pooler (Supavisor) rejections — SQLAlchemy `TimeoutError` and `OperationalError` — now retry with backoff instead of surfacing as 500s under client-connection saturation. Gated by
`DB_CONNECTION_RETRY_ENABLED` with configurable delay/backoff knobs; ~10s default budget (#758)
- `HonchoAsyncSession` — a lazy `AsyncSession` that checks out its pooled connection (with retry) on the first DB-touching call rather than at construction. Request handlers doing non-DB work (embedding, file, LLM) before their first query no longer pin a pooler connection across it. Only the checkout is retried;
the statement still runs exactly once, so writes are never duplicated (#758)
- Adaptive deriver queue polling: the poll interval backs off when the queue is idle or erroring (base → max, doubling each cycle) and snaps back to base the moment work is claimed, cutting steady-state query load against the DB. Gated by `DERIVER_POLLING_BACKOFF_ENABLED` with configurable max/multiplier (#758)
- New Prometheus `db_pool_connections` gauge (checked_out / checked_in / size / overflow), labeled `api`|`deriver`, registered in both the API lifespan and the deriver metrics server (#758)
- New Prometheus `db_connection_acquisitions{outcome=ok|retried|exhausted}` counter — the alertable early-warning signal that connection checkouts are retrying through pooler rejection, before requests start failing (#758)
- New Prometheus `db_queries_in_flight` gauge — statements actually executing on the wire (via SQLAlchemy cursor-execute events). Paired with `checked_out`, the gap reveals connections held but parked (the "idle in transaction during an external call" antipattern). Gated on `METRICS.ENABLED` for zero overhead when
off (#758)
- Explicit `SqlalchemyIntegration` in both the API and deriver Sentry inits; connection acquisition wrapped in a `db.pool.acquire` span with live pool stats captured on retry exhaustion (#758)
### Changed
- Default `POOL_TIMEOUT` lowered to 5s, with validation that it stays under the connection-retry budget when a pooled (non-null) `POOL_CLASS` is configured; `config.toml.example` and the v2/v3 configuration docs updated to match (#758)
- `HonchoAsyncSession` wraps every DB-touching session method (execute / scalar / scalars / flush / merge / refresh / commit / get / get_one / stream / stream_scalars / delete) so the lazy-checkout-with-retry guarantee has no holes; the acquired flag resets on `close()`/`reset()` so a reused session re-acquires on
next use (#758)
### Fixed
- Roll the session back on a retryable checkout failure before retrying — a failed autobegin could otherwise leave it pending-rollback, making the next connection attempt raise instead of cleanly re-checking-out (#758)
- Guard `DBPoolCollector.collect()` so a pool-read/import hiccup can't raise and abort the entire `/metrics` scrape (Prometheus drops all metrics if any collector raises) (#758)
- Clamp the pool overflow gauge to ≥ 0 (it could report negative before the pool fills) (#758)
- Removed a double-sleep in the deriver idle poll so the backoff cap is a true cap rather than 2× (#758)
## [3.0.7] - 2026-05-21
### Added
- New `src/llm/` module as the single owner of provider runtime: clients, backends, history adapters, tool loop, request builder, credentials, and caching policy (#459)
- `AttemptPlan` dataclass captures per-retry provider selection (client, model, reasoning_effort, thinking_budget_tokens, selected_config) and pins it across stream-final retries so streaming doesn't bounce back to primary after the tool loop has settled on fallback (#459)
- Gemini JSON-schema sanitizer for `function_declarations` — strips keywords Gemini's validator rejects (`additionalProperties`, `allOf`, etc.) while preserving semantics for all other backends (#459)
- Dreamer specialists derive `effective_max_tokens` from `model_config.max_output_tokens` with a per-specialist default fallback (#459)
- New cloudevent `LLMCallCompletedEvent` (`llm.call.completed`) fires once per provider hit with full cost-attribution context: transport/provider_label, model, token counts with cache breakdown, finish_reason, outcome, `is_final_attempt`, retry/fallback state, duration, tool-call shape, streaming flag, and agent correlation (`run_id` + iteration). Includes a `CallPurpose` closed enum (`deriver.representation`, `dialectic.answer`, `dream.deduction|induction`, `summary.short|long`) (#637)
- `RepresentationCompletedEvent` now carries `total_input_tokens` for full-trace cost attribution (#637)
- Per-emitter `honcho_version` injection on all CloudEvents plus emitter health metrics (#637)
- `TelemetrySettings.HIGH_VOLUME_SAMPLE_RATE` (default 1.0) — deterministic per-`run_id` sampler so an entire agent trace is kept or dropped together; aggregate envelopes bypass the sampler (#637)
- Deriver custom instructions: per-workspace/peer guidance threaded into the deriver prompt with a `MAX_CUSTOM_INSTRUCTIONS_TOKENS` budget (default 2000); deriver `MAX_INPUT_TOKENS` raised 23000 → 25000 to make room (#609)
- Configurable embedding dimensions: `EMBEDDING_MODEL_CONFIG__DIMENSIONS_MODE` (`auto`/`always`/`never`) controls whether the OpenAI `dimensions=` parameter is forwarded; `auto` (default) sends it when the operator explicitly set `EMBEDDING_VECTOR_DIMENSIONS` and the model is not on the known-rejecting allowlist (#678)
- New `honcho-cli` package — Python CLI for inspecting and managing peers, sessions, and configuration against a Honcho deployment (#424)
- `HONCHO_API_URL` env var support in the MCP Worker, enabling self-hosted Honcho deployments to point the Worker at their own instance instead of `https://api.honcho.dev` (#575)
- API ID `max_length` increased from 100 to 512 across `WorkspaceCreate`, `PeerCreate`, and `SessionCreate` to align the API contract with the underlying DB schema (#684)
- Regression tests covering fallback-config thinking-param reach, provider_params → extra_params boundary, OpenAI reasoning-model parameter routing, Gemini blocked finish_reason handling, and fail-fast `max_tool_iterations` validation (#459)
### Changed
- All LLM orchestration moved out of `src/utils/clients.py` into `src/llm/` with modules split by responsibility (api, executor, tool_loop, runtime, registry, conversation, request_builder, credentials, caching, backends, history_adapters) (#459)
- Default `ModelConfig` factories (deriver, summary, dreamer specialists, dialectic levels) normalized to `openai/gpt-5.4-mini` with no extra parameters set by default; operators add transport/thinking overrides explicitly (#459)
- OpenAI reasoning-model routing widened via `_uses_max_completion_tokens` heuristic covering `gpt-5.x` and `o1/o3/o4` — these models receive `max_completion_tokens` instead of `max_tokens` (#459)
- Override client factories switched from unbounded `@cache` to `@lru_cache(maxsize=128)` for predictable memory growth on long-running processes (#459)
- `get_backend` now delegates to `client_for_model_config`, so the live-test path and production path share one missing-API-key validation (#459)
- Blocked Gemini responses (`SAFETY`, `RECITATION`, `PROHIBITED_CONTENT`, `BLOCKLIST`) raise `LLMError` in the streaming path too (previously only the non-streaming path), ensuring retry/fallback logic fires uniformly (#459)
- Transport-change env overrides now strip transport-specific thinking params (thinking_budget_tokens vs. reasoning_effort) during config merge, including at the dialectic-level merge, so switching from Anthropic → OpenAI doesn't leave orphaned Anthropic-only params that the OpenAI backend would reject (#459)
- `max_tool_iterations` out-of-range inputs now raise `ValidationException` instead of being silently clamped (#459)
- Public API schemas (`WorkspaceCreate`, `PeerCreate`, `SessionCreate`) and SDK validation (`api_types.py`, `validation.ts`) accept IDs up to 512 chars (was 100) (#684)
- Peer card prompts reframed as stable identity markers (replaces the prior "biographical/profile facts" language). Induction specialist is now opted out of peer card writes (`can_update_peer_card = False`) so only deduction touches the card (#686)
- Vector store queries no longer fetch embedding vectors — only document metadata is returned, reducing payload size and DB load (pgvector, lancedb, turbopuffer) (#682)
- Langfuse trace metadata now includes `namespace`, `model`, and `provider` so traces can be filtered by deployment slice (#565)
- Deriver: model-aware tokenizer (replaces the previously hardcoded encoding) and explicit guard on empty message content (#647)
- Dialectic level defaults now merge correctly with per-level overrides in `src/config` (#656)
- Default dialectic tool choice switched from forced/required to `auto` (#630)
- Vector sync given a substantial retry budget to tolerate transient embedding provider outages (#604)
- `AgentToolConclusionsDeletedEvent` payload now carries `levels` for parity with the rest of the conclusion event surface (#612)
- Turbopuffer vector store: `InternalServerError` caught and surfaced as a warning rather than a hard failure; unused `upsert_with_retry` and `VectorUpsertResult` removed; explicit silent and explicit-error paths for vector DB server errors (#561)
- Troubleshooting docs updated to reflect nested-env-var form for per-component thinking-budget overrides (#459)
- README refresh (#681)
- CLAUDE.md refreshed against the current `src/` layout (#680)
### Fixed
- Fallback `ModelConfig` temperature and `thinking_budget_tokens` reach the backend on the final retry — previously the primary's values were pre-populated into caller kwargs early and clobbered fallback values via `effective_config_for_call(update=...)` (#459)
- Stream-final retries pin to the `AttemptPlan` that succeeded rather than re-running provider selection through the outer `current_attempt` ContextVar (which could roll streaming back to primary after the tool loop had already switched to fallback) (#459)
- OpenAI structured-output calls continue to use `chat.completions.parse()` with strict schema enforcement, while tool-calling paths use `chat.completions.create()` without `strict:True` for broader proxy compatibility (OpenRouter, vLLM, Ollama) (#459)
- Gemini `cached_content` reuse keys now include `system_instruction` and `tool_config` so cache hits don't cross configurations that differ only in those fields (#459)
- Removed strict parameter validation for thinking params on Anthropic and OpenAI transports — was rejecting valid per-transport configs (#686)
- `reverse` query parameter is now honored on the v3 workspace list (`POST /v3/workspaces/list`), peer list (`POST /v3/workspaces/{workspace_id}/peers/list`), workspace-scoped session list (`POST /v3/workspaces/{workspace_id}/sessions/list`), and peer-scoped session list (`POST /v3/workspaces/{workspace_id}/peers/{peer_id}/sessions`). Honcho SDKs at 2.1.0+ were already sending `reverse=true` for these routes but the server silently ignored it. Ties on `created_at` now fall back to the internal nanoid `id` so ordering remains stable across pages (#685)
- LLM client factories now receive `base_url` from `LLMSettings` for default providers — previously the override path honored `base_url` but the default path didn't, so operators pointing at OpenAI-compatible proxies via `LLM__OPENAI_BASE_URL` were ignored (#643, fixes #641)
- Internal N+1 query in dialectic agent tool execution — collapsed per-iteration DB lookups into a single fetch (#652)
- Dreamer threshold and time-guard semantics: `check_and_schedule_dream` count filter now includes only `documents.level == 'explicit'` (dreamer-created levels are output, not input, and were inflating the threshold and creating a feedback loop); `last_dream_at` write relocated from `enqueue_dream` into `process_dream` so duplicate enqueues or failed runs no longer reset the 8-hour time guard (#573)
- Deriver: blank observations are filtered out before embedding (previously triggered noisy embedding calls and persisted empty rows); blank-observation filtering unified across tool paths (#615)
- Surprisal module: filter for level observations changed from `{"level": levels}` to `{"level": {"in": levels}}` — `apply_filter()` requires operator syntax, so the prior call silently returned 0 results and made the entire Surprisal phase of the Dream cycle a no-op (#581, fixes #559)
- Removed hardcoded `stop_sequences` override from Deriver `ModelConfig` (was clobbering operator-configured stop sequences) (#587)
- Removed stale `stop_sequences` from tests (#607)
- Embedding client: `embed()` now wraps single-string input in an array, restoring compatibility with OpenAI-compatible third-party providers that reject scalar input (#586)
- Docker Compose: deriver service startup gated on the API service healthcheck (prevents races where the deriver starts before the API has run migrations) (#689)
- Docker image: `HEALTHCHECK` directive removed from the shared base image — it probed an HTTP endpoint only the API serves, permanently marking deriver containers as unhealthy. Service-level health checks now belong in each service's own configuration (k8s readiness/liveness probes on the API Deployment only) (#530)
- `tests/unified`: `--test-dir`/`--test-file` arguments now use an argparse mutually-exclusive group instead of manual validation (#650)
- CrewAI example updated for the latest CrewAI protocol (#631)
### Removed
- `src/utils/clients.py` deleted; its responsibilities are split across `src/llm/registry.py`, `src/llm/credentials.py`, and the backend-specific modules (#459)
- `HEALTHCHECK` directive removed from the shared Docker image (#530)
## [3.0.6] - 2026-04-10
### Changed
- Tightened transaction scopes across search, agent tools, queue manager, and webhook delivery to minimize DB connection hold time during external operations (#525)
- Search operations refactored to two-phase pattern — external work (embeddings, LLM calls) completes before opening a transaction (#525)
- Agent tool executor performs external operations before acquiring DB sessions (#525)
- Queue manager transaction scope reduced to only the critical section (#525)
- Webhook delivery no longer holds a DB session parameter (#525)
### Fixed
- Session leakage in non-session-scoped dialectic chat calls (#526)
### Added
- Health check endpoint (`/health`) for container orchestration and load balancer probes (#510)
## [3.0.5] - 2026-04-03
### Fixed
- explicit rollback on all transactions to force connection closed
## [3.0.4] - 2026-04-02
### Added
- JSONB metadata validation enforces 100 key limit and max depth of 5 (#419)
### Changed
- Schemas refactored from single `schemas.py` into `schemas/api.py`, `schemas/configuration.py`, and `schemas/internal.py` with backwards-compatible re-exports (#419)
### Fixed
- Missing `deleted_at` filter on `RepresentationManager._query_documents_recent()` and `._query_documents_most_derived()` allowed soft-deleted documents to leak into the deriver's working representation (#456)
- `CleanupStaleItemsCompletedEvent` emitted spuriously when no queue item was actually deleted (#454)
- Empty JSON file uploads caused unhandled errors; now returns normalized error responses (#434)
- Memory leak: `_observation_locks` switched to `WeakValueDictionary` to prevent unbounded growth (#419)
- SQL injection in `dependencies.py`: parameterized `set_config` calls to prevent injection via request context (#419)
- NUL byte crashes: string inputs (message content, queries, peer cards) now stripped at schema level (#419)
- Filter recursion depth capped at 5 to prevent stack overflow (#419)
- Dedup-skipped observations now correctly reflected in created counts (#477)
- External vector store support for message search — routes queries through configured external vector store with oversampling and
deduplication to handle chunked embeddings (#479)
- Dialectic agent no longer holds a DB connection during LLM calls — embeddings are pre-computed before tool execution, DB sessions isolated in `extract_preferences`, `query_documents` no longer accepts a DB session parameter (#477)
## [3.0.3] - 2026-02-25
### Added
- Consolidated session context into a single DB session with 40/60 token budget allocation between summary and messages
- Observation validation via `ObservationInput` Pydantic schema with partial-success support and batch embedding with per-observation fallback
- Peer card hard cap of 40 facts with case-insensitive deduplication and whitespace normalization
- Safe integer coercion (`_safe_int`) for all LLM tool inputs to handle non-integer values like `"Infinity"`
- Embedding pre-computation and reuse across multiple search calls in dialectic and representation flows
- Peer existence validation in dialectic chat endpoints — raises ResourceNotFoundException instead of silently failing
- Logging filter to suppress noisy `GET /metrics` access logs
- Oolong long-context aggregation benchmark (synth and real variants, 1K–4M token context windows)
- MolecularBench fact quality evaluation (ambiguity, decontextuality, minimality scoring)
- CoverageBench information recall evaluation (gold fact extraction, coverage matching, QA verification)
- LoCoMo summary-as-context baseline evaluation
- Webhook delivery tests, dependency lifecycle tests, queue cleanup tests, summarizer fallback tests
- Parallel test execution via pytest-xdist with worker-specific databases
- `test_reasoning_levels.py` script for LOCOM dataset testing across reasoning levels
### Changed
- Workspace deletion is now async — returns 202 Accepted, validates no active sessions (409 Conflict), cascade-deletes in background
- Redis caching layer now stores plain-dict instead of ORM objects, with v2-prefixed keys, storage, resilient `safe_cache_set`/`safe_cache_delete` helpers, and deferred post-commit cache invalidation
- All `get_or_create_*` CRUD operations now use savepoints (`db.begin_nested()`) instead of commit/rollback for race condition prevention
- Reconciler vector sync uses direct ORM mutation instead of batch parameterized UPDATE statements
- Summarizer enforces hard word limit in prompt and creates fallback text for empty summaries with `summary_tokens = 0`
- Blocked Gemini responses (SAFETY, RECITATION, PROHIBITED_CONTENT, BLOCKLIST) now raise `LLMError` to trigger retry/backup-provider logic
- Gemini client explicitly sets `max_output_tokens` from `max_tokens` parameter
- All deriver and metrics collector logging replaced with structured `logging.getLogger(__name__)` calls
- Dreamer specialist prompts updated to enforce durable-facts-only peer cards with max 40 entries and deduplication
- `GetOrCreateResult` changed from `NamedTuple` to `dataclass` with `async post_commit()` method
- FastAPI upgraded from 0.111.0 to 0.131.0; added pyarrow dependency
- Queue status filtering to only show user-facing tasks (representation, summary, dream); excludes internal infrastructure tasks
### Fixed
- JWT timestamp bug — `JWTParams.t` was evaluated once at class definition time instead of per-instance
- Session cache invalidation on deletion was missing
- `get_peer_card()` now properly propagates `ResourceNotFoundException` instead of swallowing it
- `set_peer_card()` ensures peer exists via `get_or_create_peers()` before updating
- Backup provider failover with proper tool input type safety
- Removed `setup_admin_jwt()` from server startup
- Sentry coroutine detection switched from `asyncio.iscoroutinefunction` to `inspect.iscoroutinefunction`
### Removed
- `explicit.py` and `obex.py` benchmarks replaced by coverage.py and molecular.py
- Claude Code review automation workflow (`.github/workflows/claude.yml`)
- Coverage reporting from default pytest configuration
## [3.0.2] - 2026-01-27
### Added
- Documentation for reasoning_level and Claude Code plugin
### Changed
- Gave dreaming sub-agents better prompting around peer card creation, tweaked overall prompts
### Fixed
- Added message-search fallback for memory search tool, necessary in fresh sessions
- Made FLUSH_ENABLED a config value
- Removed N+1 query in search_messages
## [3.0.1] - 2026-01-27
### Fixed
- Token counting in Explicit Agent Loop
- Backwards compatibility of queue items
## [3.0.0] - 2026-01-19
### Added
- Agentic Dreamer for intelligent memory consolidation using LLM agents
- Agentic Dialectic for query answering using LLM agents with tool use
- Reasoning levels configuration for dialectic (`minimal`, `low`, `medium`, `high`, `max`)
- Prometheus token tracking for deriver and dialectic operations
- n8n integration
- Cloud Events for auditable telemetry
- External Vector Store support for turbopuffer and lancedb with reconciliation flow
### Changed
- API route renaming for consistency
- Dreamer and dialectic now respect peer card configuration settings
- Observations renamed to Conclusions across API and SDKs
- Deriver to buffer representation tasks to normalize workloads
- Local Representation tasks to create singular QueueItems
- getContext endpoint to use `search_query` rather than force `last_user_message`
### Fixed
- Dream scheduling bugs
- Summary creation when start_message_id > end_message_id
- Cashews upgrade to prevent NoScriptError
- Memory leak in `accumulate_metric` call
### Removed
- Peer card configuration from message configuration; peer cards no longer created/updated in deriver process
## [2.5.1] - 2025-12-15
### Fixed
- Backwards compatibility for `message_ids` field in documents to handle legacy tuple format
## [2.5.0] - 2025-12-03
### Added
- Message level configurations
- CRUD operations for observations
- Comprehensive test cases for harness
- Peer level get_context
- Set Peer Card Method
- Manual dreaming trigger endpoint
### Changed
- Configurations to support more flags for fine-grained control of the deriver, peer cards, summaries, etc.
- Working Representations to support more fine-grained parameters
### Fixed
- File uploads to match `MessageCreate` structure
- Cache invalidation strategy
## [2.4.3] - 2025-11-20
### Added
- Redis caching to improve DB IO
- Backup LLM provider to avoid failures when a provider is down
### Changed
- QueueItems to use standardized columns
- Improved Deduplication logic for Representation Tasks
- More finegrained metrics for representation, summary, and peer card tasks
- DB constraint to follow standard naming conventions
## [2.4.2] - 2025-11-03 ## [2.4.2] - 2025-11-03
### Fixed ### Fixed
@ -308,7 +761,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Changed ### Changed
- `/list` endpoints to not require a request body - `/list` endpoints to not require a request body
- `metamessage_type` to `label` with backwards compatability - `metamessage_type` to `label` with backwards compatibility
- Database Provisioning to rely on alembic - Database Provisioning to rely on alembic
- Database Session Manager to explicitly rollback transactions before closing - Database Session Manager to explicitly rollback transactions before closing
the connection the connection
@ -482,7 +935,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Authentication Middleware now implemented using built-in FastAPI Security - Authentication Middleware now implemented using built-in FastAPI Security
module module
- Get by name routes for users and collections now include "name" in slug - Get by name routes for users and collections now include "name" in slug
- Python SDK moved to separate [respository](https://github.com/plastic-labs/honcho-python) - Python SDK moved to separate [repository](https://github.com/plastic-labs/honcho-python)
### Fixed ### Fixed
@ -553,7 +1006,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Changed ### Changed
- session_data is now metadata - session_data is now metadata
- session_data is a JSON field used python `dict` for compatability - session_data is a JSON field used python `dict` for compatibility
## [0.0.2] — 2024-02-01 ## [0.0.2] — 2024-02-01

283
CLAUDE.md
View File

@ -6,15 +6,15 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## What is Honcho? ## What is Honcho?
Honcho is an infrastructure layer for building AI agents with social cognition and theory of mind capabilities. Its primary purposes include: Honcho is an infrastructure layer for building AI agents with memory and social cognition. Its primary purposes include:
- Imbuing agents with a sense of identity - Imbuing agents with a sense of identity
- Personalizing user experiences through understanding user psychology - Personalizing user experiences through understanding user psychology
- Providing a Dialectic API that injects personal context just-in-time - Providing a Chat Endpoint (the Dialectic agent) that injects personal context just-in-time
- Supporting development of LLM-powered applications that adapt to end users - Supporting development of LLM-powered applications that adapt to end users
- Enabling multi-peer sessions where multiple participants (users or agents) can interact - Enabling multi-peer sessions where multiple participants (users or agents) can interact
Honcho leverages the inherent theory-of-mind capabilities of LLMs to build coherent models of user psychology over time, enabling more personalized and effective AI interactions. Honcho leverages the inherent reasoning capabilities of LLMs to build coherent models of user psychology over time, enabling more personalized and effective AI interactions.
## Core Concepts ## Core Concepts
@ -32,25 +32,27 @@ Honcho uses a peer-based model where both users and agents are represented as "p
- **Peer** (formerly User): Any participant in the system (human or AI) - **Peer** (formerly User): Any participant in the system (human or AI)
- **Session**: A conversation context that can involve multiple peers - **Session**: A conversation context that can involve multiple peers
- **Message**: Data units that can represent communication between peers OR arbitrary data ingested by a peer to enhance its global representation - **Message**: Data units that can represent communication between peers OR arbitrary data ingested by a peer to enhance its global representation
- **Collections & Documents**: Internal vector storage for theory-of-mind representations (not exposed via API) - **Collections & Documents**: Internal vector storage for peer representations. Collections are keyed by `(observer, observed)` peer pairs. Collections/Documents are not directly exposed via API, but the observations stored within them are exposed as **Conclusions** (see `/v3/.../conclusions` endpoints).
## Architecture Overview ## Architecture Overview
### API Structure ### API Structure
All API routes follow the pattern: `/v1/{resource}/{id}/{action}` All API routes follow the pattern: `/v3/{resource}/{id}/{action}`. Most "list/search" endpoints are `POST` so they can accept rich filter bodies.
- **Workspaces**: Create, list, update, search - **Workspaces**: Create, list, update, search
- **Peers**: Create, list, update, chat (dialectic), messages, representation - **Peers**: Create, list, update, chat (dialectic), messages, representation
- **Sessions**: Create, list, update, delete, clone, manage peers, get context - **Sessions**: Create, list, update, delete, clone, manage peers, get context
- **Messages**: Create (batch up to 100), list, get, update - **Messages**: Create (batch up to 100), upload (file), list, get, update
- **Conclusions**: Create, list, query (semantic search), delete — the API-facing name for observations stored in `(observer, observed)` collections
- **Keys**: Create scoped JWTs - **Keys**: Create scoped JWTs
- **Webhooks**: Register endpoint, list, delete, test
### Key Features ### Key Features
#### Dialectic API (`/peers/{peer_id}/chat`) #### Chat Endpoint (Dialectic agent) (`/peers/{peer_id}/chat`)
- Provides theory-of-mind informed responses - Provides bespoke responses informed by the representation
- Integrates long-term facts from vector storage - Integrates long-term facts from vector storage
- Supports streaming responses - Supports streaming responses
- Configurable LLM providers - Configurable LLM providers
@ -59,18 +61,11 @@ All API routes follow the pattern: `/v1/{resource}/{id}/{action}`
1. Messages created via API (batch or single) 1. Messages created via API (batch or single)
2. Enqueued for background processing: 2. Enqueued for background processing:
- `representation`: Update peer's theory of mind - `representation`: Update peer's context
- `summary`: Create session summaries - `summary`: Create session summaries
3. Session-based queue processing ensures order 3. Session-based queue processing ensures order
4. Results stored internally in vector DB 4. Results stored internally in vector DB
#### Theory of Mind System
- Multiple implementation methods (conversational, single_prompt, long_term)
- Facts extracted from messages and stored in collections
- Representations combine short-term inference with long-term facts
- Configurable via peer and session feature flags
### Configuration ### Configuration
- Hierarchical config: config.toml + environment variables - Hierarchical config: config.toml + environment variables
@ -91,6 +86,27 @@ All API routes follow the pattern: `/v1/{resource}/{id}/{action}`
- Typechecking: `uv run basedpyright` - Typechecking: `uv run basedpyright`
- Format code: `uv run ruff format src/` - Format code: `uv run ruff format src/`
### SDK Testing
#### TypeScript SDK
**🚨 DO NOT RUN `bun test` DIRECTLY. IT WILL NOT WORK. 🚨**
The TypeScript SDK tests require a running Honcho server with database and Redis. Running `bun test` alone will fail immediately because there's no server. The tests are orchestrated via pytest which handles all the infrastructure setup.
**The ONLY way to run TypeScript SDK tests:**
```bash
# From the monorepo root (not from sdks/typescript/)
uv run pytest tests/ -k typescript
```
**To type-check the TypeScript SDK (this is fine to run directly):**
```bash
cd sdks/typescript && bun run tsc --noEmit
```
### Code Style ### Code Style
- Follow isort conventions with absolute imports preferred - Follow isort conventions with absolute imports preferred
@ -99,73 +115,171 @@ All API routes follow the pattern: `/v1/{resource}/{id}/{action}`
- Line length: 88 chars (Black compatible) - Line length: 88 chars (Black compatible)
- Explicit error handling with appropriate exception types - Explicit error handling with appropriate exception types
- Docstrings: Use Google style docstrings - Docstrings: Use Google style docstrings
- **Never hold a DB session during external calls** (LLM, embedding, HTTP). If a function needs both a DB session and an external call result, compute the external result first and pass it as a parameter. This avoids tying up DB connections during slow network I/O. Use `tracked_db` for short-lived, DB-only operations; pass a shared session when multiple DB-only calls can reuse one connection.
- **Never write through a read-only session** (`tracked_db(..., read_only=True)`, `get_read_db`, `ReadSessionLocal`). These run in AUTOCOMMIT mode with no transaction: writes are NOT blocked by the database — they silently commit immediately, and `begin_nested()` savepoints break. There is no runtime guard; this is enforced by convention only. Use `read_only=True` strictly for SELECT-only windows; anything that mutates (including get-or-create paths) must use a regular write session.
#### Multi-row locking and deadlocks
Tables written concurrently by more than one worker — `documents` (deriver, dreamer, scope backfill/removal, reconciler) and `queue` (every deriver replica) — deadlock when two writers touch an overlapping row set in different orders. Rules:
- **A multi-row `SELECT ... FOR UPDATE` MUST carry an explicit `ORDER BY <pk>`.** Without it Postgres locks in scan order, which differs per plan, so two writers with overlapping sets can cycle. `_apply_document_row_updates` in `src/crud/document.py` is the reference implementation.
- **`WHERE id IN (...)` does NOT impose an order**, so sorting the Python list is a no-op — the list order is discarded and the planner picks `Bitmap Heap Scan` (ctid order), `Index Scan` (id order), or `Seq Scan` per invocation. Deterministic ordering requires either a preceding `SELECT ... ORDER BY id FOR UPDATE` or `WHERE id IN (SELECT id ... ORDER BY id FOR UPDATE)`.
- **`Document.id` is a random nanoid** (`models.py`), so id order is uncorrelated with physical order — an unordered predicate `UPDATE`/`DELETE` is roughly a coin flip against an id-ordered locker per row pair, not a rare edge case. (`QueueItem.id` is an integer identity, so there id order is also chronological.)
- **Prefer no lock at all.** A single `UPDATE ... WHERE <predicate>` acquires row locks as it writes and has no separate lock phase to get wrong. Reach for `FOR UPDATE` only when a value must be read, computed in Python, and written back — that read-modify-write is the only reason `_apply_document_row_updates` locks (it replaced a server-side `func.greatest()`), and `populate_existing=True` is required with it so the identity map doesn't serve a stale pre-lock value. Server-side expressions (`func.greatest`, the JSONB `-` operator) avoid the lock entirely; see `_clear_work_unit_retry_attempts` in `src/deriver/queue_manager.py`.
- `FOR UPDATE SKIP LOCKED` (the reconciler's claim pattern) never waits, so it cannot be a deadlock partner — but holding those locks across an external call still stalls other writers. See the "never hold a DB session during external calls" rule above.
#### Auth scoping
- **`allow_member_read=True` (in `require_auth(...)`) is read-only — NEVER set it on a route that mutates state.** It lets a peer-scoped key reach a session route when its peer is an active member of the session, so on a mutating route it would hand any session member write access (message injection, config mutation, deletion). HTTP method is not a reliable read/write signal here (some read routes use POST for a richer body), so this is enforced by an explicit allowlist in `tests/routes/test_auth_route_policy.py` — adding the flag to a new route fails that test until you consciously add the route to `EXPECTED_MEMBER_READ_ROUTES`, and you must never add a mutating method there.
- **When a member-read route is keyed by another sub-resource** (e.g. `peers/{peer_id}/config`), the handler must additionally confirm a peer-scoped caller only reads its OWN resource (`jwt_params.p == peer_id`, else raise `AuthenticationException`). Membership grants session access, not access to a co-member's data. See `get_peer_config` in `src/routers/sessions.py`.
### Runtime Architecture
Honcho runs as two cooperating processes that share a Postgres database and Redis cache:
- **API server** (`uv run fastapi dev src/main.py`) — handles HTTP, enqueues background work, returns immediately. Hosts the **Dialectic** agent inline (synchronous tool loop during chat requests).
- **Deriver worker** (`uv run python -m src.deriver`) — long-running queue consumer (uvloop). Runs the **Deriver**, **Summarizer**, and **Dreamer** off the queue. Can run multiple instances (`DERIVER_WORKERS`). Also hosts an in-process **Reconciler scheduler** (`src/reconciler/`) that periodically embeds messages with `sync_state='pending'` in `MessageEmbedding` and cleans up stale queue items — embedding generation is decoupled from message creation by design.
### Agent Architecture
Honcho uses several specialized LLM agents. They share tool definitions and the LLM client abstraction in `src/utils/agent_tools.py` + `src/llm/`.
> **Terminology:** what users see as **conclusions** (the public API surface and the term we use in documentation) is called **observations** in code symbols — `create_observations`, `delete_observations`, `get_observation_context`, etc. Doc prose below uses "conclusions"; references to actual code symbols stay as "observations."
#### 1. Deriver (`src/deriver/`)
**Role**: Memory formation through content ingestion.
The Deriver processes batches of incoming messages and extracts conclusions about peers. The current architecture is "minimal deriver" — a **single LLM call** per batch using structured output, not an agentic tool loop. This trades flexibility for cost and predictability.
- **Trigger**: Messages enqueued by `src/deriver/enqueue.py` on message create; consumed by `src/deriver/queue_manager.py` → `consumer.process_item()` → `deriver.process_representation_tasks_batch()`.
- **Output**: Explicit conclusions (direct facts) and deductive conclusions (inferences) saved to `(observer, observed)` collections.
- **Entry point**: `src/deriver/__main__.py` → `queue_manager.main()`.
- **Prompts**: `src/deriver/prompts.py` (`minimal_deriver_prompt`).
- **Custom instructions**: per-workspace/peer guidance can be threaded into the prompt via reasoning configuration; `DERIVER__MAX_CUSTOM_INSTRUCTIONS_TOKENS` caps the addition (default 2000) and `DERIVER__MAX_INPUT_TOKENS` defaults to 25000 to make room.
#### 2. Dialectic (`src/dialectic/`)
**Role**: Analysis and recall for answering queries.
The Dialectic answers questions about peers by strategically gathering context from memory. It is the only tool-using agent on the synchronous request path — it loops over `DIALECTIC_TOOLS` until it has enough context to answer. (The Dreamer specialists also use tools, but run off the queue.)
- **Trigger**: API call to `POST /v3/.../peers/{peer_id}/chat`.
- **Tools** (see `DIALECTIC_TOOLS` in `src/utils/agent_tools.py`): `search_memory`, `search_messages`, `get_observation_context`, `grep_messages`, `get_messages_by_date_range`, `search_messages_temporal`, `get_reasoning_chain`. At the `minimal` reasoning level, a reduced set (`DIALECTIC_TOOLS_MINIMAL`) is used: just `search_memory` + `search_messages`.
- **Reasoning levels**: 5 tiers — `minimal`, `low`, `medium`, `high`, `max` — each with its own model config (see `DialecticLevelSettings` in `src/config.py`).
- **Output**: Natural language response grounded in gathered context. Supports SSE streaming.
- **Entry point**: `src/dialectic/chat.py` → `agentic_chat()` / `agentic_chat_stream()` → `DialecticAgent` (in `src/dialectic/core.py`).
#### 3. Dreamer (`src/dreamer/`)
**Role**: Consolidation and self-improvement of memory.
The Dreamer is an orchestrated multi-specialist system that runs during scheduled "dreams" to consolidate conclusions and build reasoning trees.
- **Trigger**: Scheduled via `DreamScheduler` (`src/dreamer/dream_scheduler.py`) or explicit dream task on the queue.
- **Strategy**: Surprisal-based prioritization (`src/dreamer/surprisal.py`) selects which conclusions to expand. The orchestrator (`orchestrator.run_dream`) runs two specialist phases:
1. **DeductionSpecialist** (`specialists.py`) — produces deductive conclusions from explicit conclusions. Tools: `get_recent_observations`, `search_memory`, `search_messages`, `create_observations_deductive`, `delete_observations`, `update_peer_card`.
2. **InductionSpecialist** — produces inductive conclusions from explicit + deductive conclusions. Tools: same discovery set + `create_observations_inductive`, `update_peer_card`.
- **Reasoning trees** (`src/dreamer/trees/`, migration `f1a2b3c4d5e6_add_reasoning_tree_columns`): each conclusion links to its premises and downstream conclusions, enabling `get_reasoning_chain` traversal at recall time.
- **Output**: Deductive/inductive conclusions, consolidated redundancies, updated peer cards.
- **Entry point**: `src/dreamer/orchestrator.py` → `process_dream()` (the package-level export from `src/dreamer/__init__.py`), which wraps `run_dream()`.
#### 4. Summarizer (`src/utils/summarizer.py`)
**Role**: Two-tier session summarization (direct LLM call — no agentic tools).
- **Trigger**: Runs as part of the queue pipeline alongside representation tasks.
- **Tiers**: short summary every `SUMMARY_MESSAGES_PER_SHORT_SUMMARY` messages (default 20); long summary every `SUMMARY_MESSAGES_PER_LONG_SUMMARY` (default 60). Token caps configurable via `SUMMARY_MAX_TOKENS_SHORT` / `SUMMARY_MAX_TOKENS_LONG`.
#### Shared Agent Infrastructure
- **Tool definitions** (`src/utils/agent_tools.py`): unified `TOOLS` dict; per-agent lists (`DIALECTIC_TOOLS`, `DIALECTIC_TOOLS_MINIMAL`, `DREAMER_TOOLS`, `DEDUCTION_SPECIALIST_TOOLS`, `INDUCTION_SPECIALIST_TOOLS`).
- **LLM subsystem** (`src/llm/`): provider-agnostic `honcho_llm_call()`. Backends in `src/llm/backends/` (`anthropic.py`, `gemini.py`, `openai.py`). Includes prompt caching (`caching.py`), structured output (`structured_output.py`), tool loop (`tool_loop.py`), history adapters for cross-provider message formats, and a model registry. Per-retry provider selection is pinned via an `AttemptPlan` so stream-final retries don't bounce back to primary after the tool loop has settled on fallback.
- **Per-agent model config**: each agent has its own `MODEL_CONFIG` in `src/config.py` with fallback chains (see `ConfiguredModelSettings`, `FallbackModelSettings`).
- **Telemetry**: cloudevents in `src/telemetry/events/` cover API routes, dialectic, dream, deletion, reconciliation, representation, and per-call LLM accounting (`llm.py` — `LLMCallCompletedEvent` fires once per provider hit with full cost-attribution context). High-volume events are sampled deterministically per `run_id` via `TelemetrySettings.HIGH_VOLUME_SAMPLE_RATE`.
- **Prometheus metrics** (`src/telemetry/prometheus/`): every metric carries a `namespace` label and every recorder is fail-soft (a metrics error never propagates into a request or a worker loop). Counter children with a *bounded* label domain are zero-initialized per process at startup — `initialize_bounded_metrics(instance_type=...)`, called from the `src/main.py` lifespan (`api`) and `src/deriver/__main__.py` (`deriver`) — so an absent series means a broken scrape rather than "nothing happened". Two consequences worth knowing before touching telemetry:
- **Adding a `BaseEvent` subclass requires adding its `_event_type` to `ALL_EVENT_TYPES`** in `src/telemetry/events/__init__.py` (and to `HIGH_VOLUME_EVENT_TYPES` if `_volume_class == "high_volume"`). Enforced by the drift guards in `tests/telemetry/test_metric_zero_init.py`, which assert set-equality against the discovered subclasses.
- **A service-wide, non-additive gauge must be refreshed by every replica on its own timer**, and aggregated with `max()`/`avg()`, never `sum()`. `message_embeddings_pending` is the example: it reports a DB-global count, so it is driven from `ReconcilerScheduler._scheduler_loop` (runs on all replicas) rather than from the work-unit-deduped reconciliation cycle — otherwise, combined with the zero-init, every replica that never won the work unit would export a confident permanent `0`.
### Project Structure ### Project Structure
``` ```
src/ src/
├── main.py # FastAPI app setup with middleware and exception handlers ├── main.py # FastAPI app: middleware, routers, lifespan, exception handlers
├── models.py # SQLAlchemy ORM models with proper type annotations ├── models.py # SQLAlchemy ORM models (Workspace/Peer/Session/Message/
├── schemas.py # Pydantic validation schemas for API │ # MessageEmbedding/Collection/Document/QueueItem/...)
├── config.py # Configuration management ├── config.py # Pydantic-settings configuration (very large; see README)
├── db.py # Database connection and session management ├── db.py # Engine + session/context management (request_context var)
├── dependencies.py # Dependency injection (DB sessions) ├── dependencies.py # FastAPI DI (tracked_db, etc.)
├── exceptions.py # Custom exception types ├── exceptions.py # Custom exception types (HonchoException + subclasses)
├── security.py # JWT authentication ├── security.py # JWT authentication
├── embedding_client.py # Embedding service client ├── embedding_client.py # Embedding provider client (configurable dimensions
├── crud/ # Database operations │ # via EMBEDDING_MODEL_CONFIG__DIMENSIONS_MODE)
│ ├── __init__.py ├── schemas/ # Pydantic schemas
│ ├── collection.py # Collection CRUD operations │ ├── api.py # Public API request/response schemas
│ ├── deriver.py # Deriver-related CRUD operations │ ├── configuration.py # Per-resource configuration schemas
│ ├── document.py # Document CRUD operations │ └── internal.py # Internal-only schemas (queue payloads, etc.)
│ ├── message.py # Message CRUD operations ├── crud/ # Per-resource DB operations
│ ├── peer.py # Peer CRUD operations │ ├── collection.py, deriver.py, document.py, message.py
│ ├── peer_card.py # Peer Card CRUD operations │ ├── peer.py, peer_card.py, representation.py (RepresentationManager)
│ ├── representation.py # RepresentationManager and representation operations │ ├── session.py, webhook.py, workspace.py
│ ├── session.py # Session CRUD operations ├── routers/ # FastAPI route handlers (all under /v3)
│ ├── webhook.py # Webhook CRUD operations │ ├── workspaces.py, peers.py (dialectic /chat lives here), sessions.py
│ └── workspace.py # Workspace CRUD operations │ ├── messages.py, conclusions.py, keys.py, webhooks.py
├── dialectic/ # Dialectic API implementation ├── dialectic/ # Dialectic agent — runs inline per chat request
│ ├── __init__.py │ ├── chat.py # agentic_chat() / agentic_chat_stream()
│ ├── chat.py # Chat functionality │ ├── core.py # DialecticAgent (the tool-loop driver)
│ ├── prompts.py # Prompt templates │ └── prompts.py
│ └── utils.py # Dialectic utilities ├── deriver/ # Background queue consumer (separate process)
├── routers/ # API endpoints │ ├── __main__.py # `python -m src.deriver` entry point
│ ├── workspaces.py │ ├── queue_manager.py # QueueManager + main() loop
│ ├── peers.py │ ├── consumer.py # process_item dispatcher (representation / deletion / reconciler)
│ ├── sessions.py │ ├── deriver.py # "minimal deriver" — single-LLM-call batch processor
│ ├── messages.py │ ├── enqueue.py # API → queue producer
│ ├── keys.py │ └── prompts.py
│ └── webhooks.py # Webhook endpoints ├── dreamer/ # Memory consolidation (runs off the queue)
├── deriver/ # Background processing system │ ├── orchestrator.py # run_dream() / process_dream()
│ ├── __init__.py │ ├── specialists.py # DeductionSpecialist + InductionSpecialist
│ ├── __main__.py # Deriver entry point │ ├── dream_scheduler.py
│ ├── consumer.py # Message consumer │ ├── surprisal.py # Surprisal-based conclusion prioritization
│ ├── deriver.py # Main deriver logic │ └── trees/ # Reasoning-tree primitives
│ ├── enqueue.py # Queue operations ├── reconciler/ # In-process scheduler hosted by the deriver worker
│ ├── prompts.py # Deriver prompts │ ├── scheduler.py # ReconcilerScheduler (started from queue_manager.py)
│ ├── queue_manager.py # Queue management │ ├── sync_vectors.py # Embeds MessageEmbedding rows with sync_state='pending'
│ ├── queue_payload.py # Queue payload schemas │ └── queue_cleanup.py # Removes stale queue items
│ └── utils.py # Deriver utilities ├── llm/ # Provider-agnostic LLM client subsystem
├── utils/ # Utilities │ ├── api.py, backend.py, executor.py, runtime.py, registry.py
│ ├── __init__.py │ ├── caching.py, structured_output.py, tool_loop.py, conversation.py
│ ├── clients.py # LLM client abstraction │ ├── history_adapters.py, request_builder.py, credentials.py, types.py
│ ├── files.py # File handling utilities │ └── backends/ # anthropic.py, gemini.py, openai.py
│ ├── filter.py # Query filtering utilities ├── cache/ # Redis cache abstraction (cashews-backed)
│ ├── formatting.py # Message formatting utilities │ └── client.py
│ ├── logging.py # Logging configuration ├── vector_store/ # Optional external vector stores (pgvector is default,
│ ├── search.py # Search functionality │ │ # implemented via MessageEmbedding/Document in models+crud)
│ ├── shared_models.py # Shared data models │ ├── lancedb.py
│ ├── summarizer.py # Session summarization │ └── turbopuffer.py
│ └── types.py # Type definitions ├── telemetry/ # Observability
└── webhooks/ # Webhook system │ ├── emitter.py # CloudEvents emitter
├── events.py # Webhook event definitions │ ├── logging.py # Logging helpers + route-template extraction
├── webhook_delivery.py # Webhook delivery logic │ ├── metrics_collector.py, reasoning_traces.py, sentry.py
└── README.md # Webhook documentation │ ├── events/ # Event type definitions
│ └── prometheus/ # Prometheus metric definitions
├── utils/ # Cross-cutting utilities
│ ├── agent_tools.py # Tool definitions + per-agent tool lists
│ ├── summarizer.py # Two-tier session summarizer
│ ├── representation.py # Representation formatting (distinct from crud/representation.py)
│ ├── search.py, filter.py, formatting.py
│ ├── tokens.py # tiktoken-based counting
│ ├── work_unit.py, queue_payload.py
│ ├── config_helpers.py, json_parser.py, files.py
│ └── types.py
└── webhooks/ # Webhook delivery
├── events.py
└── webhook_delivery.py
``` ```
- Tests in pytest with fixtures in tests/conftest.py - Tests in pytest with fixtures in tests/conftest.py; subdirs mirror src/ (`tests/deriver/`, `tests/dialectic/`, etc.) plus `tests/bench/` (perf benchmarks), `tests/integration/`, `tests/live_llm/` (gated by `--live-llm`), and `tests/unified/` (the unified runner).
- Use environment variables via python-dotenv (.env) - Use environment variables via python-dotenv (.env). Config precedence: env > .env > config.toml > defaults.
### Database Design ### Database Design
@ -178,13 +292,18 @@ src/
### Key Architectural Decisions ### Key Architectural Decisions
1. **Multi-Peer Sessions**: Sessions can have multiple participants with different observation settings 1. **Peer Paradigm**: humans and AI agents are unified as "Peers"; many-to-many with Sessions. Internal vector storage (Collections/Documents) is keyed by `(observer, observed)` peer pairs — the same mechanism powers self-representation (`observer == observed`) and cross-peer modeling.
2. **Flexible Theory of Mind**: Pluggable ToM implementations (conversational, single_prompt, long_term) 2. **Multi-Peer Sessions**: Sessions can have multiple participants with different observation settings.
3. **Background Processing**: Async queue system for expensive operations 3. **API server / worker split**: API enqueues, deriver worker process consumes. Never block HTTP on LLM work. The Reconciler runs as an in-process scheduler inside the deriver, handling async embedding sync and queue cleanup.
4. **Provider Abstraction**: Model client supports multiple LLM providers 4. **"Minimal" deriver**: memory formation is a single structured-output LLM call per batch, not an agentic tool loop. Predictable cost, lower latency. The Dialectic is the one true tool-using agent.
5. **Scoped Authentication**: JWTs can be scoped to workspace, peer, or session level 5. **Provider-agnostic LLM layer** (`src/llm/`): all model calls go through `honcho_llm_call()`. Backends (`anthropic`, `gemini`, `openai`) sit behind a registry; per-agent `MODEL_CONFIG` with fallback chains is resolved at call time.
6. **Batch Operations**: Support for bulk message creation (up to 100 messages) 6. **Dialectic reasoning tiers**: 5 levels (`minimal` → `max`); each level has its own model config and tool set (`minimal` uses a reduced toolset).
7. **Session History**: Two-tier summarization (short every 20 messages, long every 60) 7. **Hybrid search**: Postgres FTS (GIN index on `to_tsvector('english', content)`) + vector similarity (HNSW on `MessageEmbedding.embedding`). `MessageEmbedding` is a separate table from `Message` with its own `sync_state` so embedding is decoupled from message creation.
8. **Pluggable external vector stores**: defaults to pgvector inline; can swap to turbopuffer or lancedb (`VECTOR_STORE_*` config; `src/vector_store/`).
9. **Composite-FK multi-tenancy**: `workspace_name` participates in nearly every composite FK. Cross-workspace data leakage is structurally impossible at the schema level.
10. **Scoped Authentication**: JWTs can be scoped to workspace, peer, or session level.
11. **Batch Operations**: Bulk message creation up to 100 messages per request.
12. **Session History**: Two-tier summarization — short every `SUMMARY_MESSAGES_PER_SHORT_SUMMARY` (default 20), long every `SUMMARY_MESSAGES_PER_LONG_SUMMARY` (default 60).
### Error Handling ### Error Handling

View File

@ -1,176 +1,414 @@
# Contributing to Honcho # Contributing to Honcho
Thank you for your interest in contributing to Honcho! This guide outlines the process for contributing to the project and our development conventions. <!-- This file is mirrored at docs/v3/contributing/guidelines.mdx. Update both. -->
## Getting Started Thanks for your interest in contributing. This guide covers how work gets accepted, how
Honcho is put together, and what a mergeable pull request looks like.
Before you start contributing, please: Honcho is a small team maintaining a project that gets more proposals than we can review.
The rules below exist so that the work you do has somewhere to land — not to keep you out.
1. **Set up your development environment** - Follow the [Local Development guide](./README.md#local-development) in the README to get Honcho running locally. ## Contents
2. **Join our community** - Feel free to join us in our [Discord](http://discord.gg/plasticlabs) to discuss your changes, get help, or ask questions. - [Before you write code](#before-you-write-code)
- [What gets prioritized](#what-gets-prioritized)
- [If you're an agent](#if-youre-an-agent)
- [How Honcho works](#how-honcho-works)
- [Where to change what](#where-to-change-what)
- [Local setup](#local-setup)
- [Making the change](#making-the-change)
- [Opening the pull request](#opening-the-pull-request)
- [Reporting bugs and requesting features](#reporting-bugs-and-requesting-features)
- [Security](#security)
- [License](#license)
3. **Review existing issues** - Check the [issues tab](https://github.com/plastic-labs/honcho/issues) to see what's already being worked on or to find something to contribute to. ## Before you write code
## Contribution Workflow **Every pull request needs an issue, and that issue needs the `maintainer-approved` label.**
### 1. Fork and Clone A pull request that is not linked to an approved issue gets labelled
`needs-approved-issue`, with a comment explaining why. You then have 72 hours to link one
before it is closed automatically. Reopening costs nothing once the link is in place. This
is automated. We do this because an unreviewable backlog helps nobody: a PR against an
unapproved issue is work you did that we may not be able to merge, no matter how good it
is.
1. Fork the repository on GitHub So, in order:
2. Clone your fork locally:
1. **Find approved work.** Browse
[issues labelled `maintainer-approved`](https://github.com/plastic-labs/honcho/issues?q=is%3Aissue+is%3Aopen+label%3Amaintainer-approved).
That label is the queue of things we have agreed should be built. Anything in it is fair
game — comment on the issue to claim it.
2. **Or open an issue and get it approved.** Use the
[issue templates](https://github.com/plastic-labs/honcho/issues/new/choose). Maintainers
triage and apply the label.
3. **If you feel strongly about an issue, come to [Discord](https://discord.gg/honcho).**
This is the fastest path by a wide margin. Maintainers are more active there than in the
issue tracker, and a five-minute conversation about what you want to build usually
resolves whether it fits before either side spends real time on it.
4. **Then open the PR** and link the issue — either `Fixes #123` in the description, or
**Development → link an issue** in the sidebar. Both work.
Small exceptions we will not be pedantic about: fixing a typo, a broken link, or an
obviously wrong code sample. Open the PR, explain it in one line, and we will sort out the
issue linkage.
## What gets prioritized
Roughly, work on Honcho falls along these axes. Knowing which one your idea sits on tells
you a lot about how likely it is to get approved.
| Axis | What it covers |
| --- | --- |
| **Observability** | Understanding how Honcho behaves in production — telemetry, tracing, CloudEvents, metrics. |
| **Memory quality** | Better conclusions from the same input — the deriver, dreamer, and dialectic; eval results. |
| **Developer experience** | Fitting cleanly into more application architectures — SDKs, scopes, composable peers, the CLI. |
| **Breadth of input** | Widening what Honcho can ingest and represent — multimodal and non-conversational data. |
| **Ubiquity** | Reachable wherever a developer already works — integrations, self-hosting, alternate vector-store and inference backends, local-first defaults. |
| **Reliability and cost** | Trustworthy in production — connection and concurrency hardening, queue throughput, cost per token. |
In practice, **Ubiquity** and **Developer experience** are where outside contributions land
most easily. A new integration, a self-hosting rough edge, a vector-store or inference
backend, an SDK ergonomics fix — these are additive and rarely collide with work already in
flight.
Changes to the reasoning pipeline itself — deriver prompts, dialectic tool design, dreamer
strategy — are the hardest to accept from outside. Not because they are unwelcome, but
because they are measured against eval results we run internally, and they frequently
conflict with in-flight work. Talk to us in Discord first, always.
## If you're an agent
If you are a coding agent working on this repository, read this section before writing code.
The most common failure we see is a well-formed, well-tested pull request against an issue
that was never approved. That gets closed, and the work is wasted.
- **Check the gate first.** Before writing code:
```bash ```bash
git clone https://github.com/YOUR_USERNAME/honcho.git gh issue view <N> --repo plastic-labs/honcho --json number,title,state,labels
cd honcho
``` ```
3. Add the upstream repository as a remote: Stop if there is no issue number, if the issue is closed, or if `maintainer-approved` is
not in the labels. Report that to the person you are working with instead of proceeding.
- **Do not open a PR in order to establish the issue link afterwards.** The issue comes
first.
- **Do not report checks you did not run.** If you did not execute the test command, say so.
A PR body claiming a green run that did not happen costs a maintainer more time than no
claim at all.
- **Use the checklist.** [`skills/pre-pr/SKILL.md`](./skills/pre-pr/SKILL.md) in this repo
encodes the gate, the test-layer matrix, and the PR body format. If your harness supports
skills, invoke it rather than reimplementing the checks.
## How Honcho works
Enough architecture to find your way around. For the user-facing model — what a Peer is, what
`get_context` returns — see [Core Concepts in the README](./README.md#core-concepts) and the
[documentation](https://honcho.dev/docs/).
### Two processes
Honcho runs as two cooperating processes over a shared Postgres database and Redis cache.
| | API server | Deriver worker |
| --- | --- | --- |
| Start | `uv run fastapi dev src/main.py` | `uv run python -m src.deriver` |
| Entry | `src/main.py` | `src/deriver/__main__.py` |
| Does | Serves HTTP, enqueues background work, returns immediately | Consumes the queue: Deriver, Summarizer, Dreamer, Reconciler |
| Hosts | The Dialectic agent, inline on the request path | Everything else |
The split is the load-bearing design decision: **an HTTP request never blocks on LLM work**,
with the single exception of the Dialectic chat endpoint, which is synchronous by nature.
If you are adding something slow, it belongs in the worker.
The deriver is a separate process. If messages go in and nothing ever comes out, the usual
cause is that nobody started it.
### The path of a message
Worth tracing once, because it crosses most of the codebase:
1. `POST /v3/workspaces/{w}/sessions/{s}/messages` lands in `src/routers/messages.py`.
2. The row is written, then `enqueue()` in `src/deriver/enqueue.py` creates `queue_item`
rows — one set of work per observing peer.
3. `src/deriver/queue_manager.py` polls the queue, claiming work units so that messages in a
session are processed in order.
4. `process_item()` in `src/deriver/consumer.py` dispatches on task type — representation,
summary, deletion, reconciliation.
5. For a representation task, `process_representation_tasks_batch()` in
`src/deriver/deriver.py` makes **one structured-output LLM call for the whole batch** and
writes the resulting conclusions into the collection keyed by the
`(observer, observed)` peer pair.
6. Later, `src/dialectic/` reads those conclusions back at recall time to answer a chat
request.
Embedding is deliberately *not* on this path. `MessageEmbedding` rows are written with
`sync_state='pending'` and embedded asynchronously by the Reconciler
(`src/reconciler/sync_vectors.py`), which runs on a scheduler inside the deriver process.
### The four agents
They share tool definitions in `src/utils/agent_tools.py` and the provider-agnostic LLM
client in `src/llm/`. Each has its own `MODEL_CONFIG` with a fallback chain in
`src/config.py`.
| Agent | Where | Shape |
| --- | --- | --- |
| **Deriver** | `src/deriver/` | A single structured-output call per message batch. Not a tool loop — this is a deliberate cost and latency tradeoff. |
| **Dialectic** | `src/dialectic/` | The one tool-using agent on the request path. Loops over tools until it can answer. Five reasoning tiers from `minimal` to `max`, each with its own model and tool set. |
| **Dreamer** | `src/dreamer/` | Off-queue consolidation. Two specialist phases (deduction, then induction) that build reasoning trees over existing conclusions. |
| **Summarizer** | `src/utils/summarizer.py` | Direct LLM call, no tools. Two tiers — short and long summaries at different message counts. |
Prompts live in `src/deriver/prompts.py`, `src/dialectic/prompts.py`, and
`src/dreamer/specialists.py`.
### A note on naming
What the public API and documentation call **conclusions** are called **observations**
throughout the code — `create_observations`, `get_observation_context`, and so on. Likewise
**collections** and **documents** are internal storage concepts that are not exposed
directly through the API. Do not rename across that boundary in a drive-by change; the
public and internal vocabularies are being reconciled deliberately.
## Where to change what
| I want to change... | Start here |
| --- | --- |
| An HTTP endpoint | `src/routers/` — one module per resource |
| A database query | `src/crud/` — mirrors the router layout |
| The database schema | `src/models.py`, plus a migration in `migrations/versions/` |
| A configuration value | `src/config.py`, and add it to `config.toml.example` and `.env.template` |
| A tool an agent can call | `src/utils/agent_tools.py` — definitions plus the per-agent tool lists |
| A prompt | `src/deriver/prompts.py`, `src/dialectic/prompts.py`, `src/dreamer/specialists.py` |
| LLM provider behavior | `src/llm/backends/` — `anthropic.py`, `gemini.py`, `openai.py` |
| Embeddings or vector storage | `src/embedding_client.py`, `src/vector_store/` |
| Telemetry or metrics | `src/telemetry/` — see the notes in `CLAUDE.md` before adding an event type |
| Authentication and scoping | `src/security.py`, `src/dependencies.py` |
| The Python or TypeScript SDK | `sdks/python/`, `sdks/typescript/` |
| The CLI | `honcho-cli/` |
| The MCP server | `mcp/` |
| Public documentation | `docs/v3/` — Mintlify; nav lives in `docs/docs.json` |
Tests in `tests/` mirror `src/`. `CLAUDE.md` at the repo root has more detail on house
conventions, and is worth skimming even if you are not using an agent.
## Local setup
To run a personal instance, install the CLI (`uv tool install honcho-cli`) and then run `honcho start --setup` (Docker + an LLM provider key — not the Honcho API key from `honcho init`) — [CLI in the README](./README.md#cli).
To **develop this repo**, clone it and:
```bash ```bash
git remote add upstream https://github.com/plastic-labs/honcho.git uv sync # create the venv and install dependencies
uv run alembic upgrade head # apply migrations
``` ```
### 2. Create a Branch Run both processes, in separate terminals:
Create a new branch for your feature or bug fix: ```bash
uv run fastapi dev src/main.py # API server, reloads on change
uv run python -m src.deriver # background worker
```
Everything Python goes through `uv run`. Redis is optional for local development; without it
caching is simply disabled.
### Running without a model provider
`src/mock_provider/` is a deterministic, OpenAI-compatible endpoint, so you can run the full
stack with no provider account, no API key, and no spend. It answers `/v1/chat/completions`
and `/v1/embeddings` with obviously-synthetic content derived from the request, and the same
request always produces the same response. Run it from the standard image or the repo:
```bash
uv run fastapi run --host 0.0.0.0 --port 8106 src/mock_provider/main.py
```
Then point Honcho at it. All three variables are required:
```bash
export LLM_OPENAI_API_KEY=any-non-empty-string # only truthiness is checked
export LLM_OPENAI_BASE_URL=http://localhost:8106/v1
export EMBEDDING_MODEL_CONFIG__OVERRIDES__BASE_URL=http://localhost:8106/v1
```
The key's *value* is never checked — the mock reads no Authorization header, and Honcho only
tests it for truthiness before building the client (`src/llm/registry.py`). Set the base URL
without it and the client is never constructed, so the base URL is silently ignored. Keep the
value obviously fake, so a module that ever escapes the override 401s rather than spends.
Embeddings resolve through a separate client that reads the base URL only from the per-module
override, so without the third variable your embedding calls go to `api.openai.com` for real.
Do not set any per-module credential override (`..._OVERRIDES__API_KEY` / `API_KEY_ENV`) —
that makes the module ignore the global base URL.
Two things to know:
- **A repo `.env` beats your exported environment.** `src/config.py` calls
`load_dotenv(override=True)` at import, so a stale `.env` silently wins over the variables
above. Set `PYTHON_DOTENV_DISABLED=1` (and `HONCHO_CONFIG_TOML_DISABLED=1` for a local
`config.toml`) when you need the environment to be the only input.
- **Mock embeddings are hash-derived and carry no semantic similarity.** Two paraphrases are as
far apart as two unrelated strings. Recall against this provider must use lexical/full-text
search; anything asserting on vector ranking needs a real embedding provider.
## Making the change
### Branches and commits
```bash ```bash
git checkout -b feature/your-feature-name git checkout -b feature/your-feature-name
# or
git checkout -b fix/your-bug-fix-name
``` ```
**Branch naming conventions:** Prefixes: `feature/`, `fix/`, `docs/`, `refactor/`, `test/`.
- `feature/description` - for new features Commits follow [Conventional Commits](https://www.conventionalcommits.org/), enforced by a
- `fix/description` - for bug fixes `commit-msg` hook:
- `docs/description` - for documentation updates
- `refactor/description` - for code refactoring
- `test/description` - for adding or updating tests
### 3. Make Your Changes
- Write clean, readable code that follows our coding standards (see below)
- Add tests for new functionality
- Update documentation as needed
- Make sure your changes don't break existing functionality
### 4. Commit Your Changes
We follow conventional commit standards. Format your commit messages as:
```
type(scope): description
[optional body]
[optional footer]
```
**Types:**
- `feat`: A new feature
- `fix`: A bug fix
- `docs`: Documentation only changes
- `style`: Changes that do not affect the meaning of the code
- `refactor`: A code change that neither fixes a bug nor adds a feature
- `test`: Adding missing tests or correcting existing tests
- `chore`: Changes to the build process or auxiliary tools
**Examples:**
```bash ```bash
git commit -m "feat(api): add new dialectic endpoint for user insights" git commit -m "feat(api): add new dialectic endpoint for user insights"
git commit -m "fix(db): resolve connection pool timeout issue" git commit -m "fix(db): resolve connection pool timeout issue"
git commit -m "docs(readme): update installation instructions"
``` ```
### 5. Submit a Pull Request Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`.
1. Push your branch to your fork: ### Pre-commit hooks
Install them. CI runs the same checks, and it is much faster to find out locally.
```bash ```bash
git push origin your-branch-name uv run pre-commit install \
--hook-type pre-commit \
--hook-type commit-msg \
--hook-type pre-push
``` ```
2. Create a pull request on GitHub from your branch to the `main` branch At **commit** time: ruff lint and format, biome for TypeScript, basedpyright, bandit,
markdownlint, and file hygiene. At **push** time: pytest, the alembic migration tests, and
the SDK builds.
3. Fill out the pull request template with: That split matters — **a clean commit is not a clean push.** The test suite only runs at
- A clear description of what changes you've made `pre-push`, so the first time you see test failures may be well after you thought you were
- The motivation for the changes done.
- Any relevant issue numbers (use "Closes #123" to auto-close issues)
- Screenshots or examples if applicable
## Coding Standards Run them by hand at any time:
### Python Code Style ```bash
uv run pre-commit run --all-files
uv run pre-commit run ruff --all-files
```
- Follow [PEP 8](https://www.python.org/dev/peps/pep-0008/) style guidelines Or the individual tools:
- Use [Black](https://black.readthedocs.io/) for code formatting (we may add this to CI in the future)
- Use type hints where possible
- Write docstrings for functions and classes using Google style docstrings
### Code Organization ```bash
uv run ruff check src/
uv run ruff format src/
uv run basedpyright
```
- Keep functions focused and single-purpose ### Tests
- Use meaningful variable and function names
- Add comments for complex logic
- Follow existing patterns in the codebase
### Testing Write tests for new functionality, in the directory under `tests/` that mirrors the code you
changed. Which layer you need depends on what you touched:
- Write unit tests for new functionality | What you changed | What to run |
- Ensure existing tests pass before submitting | --- | --- |
- Use descriptive test names that explain what is being tested | Anything in `src/` | Unit tests in the matching `tests/` tree — `uv run pytest tests/...` |
- Mock external dependencies appropriately | Deriver, dialectic, dreamer, or the LLM path | Unit tests, and consider `tests/live_llm/` (gated behind `--live-llm`) |
| Queue behavior, config hierarchy, multi-turn flows, SDK contracts | `uv run python -m tests.unified.run` |
| A `/v3` endpoint or deriver queue behavior | Actually run the stack and exercise it — not just pytest |
| A migration | `uv run python scripts/run_alembic_tests.py`; every revision needs a test file |
The TypeScript SDK tests need a running server with a database and Redis, which pytest
orchestrates. Run them with `uv run pytest tests/ -k typescript` from the repo root —
`bun test` on its own will fail. To type-check the SDK alone:
`cd sdks/typescript && bun run tsc --noEmit`.
### Documentation ### Documentation
- Update relevant documentation for new features Update docs in the same PR when you change a public surface: `/v3` endpoints, SDK exports,
- Include examples in docstrings where helpful or anything in `config.toml` / settings. Docs live in `docs/v3/`, and new pages need an entry
- Keep README and other docs up to date with changes in `docs/docs.json` or they will not appear in the nav.
## Review Process ## Opening the pull request
1. **Automated checks** - Your PR will run through automated checks including tests and linting ### Leave "Allow edits by maintainers" checked
2. **Project maintainer review** - A project maintainer will review your code for:
- Code quality and adherence to standards
- Functionality and correctness
- Test coverage
- Documentation completeness
3. **Discussion and iteration** - You may be asked to make changes or clarifications
4. **Approval and merge** - Once approved, your PR will be merged into `main`
## Types of Contributions This is the single most useful thing you can do to get your PR merged quickly.
We welcome various types of contributions: Most contributor PRs arrive nearly right, needing a rename, a missing test, or a lint fix.
If we can push that commit ourselves, it merges the same day. If we cannot, it becomes a
review comment, and then we wait — sometimes for weeks — for a round trip on a two-line
change.
- **Bug fixes** - Help us squash bugs and improve stability GitHub checks the box by default when you fork. Leave it checked.
- **New features** - Add functionality that benefits the community
- **Documentation** - Improve or expand our documentation
- **Tests** - Increase test coverage and reliability
- **Performance improvements** - Help make Honcho faster and more efficient
- **Examples and tutorials** - Help other developers use Honcho
## Issue Reporting One caveat worth knowing: **the option does not exist on forks owned by an organization.**
If you have the choice, fork from your personal account.
When reporting bugs or requesting features: ### Fill out the template
1. Check if the issue already exists `.github/pull_request_template.md` asks for a description, proofs, and the issue checkbox.
2. Use the appropriate issue template
3. Provide clear reproduction steps for bugs
4. Include relevant environment information
5. Be specific about expected vs actual behavior
## Questions and Support "Proofs" means evidence the change works: the command you ran and its result, a log snippet,
a screenshot, the failing case before and after. This is the section that most determines
how fast your PR gets reviewed. Do not add sections to the template.
- **General questions** - Join our [Discord](http://discord.gg/plasticlabs) Link the issue so the gate can see it: `Fixes #123` in the description, or the
- **Bug reports** - Use GitHub issues **Development** section of the sidebar. The gate reads GitHub's own resolved issue links, so
- **Feature requests** - Use GitHub issues with the feature request template either route works — but a bare `#123` mention is only a reference and does not count.
- **Security issues** - Please email us privately rather than opening a public issue
### Review
1. Automated checks run — tests, linting, static analysis, and the issue gate.
2. A maintainer reviews for correctness, test coverage, and fit with the surrounding code.
`.github/CODEOWNERS` routes the request to whoever owns the area you touched.
3. You may be asked for changes. Or we may just push them, if you left edits enabled.
4. Once approved, we merge to `main`.
If a PR goes quiet, nudge us in [Discord](https://discord.gg/honcho).
Please respond within 7 days - we may close any PRs that have seen no activity within a 7 day
window. If you need more time, let us know in the PR comments.
## Reporting bugs and requesting features
Use the [issue templates](https://github.com/plastic-labs/honcho/issues/new/choose). There is
one per kind of report, and picking the right one is most of what gets an issue triaged
quickly:
- **Bug report** — something is broken or behaves incorrectly
- **Memory / recall quality** — the deriver or dialectic returns poor, wrong, or missing context
- **Feature request** — a new capability or API surface
- **Integration request** — plugins, framework integrations, app-store listings
- **Documentation issue** — anything wrong or missing in the docs
- **General questions** — not an issue at all; ask in [Discord](https://discord.gg/honcho)
Before opening one, search existing issues, including closed ones.
A good bug report has the Honcho version or commit, whether you are self-hosted or on
`api.honcho.dev`, the steps to reproduce, and what you expected instead. If it involves the
deriver, logs from the worker process are usually the thing we ask for first.
**Redact before you post.** Issues are public, and Honcho stores conversational data — strip
API keys, JWTs, and production user content out of any log or payload you attach.
## Security
Do not open a public issue for a suspected vulnerability. Report it privately through
[GitHub Private Vulnerability Reporting](https://github.com/plastic-labs/honcho/security/advisories/new),
which is the preferred channel, or by email. See [SECURITY.md](./SECURITY.md) for what to
include, and note that Honcho does not operate a bug bounty.
## License ## License
By contributing to Honcho, you agree that your contributions will be licensed under the same [AGPL-3.0 License](./LICENSE) that covers the project. By contributing to Honcho, you agree that your contributions will be licensed under the same
[AGPL-3.0 License](./LICENSE) that covers the project.
Thank you for helping make Honcho better! 🫡 Thank you for helping make Honcho better! 🫡

View File

@ -1,46 +1,77 @@
# syntax=docker/dockerfile:1
# https://pythonspeed.com/articles/base-image-python-docker-images/ # https://pythonspeed.com/articles/base-image-python-docker-images/
# https://testdriven.io/blog/docker-best-practices/ # https://testdriven.io/blog/docker-best-practices/
FROM python:3.11-slim-bullseye FROM python:3.13-slim-bookworm AS builder
COPY --from=ghcr.io/astral-sh/uv:0.4.9 /uv /bin/uv COPY --from=ghcr.io/astral-sh/uv:0.9.24 /uv /bin/uv
# Set Working directory
WORKDIR /app WORKDIR /app
RUN addgroup --system app && adduser --system --group app
RUN chown -R app:app /app
USER app
# Enable bytecode compilation # Enable bytecode compilation
ENV UV_COMPILE_BYTECODE=1 ENV UV_COMPILE_BYTECODE=1
# Copy from the cache instead of linking since it's a mounted volume # Copy from the cache instead of linking since it's a mounted volume
ENV UV_LINK_MODE=copy ENV UV_LINK_MODE=copy
# Install the project's dependencies using the lockfile and settings # Python optimizations
RUN --mount=type=cache,target=/root/.cache/uv \ ENV PYTHONDONTWRITEBYTECODE=1
--mount=type=bind,source=uv.lock,target=uv.lock \ ENV PYTHONUNBUFFERED=1
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --no-install-project --no-dev
# Copy only requirements to cache them in docker layer # Copy only requirements to cache them in docker layer
COPY uv.lock pyproject.toml /app/ COPY uv.lock pyproject.toml /app/
# Sync the project # Optionall include lancedb with:
# docker build --build-arg INSTALL_LANCEDB=true .
ARG INSTALL_LANCEDB=false
RUN --mount=type=cache,target=/root/.cache/uv \ RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev if [ "$INSTALL_LANCEDB" = "true" ]; then \
uv sync --frozen --no-install-project --no-group dev --extra lancedb; \
elif [ "$INSTALL_LANCEDB" = "false" ]; then \
uv sync --frozen --no-install-project --no-group dev; \
else \
echo "INSTALL_LANCEDB must be 'true' or 'false'" >&2; \
exit 2; \
fi
FROM python:3.13-slim-bookworm AS runtime
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# Create the runtime user before copying dependencies with their final owner.
# A recursive chown in a later layer would copy the whole virtualenv and nearly
# double the image size.
RUN addgroup --system app \
&& adduser --system --group app \
&& chown app:app /app \
# Pre-create the LanceDB dir so a named volume mounted here inherits app
# ownership instead of defaulting to root.
&& mkdir /app/lancedb_data \
&& chown app:app /app/lancedb_data
COPY --from=builder --chown=app:app /app/.venv /app/.venv
# Place executables in the environment at the front of the path # Place executables in the environment at the front of the path
ENV PATH="/app/.venv/bin:$PATH" ENV PATH="/app/.venv/bin:$PATH"
ENV HOME=/app
COPY --chown=app:app src/ /app/src/ COPY --chown=app:app src/ /app/src/
COPY --chown=app:app migrations/ /app/migrations/ COPY --chown=app:app migrations/ /app/migrations/
COPY --chown=app:app scripts/ /app/scripts/ COPY --chown=app:app scripts/ /app/scripts/
COPY --chown=app:app docker/ /app/docker/
COPY --chown=app:app alembic.ini /app/alembic.ini COPY --chown=app:app alembic.ini /app/alembic.ini
# src/_version.py reads the service version from here at runtime, so this
# is a runtime input as well as a build input.
COPY --chown=app:app pyproject.toml /app/pyproject.toml
# Copy config files - this will copy config.toml if it exists, and config.toml.example # Copy config files - this will copy config.toml if it exists, and config.toml.example
COPY --chown=app:app config.toml* /app/ COPY --chown=app:app config.toml* /app/
# Switch to non-root user
USER app
EXPOSE 8000 EXPOSE 8000
# https://stackoverflow.com/questions/29663459/python-app-does-not-print-anything-when-running-detached-in-docker
CMD ["fastapi", "run", "--host", "0.0.0.0", "src/main.py"] CMD ["fastapi", "run", "--host", "0.0.0.0", "src/main.py"]

868
README.md

File diff suppressed because it is too large Load Diff

73
SECURITY.md Normal file
View File

@ -0,0 +1,73 @@
# Security Policy
## Supported Versions
The `main` branch of this repo maps to the latest canary version of Honcho. To see which versions are supported please refer to the git tags in the repo or the [compatibility guide](https://honcho.dev/docs/changelog/compatibility-guide).
## Reporting a Vulnerability
Do not open a public issue for a suspected vulnerability. Report it privately through one of:
1. **[GitHub Private Vulnerability Reporting](https://github.com/plastic-labs/honcho/security/advisories/new)** — preferred; it keeps the report, our replies, and any fix coordinated in one place.
2. Email [support@honcho.dev](mailto:support@honcho.dev) with `[SECURITY]` in the subject.
Include as much of the following as you have:
- **Version** — a git commit SHA, or the release tag you are running
- **Deployment** — self-hosted or the managed service at `api.honcho.dev`
- **Affected component** — API, deriver, dialectic, auth/JWT, an SDK, or the managed offering
- **Reproduction** — the exact steps, requests, or script that trigger it
- **Proof of concept** — the smallest thing that demonstrates the issue actually works
- **Impact** — what an attacker gains, and what they need to already have to get it
- **How you found it** — manual review, fuzzing, a scanner, or model-assisted analysis
Reports with a working proof of concept get looked at first. A report that only describes a
theoretical problem is much slower for us to act on, because we have to build the repro
ourselves before we can confirm anything.
Honcho stores conversational data and peer representations. **Do not attach production user
content, API keys, or JWTs** to a report — if we need a sample, we will ask for a redacted
one.
## Testing
Test against an instance you operate. Do not run security testing against `api.honcho.dev`
or against any Honcho deployment that is not yours — self-hosting is a first-class path and
takes a few minutes to set up — install the CLI (`uv tool install honcho-cli`) then run `honcho start --setup` (Docker + an LLM provider key), or see [Self-hosting](./README.md#self-hosting).
## What to Expect
We will acknowledge your report and tell you whether we consider it in scope. If it is, we
will let you know when a fix ships.
We do not commit to a response SLA, we do not coordinate CVE assignment on request, and we
do not operate a disclosure timeline you can hold us to. This is a small team.
## Out of Scope
The following are not treated as vulnerabilities. Reports consisting only of these will be
closed without a detailed response:
- Automated scanner output with no working proof of concept
- Model-generated findings that have not been verified by a human against a running instance
- Missing security headers or TLS configuration with no demonstrated exploit
- Rate limiting, or resource exhaustion with no demonstrated impact beyond your own instance
- Vulnerabilities in dependencies with no demonstrated exploit path through Honcho
- Configuration weaknesses that require an already-compromised host, or that come from
deliberately insecure settings (for example running with `AUTH_USE_AUTH=false`, which is
the documented local-development default and is not intended for a public deployment)
- Social engineering, phishing, and physical access
For ordinary bugs, memory or recall quality problems, and feature requests, use the
[issue templates](https://github.com/plastic-labs/honcho/issues/new/choose) instead.
## No Bug Bounty
The Honcho project does not offer any rewards for reported bugs or
vulnerabilities. We do not aid security researchers to get such rewards for
Honcho problems from other sources.
A bug bounty gives people too strong incentives to find and make up "problems"
in bad faith that cause overload and abuse.
We still appreciate and value valid vulnerability reports.

View File

@ -6,16 +6,18 @@
# Application-level settings # Application-level settings
[app] [app]
LOG_LEVEL = "INFO" LOG_LEVEL = "INFO"
PERFORMANCE_LOG_FORMAT = "compact" # "compact" for single-line logs, "rich" for local panels
SESSION_OBSERVERS_LIMIT = 10 SESSION_OBSERVERS_LIMIT = 10
GET_CONTEXT_MAX_TOKENS = 100000 GET_CONTEXT_MAX_TOKENS = 100000
MAX_FILE_SIZE = 5242880 # 5MB MAX_FILE_SIZE = 5242880 # 5MB
MAX_MESSAGE_SIZE = 25000 # Characters MAX_MESSAGE_SIZE = 25000 # Characters
EMBED_MESSAGES = true EMBED_MESSAGES = true
MAX_EMBEDDING_TOKENS = 8192
MAX_EMBEDDING_TOKENS_PER_REQUEST = 300000
# LANGFUSE_HOST = "https://api.langfuse.com" # LANGFUSE_HOST = "https://api.langfuse.com"
# LANGFUSE_PUBLIC_KEY = "your-public-key-here" # LANGFUSE_PUBLIC_KEY = "your-public-key-here"
# COLLECT_METRICS_LOCAL = false
# LOCAL_METRICS_FILE = "metrics.jsonl"
# REASONING_TRACES_FILE = "traces.jsonl" # Path to JSONL file for reasoning traces
NAMESPACE = "honcho"
# Database settings # Database settings
[db] [db]
@ -25,11 +27,14 @@ POOL_CLASS = "default"
POOL_PRE_PING = true POOL_PRE_PING = true
POOL_SIZE = 10 POOL_SIZE = 10
MAX_OVERFLOW = 20 MAX_OVERFLOW = 20
POOL_TIMEOUT = 30 # seconds POOL_TIMEOUT = 5 # seconds a pooled checkout waits for a free connection (QueuePool only)
POOL_RECYCLE = 300 # seconds POOL_RECYCLE = 300 # seconds
POOL_USE_LIFO = true POOL_USE_LIFO = true
SQL_DEBUG = false SQL_DEBUG = false
TRACING = false TRACING = false
# Per-connection establish timeout (seconds) so a single connection attempt
# fails fast instead of hanging when the server/pooler is unreachable.
CONNECT_TIMEOUT_SECONDS = 2
# Authentication settings # Authentication settings
[auth] [auth]
@ -48,66 +53,239 @@ PROFILES_SAMPLE_RATE = 0.1
# LLM settings # LLM settings
[llm] [llm]
DEFAULT_MAX_TOKENS = 2500 DEFAULT_MAX_TOKENS = 2500
MAX_TOOL_OUTPUT_CHARS = 10000 # Max chars for tool output (~2500 tokens)
MAX_MESSAGE_CONTENT_CHARS = 2000 # Max chars per message in tool results
# API Keys for LLM providers # API Keys for LLM providers (set the ones you need)
# Supported transports: openai, anthropic, gemini
# Base URLs are set per-module via model_config.overrides.base_url
# Built-in text-generation defaults use openai / gpt-5.4-mini.
# Embeddings default to openai / text-embedding-3-small.
OPENAI_API_KEY = "your-api-key-here"
# ANTHROPIC_API_KEY = "your-api-key" # ANTHROPIC_API_KEY = "your-api-key"
# OPENAI_API_KEY = "your-api-key"
# OPENAI_COMPATIBLE_API_KEY = "your-api-key"
# GEMINI_API_KEY = "your-api-key" # GEMINI_API_KEY = "your-api-key"
# GROQ_API_KEY = "your-api-key"
# OPENAI_COMPATIBLE_BASE_URL = "your-base-url" # Embedding settings
[embedding]
VECTOR_DIMENSIONS = 1536
MAX_INPUT_TOKENS = 8192
MAX_TOKENS_PER_REQUEST = 300000
[embedding.model_config]
transport = "openai"
model = "text-embedding-3-small"
# Optional provider request input cap. Useful for OpenAI-compatible embedding
# APIs with smaller limits, such as DashScope text-embedding-v4.
# max_batch_size = 10
# Optional client HTTP timeout in seconds (OpenAI + Gemini).
# timeout = 90.0
# Optional module-level endpoint overrides
# [embedding.model_config.overrides]
# base_url = "https://embedding-proxy.internal.example/v1"
# api_key_env = "EMBEDDING_CUSTOM_API_KEY"
# Deriver settings # Deriver settings
[deriver] [deriver]
ENABLED = true
WORKERS = 1 WORKERS = 1
POLLING_SLEEP_INTERVAL_SECONDS = 1.0 POLLING_SLEEP_INTERVAL_SECONDS = 1.0
# Adaptive polling: when idle/erroring, the sleep interval grows from
# POLLING_SLEEP_INTERVAL_SECONDS toward POLLING_SLEEP_MAX_INTERVAL_SECONDS by
# POLLING_BACKOFF_MULTIPLIER each cycle, then snaps back to base when work is
# found. Cuts steady-state query load against the shared DB/pooler.
POLLING_BACKOFF_ENABLED = true
POLLING_SLEEP_MAX_INTERVAL_SECONDS = 30.0
POLLING_BACKOFF_MULTIPLIER = 2.0
# Jitter so instances that start together don't poll in lockstep. Startup:
# sleep a random delay in [0, POLLING_STARTUP_JITTER_SECONDS] before the first
# poll (0.0 disables). Per-cycle: multiply every poll sleep by a random factor
# in [1 - ratio, 1 + ratio] (0.5 -> [0.5x, 1.5x]; 0.0 disables).
POLLING_STARTUP_JITTER_SECONDS = 30.0
POLLING_JITTER_RATIO = 0.5
STALE_SESSION_TIMEOUT_MINUTES = 5 STALE_SESSION_TIMEOUT_MINUTES = 5
PROVIDER = "google" # Minimum (jittered) spacing between stale-work-unit cleanup runs per instance.
MODEL = "gemini-2.0-flash-lite" # Staleness is a minutes-timescale condition, so cleanup doesn't need to run on
MAX_OUTPUT_TOKENS = 2500 # every seconds-scale poll (0.0 = run every poll, legacy behavior).
THINKING_BUDGET_TOKENS = 1024 # only applied when using Anthropic STALE_WORK_UNIT_CLEANUP_INTERVAL_SECONDS = 60.0
# QUEUE_ERROR_RETENTION_SECONDS = 2592000 # 30 days
DEDUPLICATE = true
LOG_OBSERVATIONS = false
MAX_INPUT_TOKENS = 25000
MAX_CUSTOM_INSTRUCTIONS_TOKENS = 2000
WORKING_REPRESENTATION_MAX_OBSERVATIONS = 100 WORKING_REPRESENTATION_MAX_OBSERVATIONS = 100
REPRESENTATION_BATCH_MAX_TOKENS = 4096 REPRESENTATION_BATCH_WORK_UNIT_TARGET_TOKENS = 512 # Min tokens a work unit accumulates before the deriver claims it; 0 disables the gate
MAX_INPUT_TOKENS = 23000 REPRESENTATION_BATCH_TARGET_INPUT_TOKENS = 1024 # Max context-window tokens per deriver LLM call
REPRESENTATION_BATCH_MAX_AGE_SECONDS = 1800
FLUSH_ENABLED = false # Bypass batch token threshold, process work immediately
[deriver.model_config]
transport = "openai"
model = "gpt-5.4-mini"
# temperature = 0.0
# thinking_effort = "minimal"
# thinking_budget_tokens = 1024
# max_output_tokens = 4096
# Optional module-level endpoint overrides
# transport = "openai"
# model = "my-local-model"
# [deriver.model_config.overrides]
# base_url = "https://llm.internal.example/v1"
# api_key_env = "DERIVER_CUSTOM_API_KEY"
# Optional fallback model
# [deriver.model_config.fallback]
# transport = "anthropic"
# model = "claude-haiku-4-5"
# [deriver.model_config.fallback.overrides]
# base_url = "https://llm-backup.internal.example/v1"
# api_key_env = "DERIVER_CUSTOM_BACKUP_API_KEY"
# [deriver.model_config.overrides.provider_params]
# verbosity = "low"
# timeout = 3600.0
# Peer card settings # Peer card settings
[peer_card] [peer_card]
ENABLED = true ENABLED = true
PROVIDER = "openai"
MODEL = "gpt-5-nano-2025-08-07"
MAX_OUTPUT_TOKENS = 4000
# Dialectic settings # Dialectic settings
[dialectic] [dialectic]
PROVIDER = "anthropic" MAX_OUTPUT_TOKENS = 8192
MODEL = "claude-sonnet-4-20250514" MAX_INPUT_TOKENS = 100000
PERFORM_QUERY_GENERATION = false HISTORY_TOKEN_LIMIT = 8192
QUERY_GENERATION_PROVIDER = "groq" SESSION_HISTORY_MAX_TOKENS = 4096
QUERY_GENERATION_MODEL = "llama-3.1-8b-instant"
MAX_OUTPUT_TOKENS = 2500 # Per-level settings for reasoning levels
SEMANTIC_SEARCH_TOP_K = 10 # MAX_OUTPUT_TOKENS is optional per level; if not set, uses global MAX_OUTPUT_TOKENS
SEMANTIC_SEARCH_MAX_DISTANCE = 0.85 [dialectic.levels.minimal]
THINKING_BUDGET_TOKENS = 1024 MAX_TOOL_ITERATIONS = 1
CONTEXT_WINDOW_SIZE = 100000 MAX_OUTPUT_TOKENS = 250
TOOL_CHOICE = "auto"
[dialectic.levels.minimal.model_config]
transport = "openai"
model = "gpt-5.4-mini"
[dialectic.levels.low]
MAX_TOOL_ITERATIONS = 5
TOOL_CHOICE = "auto"
[dialectic.levels.low.model_config]
transport = "openai"
model = "gpt-5.4-mini"
[dialectic.levels.medium]
MAX_TOOL_ITERATIONS = 2
[dialectic.levels.medium.model_config]
transport = "openai"
model = "gpt-5.4-mini"
[dialectic.levels.high]
MAX_TOOL_ITERATIONS = 4
[dialectic.levels.high.model_config]
transport = "openai"
model = "gpt-5.4-mini"
[dialectic.levels.max]
MAX_TOOL_ITERATIONS = 10
[dialectic.levels.max.model_config]
transport = "openai"
model = "gpt-5.4-mini"
# [dialectic.levels.max.model_config.fallback]
# transport = "gemini"
# model = "gemini-2.5-pro"
# Summary settings # Summary settings
[summary] [summary]
ENABLED = true ENABLED = true
MESSAGES_PER_SHORT_SUMMARY = 20 MESSAGES_PER_SHORT_SUMMARY = 20
MESSAGES_PER_LONG_SUMMARY = 60 MESSAGES_PER_LONG_SUMMARY = 60
PROVIDER = "google"
MODEL = "gemini-1.5-flash-latest"
MAX_TOKENS_SHORT = 1000 MAX_TOKENS_SHORT = 1000
MAX_TOKENS_LONG = 2000 MAX_TOKENS_LONG = 4000
THINKING_BUDGET_TOKENS = 512
[summary.model_config]
transport = "openai"
model = "gpt-5.4-mini"
# thinking_effort = "minimal"
# thinking_budget_tokens = 1024
# [summary.model_config.fallback]
# transport = "anthropic"
# model = "claude-haiku-4-5"
# Dream settings
[dream]
ENABLED = true
DOCUMENT_THRESHOLD = 50
IDLE_TIMEOUT_MINUTES = 60
MIN_HOURS_BETWEEN_DREAMS = 8
ENABLED_TYPES = ["omni"]
MAX_TOOL_ITERATIONS = 20
HISTORY_TOKEN_LIMIT = 16384
[dream.deduction_model_config]
transport = "openai"
model = "gpt-5.4-mini"
[dream.induction_model_config]
transport = "openai"
model = "gpt-5.4-mini"
# Surprisal-based sampling subsystem
[dream.surprisal]
ENABLED = false
TREE_TYPE = "kdtree" # Options: kdtree, balltree, rptree, covertree, lsh, graph, prototype
TREE_K = 5 # k for kNN-based trees
SAMPLING_STRATEGY = "recent" # Options: recent, random, all
SAMPLE_SIZE = 200
TOP_PERCENT_SURPRISAL = 0.10 # Top 10% of observations
MIN_HIGH_SURPRISAL_FOR_REPLACE = 10
INCLUDE_LEVELS = ["explicit", "deductive"]
# Webhook settings # Webhook settings
[webhook] [webhook]
SECRET = "" SECRET = ""
MAX_WORKSPACE_LIMIT = 10 MAX_WORKSPACE_LIMIT = 10
# Metrics settings # Prometheus metrics settings (pull-based metrics)
[metrics] [metrics]
ENABLED = false ENABLED = false
# NAMESPACE = "honcho" # Inherits from app.NAMESPACE if not set
# CloudEvents telemetry settings (analytics events)
[telemetry]
ENABLED = false
# ENDPOINT = "https://telemetry.honcho.dev/v1/events"
# HEADERS = '{"Authorization": "Bearer your-token"}' # JSON string for auth headers
BATCH_SIZE = 100
FLUSH_INTERVAL_SECONDS = 1.0
FLUSH_THRESHOLD = 50
MAX_RETRIES = 3
MAX_BUFFER_SIZE = 10000
# NAMESPACE = "honcho" # Inherits from app.NAMESPACE if not set
# Cache settings
[cache]
ENABLED = false
URL = "redis://localhost:6379/0?suppress=true"
# NAMESPACE = "honcho" # Inherits from app.NAMESPACE if not set
DEFAULT_TTL_SECONDS = 300
DEFAULT_LOCK_TTL_SECONDS = 5
# Vector store settings
[vector_store]
# Vector store type: "pgvector", "turbopuffer", or "lancedb"
TYPE = "pgvector"
# Migration flag: set to true when migration from pgvector is complete
MIGRATED = false
NAMESPACE = "honcho" NAMESPACE = "honcho"
# DIMENSIONS is deprecated; embedding.vector_dimensions is authoritative.
# TURBOPUFFER_API_KEY = "your-turbopuffer-api-key"
# TURBOPUFFER_REGION = "us-east-1"
LANCEDB_PATH = "./lancedb_data"
RECONCILIATION_INTERVAL_SECONDS = 300

View File

@ -1,47 +1,180 @@
# Honcho Docker Compose
#
# Usage:
# cp docker-compose.yml.example docker-compose.yml
# cp .env.template .env # edit with your provider config
# docker compose up -d --build
# INSTALL_LANCEDB=true docker compose up -d --build # optional local vector store
#
# By default, ports are bound to 127.0.0.1 (localhost only).
# For development, uncomment the source mounts and monitoring services below.
services: services:
api: api:
image: honcho:latest
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
args:
INSTALL_LANCEDB: ${INSTALL_LANCEDB:-false}
entrypoint: ["sh", "docker/entrypoint.sh"]
depends_on: depends_on:
database: database:
condition: service_healthy condition: service_healthy
redis:
condition: service_healthy
ports: ports:
- 8000:8000 - "127.0.0.1:8000:8000"
healthcheck:
test:
[
"CMD",
"/app/.venv/bin/python",
"-c",
"import urllib.request; urllib.request.urlopen('http://localhost:8000/health', timeout=2).read()",
]
interval: 5s
timeout: 5s
retries: 5
start_period: 10s
volumes: volumes:
- .:/app # Shared LanceDB data (used when VECTOR_STORE_TYPE=lancedb)
- lancedb-data:/app/lancedb_data
# -- Development: mount source for live reload --
# - .:/app
# - venv:/app/.venv
environment:
- DB_CONNECTION_URI=postgresql+psycopg://postgres:postgres@database:5432/postgres
- CACHE_URL=redis://redis:6379/0?suppress=true
- CACHE_ENABLED=true
env_file: env_file:
- .env - path: .env
required: false
restart: unless-stopped
deriver: deriver:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
entrypoint: ["uv", "run", "python", "-m", "src.deriver"] args:
INSTALL_LANCEDB: ${INSTALL_LANCEDB:-false}
entrypoint: ["/app/.venv/bin/python", "-m", "src.deriver"]
depends_on: depends_on:
api:
condition: service_healthy
database: database:
condition: service_healthy condition: service_healthy
redis:
condition: service_healthy
volumes: volumes:
- .:/app # Shared LanceDB data (used when VECTOR_STORE_TYPE=lancedb)
env_file: - lancedb-data:/app/lancedb_data
- .env # -- Development: mount source for live reload --
database: # - .:/app
image: pgvector/pgvector:pg15 # - venv:/app/.venv
restart: always
ports:
- 5432:5432
command: ["postgres", "-c", "max_connections=800"]
environment: environment:
- POSTGRES_DB=honcho - DB_CONNECTION_URI=postgresql+psycopg://postgres:postgres@database:5432/postgres
- POSTGRES_USER=testuser - CACHE_URL=redis://redis:6379/0?suppress=true
- POSTGRES_PASSWORD=testpwd - CACHE_ENABLED=true
- POSTGRES_HOST_AUTH_METHOD=trust env_file:
- PGDATA=/var/lib/postgresql/data/pgdata - path: .env
volumes: required: false
- ./init.sql:/docker-entrypoint-initdb.d/init.sql restart: unless-stopped
- ./data:/var/lib/postgresql/data/
mcp:
build:
context: ./mcp
dockerfile: Dockerfile
depends_on:
api:
condition: service_healthy
ports:
- "127.0.0.1:3000:3000"
environment:
- HONCHO_API_URL=http://api:8000
env_file:
- path: .env
required: false
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U testuser -d honcho"] test:
[
"CMD",
"bun",
"-e",
"fetch('http://127.0.0.1:3000/health').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))",
]
interval: 5s interval: 5s
timeout: 5s timeout: 5s
retries: 5 retries: 5
start_period: 10s
restart: unless-stopped
database:
image: pgvector/pgvector:pg15
restart: unless-stopped
ports:
- "127.0.0.1:5432:5432"
command: ["postgres", "-c", "max_connections=200"]
environment:
- POSTGRES_DB=postgres
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
# Allow passwordless connections from the host (port is bound to 127.0.0.1).
# Lets the local test suite and ad-hoc tools connect without supplying a
# password. Do NOT use this in production.
- POSTGRES_HOST_AUTH_METHOD=trust
- PGDATA=/var/lib/postgresql/data/pgdata
volumes:
- ./database/init.sql:/docker-entrypoint-initdb.d/init.sql
- pgdata:/var/lib/postgresql/data/
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:8.2
restart: unless-stopped
ports:
- "127.0.0.1:6379:6379"
volumes:
- redis-data:/data
healthcheck:
test: ["CMD-SHELL", "redis-cli ping"]
interval: 5s
timeout: 5s
retries: 5
# -- Development: monitoring stack (uncomment to enable) --
# prometheus:
# image: prom/prometheus:v3.2.1
# ports:
# - "127.0.0.1:9090:9090"
# volumes:
# - ./docker/prometheus.yml:/etc/prometheus/prometheus.yml:ro
# - prometheus-data:/prometheus
# depends_on:
# api:
# condition: service_started
# grafana:
# image: grafana/grafana:11.4.0
# ports:
# - "127.0.0.1:3000:3000"
# environment:
# - GF_SECURITY_ADMIN_USER=admin
# - GF_SECURITY_ADMIN_PASSWORD=admin
# - GF_AUTH_ANONYMOUS_ENABLED=true
# - GF_AUTH_ANONYMOUS_ORG_ROLE=Viewer
# volumes:
# - ./docker/grafana-datasource.yml:/etc/grafana/provisioning/datasources/datasource.yml:ro
# depends_on:
# prometheus:
# condition: service_started
volumes:
pgdata:
redis-data:
lancedb-data:
# -- Development: uncomment if using source mounts --
# venv:
# prometheus-data:

8
docker/entrypoint.sh Executable file
View File

@ -0,0 +1,8 @@
#!/bin/sh
set -e
echo "Running database migrations..."
/app/.venv/bin/python scripts/provision_db.py
echo "Starting API server..."
exec /app/.venv/bin/fastapi run --host 0.0.0.0 --workers "${API_WORKERS:-1}" src/main.py

View File

@ -0,0 +1,9 @@
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: false

10
docker/prometheus.yml Normal file
View File

@ -0,0 +1,10 @@
global:
scrape_interval: 15s
scrape_configs:
- job_name: honcho-api
static_configs:
- targets: ["api:8000"]
- job_name: honcho-deriver
static_configs:
- targets: ["deriver:9090"]

View File

@ -1,14 +1,14 @@
{ {
"lockfileVersion": 1, "lockfileVersion": 1,
"configVersion": 0,
"workspaces": { "workspaces": {
"": { "": {
"name": "honcho-docs", "name": "honcho-docs",
"dependencies": { "dependencies": {
"@mintlify/scraping": "^4.0.284", "@mintlify/scraping": "^4.0.467",
"honcho-ai": "^0.0.11",
}, },
"devDependencies": { "devDependencies": {
"mint": "^4.2.123", "mint": "^4.2.204",
}, },
}, },
}, },
@ -17,9 +17,9 @@
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
"@ark/schema": ["@ark/schema@0.49.0", "", { "dependencies": { "@ark/util": "0.49.0" } }, "sha512-GphZBLpW72iS0v4YkeUtV3YIno35Gimd7+ezbPO9GwEi9kzdUrPVjvf6aXSBAfHikaFc/9pqZOpv3pOXnC71tw=="], "@ark/schema": ["@ark/schema@0.55.0", "", { "dependencies": { "@ark/util": "0.55.0" } }, "sha512-IlSIc0FmLKTDGr4I/FzNHauMn0MADA6bCjT1wauu4k6MyxhC1R9gz0olNpIRvK7lGGDwtc/VO0RUDNvVQW5WFg=="],
"@ark/util": ["@ark/util@0.49.0", "", {}, "sha512-/BtnX7oCjNkxi2vi6y1399b+9xd1jnCrDYhZ61f0a+3X8x8DxlK52VgEEzyuC2UQMPACIfYrmHkhD3lGt2GaMA=="], "@ark/util": ["@ark/util@0.55.0", "", {}, "sha512-aWFNK7aqSvqFtVsl1xmbTjGbg91uqtJV7Za76YGNEwIO4qLjMfyY8flmmbhooYMuqPCO2jyxu8hve943D+w3bA=="],
"@asyncapi/parser": ["@asyncapi/parser@3.4.0", "", { "dependencies": { "@asyncapi/specs": "^6.8.0", "@openapi-contrib/openapi-schema-to-json-schema": "~3.2.0", "@stoplight/json": "3.21.0", "@stoplight/json-ref-readers": "^1.2.2", "@stoplight/json-ref-resolver": "^3.1.5", "@stoplight/spectral-core": "^1.18.3", "@stoplight/spectral-functions": "^1.7.2", "@stoplight/spectral-parsers": "^1.0.2", "@stoplight/spectral-ref-resolver": "^1.0.3", "@stoplight/types": "^13.12.0", "@types/json-schema": "^7.0.11", "@types/urijs": "^1.19.19", "ajv": "^8.17.1", "ajv-errors": "^3.0.0", "ajv-formats": "^2.1.1", "avsc": "^5.7.5", "js-yaml": "^4.1.0", "jsonpath-plus": "^10.0.0", "node-fetch": "2.6.7" } }, "sha512-Sxn74oHiZSU6+cVeZy62iPZMFMvKp4jupMFHelSICCMw1qELmUHPvuZSr+ZHDmNGgHcEpzJM5HN02kR7T4g+PQ=="], "@asyncapi/parser": ["@asyncapi/parser@3.4.0", "", { "dependencies": { "@asyncapi/specs": "^6.8.0", "@openapi-contrib/openapi-schema-to-json-schema": "~3.2.0", "@stoplight/json": "3.21.0", "@stoplight/json-ref-readers": "^1.2.2", "@stoplight/json-ref-resolver": "^3.1.5", "@stoplight/spectral-core": "^1.18.3", "@stoplight/spectral-functions": "^1.7.2", "@stoplight/spectral-parsers": "^1.0.2", "@stoplight/spectral-ref-resolver": "^1.0.3", "@stoplight/types": "^13.12.0", "@types/json-schema": "^7.0.11", "@types/urijs": "^1.19.19", "ajv": "^8.17.1", "ajv-errors": "^3.0.0", "ajv-formats": "^2.1.1", "avsc": "^5.7.5", "js-yaml": "^4.1.0", "jsonpath-plus": "^10.0.0", "node-fetch": "2.6.7" } }, "sha512-Sxn74oHiZSU6+cVeZy62iPZMFMvKp4jupMFHelSICCMw1qELmUHPvuZSr+ZHDmNGgHcEpzJM5HN02kR7T4g+PQ=="],
@ -29,6 +29,8 @@
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
"@canvas/image-data": ["@canvas/image-data@1.1.0", "", {}, "sha512-QdObRRjRbcXGmM1tmJ+MrHcaz1MftF2+W7YI+MsphnsCrmtyfS0d5qJbk0MeSbUeyM/jCb0hmnkXPsy026L7dA=="],
"@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="], "@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="],
"@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="], "@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="],
@ -77,31 +79,35 @@
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="], "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="],
"@inquirer/checkbox": ["@inquirer/checkbox@4.2.0", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/figures": "^1.0.13", "@inquirer/type": "^3.0.8", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-fdSw07FLJEU5vbpOPzXo5c6xmMGDzbZE2+niuDHX5N6mc6V0Ebso/q3xiHra4D73+PMsC8MJmcaZKuAAoaQsSA=="], "@inquirer/ansi": ["@inquirer/ansi@1.0.2", "", {}, "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ=="],
"@inquirer/confirm": ["@inquirer/confirm@5.1.14", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/type": "^3.0.8" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-5yR4IBfe0kXe59r1YCTG8WXkUbl7Z35HK87Sw+WUyGD8wNUx7JvY7laahzeytyE1oLn74bQnL7hstctQxisQ8Q=="], "@inquirer/checkbox": ["@inquirer/checkbox@4.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/core": "^10.3.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA=="],
"@inquirer/confirm": ["@inquirer/confirm@5.1.21", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ=="],
"@inquirer/core": ["@inquirer/core@10.1.15", "", { "dependencies": { "@inquirer/figures": "^1.0.13", "@inquirer/type": "^3.0.8", "ansi-escapes": "^4.3.2", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-8xrp836RZvKkpNbVvgWUlxjT4CraKk2q+I3Ksy+seI2zkcE+y6wNs1BVhgcv8VyImFecUhdQrYLdW32pAjwBdA=="], "@inquirer/core": ["@inquirer/core@10.1.15", "", { "dependencies": { "@inquirer/figures": "^1.0.13", "@inquirer/type": "^3.0.8", "ansi-escapes": "^4.3.2", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-8xrp836RZvKkpNbVvgWUlxjT4CraKk2q+I3Ksy+seI2zkcE+y6wNs1BVhgcv8VyImFecUhdQrYLdW32pAjwBdA=="],
"@inquirer/editor": ["@inquirer/editor@4.2.15", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/type": "^3.0.8", "external-editor": "^3.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-wst31XT8DnGOSS4nNJDIklGKnf+8shuauVrWzgKegWUe28zfCftcWZ2vktGdzJgcylWSS2SrDnYUb6alZcwnCQ=="], "@inquirer/editor": ["@inquirer/editor@4.2.23", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/external-editor": "^1.0.3", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ=="],
"@inquirer/expand": ["@inquirer/expand@4.0.17", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/type": "^3.0.8", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PSqy9VmJx/VbE3CT453yOfNa+PykpKg/0SYP7odez1/NWBGuDXgPhp4AeGYYKjhLn5lUUavVS/JbeYMPdH50Mw=="], "@inquirer/expand": ["@inquirer/expand@4.0.23", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew=="],
"@inquirer/figures": ["@inquirer/figures@1.0.13", "", {}, "sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw=="], "@inquirer/external-editor": ["@inquirer/external-editor@1.0.3", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA=="],
"@inquirer/input": ["@inquirer/input@4.2.1", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/type": "^3.0.8" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-tVC+O1rBl0lJpoUZv4xY+WGWY8V5b0zxU1XDsMsIHYregdh7bN5X5QnIONNBAl0K765FYlAfNHS2Bhn7SSOVow=="], "@inquirer/figures": ["@inquirer/figures@1.0.15", "", {}, "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g=="],
"@inquirer/number": ["@inquirer/number@3.0.17", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/type": "^3.0.8" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-GcvGHkyIgfZgVnnimURdOueMk0CztycfC8NZTiIY9arIAkeOgt6zG57G+7vC59Jns3UX27LMkPKnKWAOF5xEYg=="], "@inquirer/input": ["@inquirer/input@4.3.1", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g=="],
"@inquirer/password": ["@inquirer/password@4.0.17", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/type": "^3.0.8", "ansi-escapes": "^4.3.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-DJolTnNeZ00E1+1TW+8614F7rOJJCM4y4BAGQ3Gq6kQIG+OJ4zr3GLjIjVVJCbKsk2jmkmv6v2kQuN/vriHdZA=="], "@inquirer/number": ["@inquirer/number@3.0.23", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg=="],
"@inquirer/prompts": ["@inquirer/prompts@7.7.1", "", { "dependencies": { "@inquirer/checkbox": "^4.2.0", "@inquirer/confirm": "^5.1.14", "@inquirer/editor": "^4.2.15", "@inquirer/expand": "^4.0.17", "@inquirer/input": "^4.2.1", "@inquirer/number": "^3.0.17", "@inquirer/password": "^4.0.17", "@inquirer/rawlist": "^4.1.5", "@inquirer/search": "^3.0.17", "@inquirer/select": "^4.3.1" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-XDxPrEWeWUBy8scAXzXuFY45r/q49R0g72bUzgQXZ1DY/xEFX+ESDMkTQolcb5jRBzaNJX2W8XQl6krMNDTjaA=="], "@inquirer/password": ["@inquirer/password@4.0.23", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA=="],
"@inquirer/rawlist": ["@inquirer/rawlist@4.1.5", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/type": "^3.0.8", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-R5qMyGJqtDdi4Ht521iAkNqyB6p2UPuZUbMifakg1sWtu24gc2Z8CJuw8rP081OckNDMgtDCuLe42Q2Kr3BolA=="], "@inquirer/prompts": ["@inquirer/prompts@7.10.1", "", { "dependencies": { "@inquirer/checkbox": "^4.3.2", "@inquirer/confirm": "^5.1.21", "@inquirer/editor": "^4.2.23", "@inquirer/expand": "^4.0.23", "@inquirer/input": "^4.3.1", "@inquirer/number": "^3.0.23", "@inquirer/password": "^4.0.23", "@inquirer/rawlist": "^4.1.11", "@inquirer/search": "^3.2.2", "@inquirer/select": "^4.4.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg=="],
"@inquirer/search": ["@inquirer/search@3.0.17", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/figures": "^1.0.13", "@inquirer/type": "^3.0.8", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-CuBU4BAGFqRYors4TNCYzy9X3DpKtgIW4Boi0WNkm4Ei1hvY9acxKdBdyqzqBCEe4YxSdaQQsasJlFlUJNgojw=="], "@inquirer/rawlist": ["@inquirer/rawlist@4.1.11", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw=="],
"@inquirer/select": ["@inquirer/select@4.3.1", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/figures": "^1.0.13", "@inquirer/type": "^3.0.8", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Gfl/5sqOF5vS/LIrSndFgOh7jgoe0UXEizDqahFRkq5aJBLegZ6WjuMh/hVEJwlFQjyLq1z9fRtvUMkb7jM1LA=="], "@inquirer/search": ["@inquirer/search@3.2.2", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA=="],
"@inquirer/select": ["@inquirer/select@4.4.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/core": "^10.3.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w=="],
"@inquirer/type": ["@inquirer/type@3.0.8", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw=="], "@inquirer/type": ["@inquirer/type@3.0.8", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw=="],
@ -127,25 +133,25 @@
"@mdx-js/react": ["@mdx-js/react@3.1.0", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ=="], "@mdx-js/react": ["@mdx-js/react@3.1.0", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ=="],
"@mintlify/cli": ["@mintlify/cli@4.0.727", "", { "dependencies": { "@mintlify/common": "1.0.537", "@mintlify/link-rot": "3.0.674", "@mintlify/models": "0.0.229", "@mintlify/prebuild": "1.0.661", "@mintlify/previewing": "4.0.710", "@mintlify/validation": "0.1.471", "chalk": "^5.2.0", "detect-port": "^1.5.1", "fs-extra": "^11.2.0", "gray-matter": "^4.0.3", "ink": "^6.0.1", "inquirer": "^12.3.0", "js-yaml": "^4.1.0", "react": "^19.1.0", "semver": "^7.7.2", "yargs": "^17.6.0" }, "bin": { "mint": "bin/index.js", "mintlify": "bin/index.js" } }, "sha512-6iplgwOC9wK1FFdSFE9NX92qhxF0TkuZADf2rVkSR/A3MQ9LzTh9iKttRWD0q66pQ8l2kwOhzwqt/tW22ZLmcA=="], "@mintlify/cli": ["@mintlify/cli@4.0.808", "", { "dependencies": { "@inquirer/prompts": "^7.9.0", "@mintlify/common": "1.0.607", "@mintlify/link-rot": "3.0.750", "@mintlify/models": "0.0.240", "@mintlify/prebuild": "1.0.736", "@mintlify/previewing": "4.0.786", "@mintlify/validation": "0.1.521", "adm-zip": "^0.5.10", "chalk": "^5.2.0", "color": "^4.2.3", "detect-port": "^1.5.1", "fs-extra": "^11.2.0", "gray-matter": "^4.0.3", "ink": "^6.0.1", "inquirer": "^12.3.0", "js-yaml": "^4.1.0", "mdast-util-mdx-jsx": "^3.2.0", "react": "^19.1.0", "semver": "^7.7.2", "unist-util-visit": "^5.0.0", "yargs": "^17.6.0" }, "bin": { "mint": "bin/index.js", "mintlify": "bin/index.js" } }, "sha512-oVd+33DuORSXQPyVhX9VamME+qZkbGwSGAaas3LaNoabGxw9O9Nb34KYDvABvVOf9LqHCy/O+FhmegW+zr3upQ=="],
"@mintlify/common": ["@mintlify/common@1.0.461", "", { "dependencies": { "@asyncapi/parser": "^3.4.0", "@mintlify/mdx": "^2.0.3", "@mintlify/models": "0.0.213", "@mintlify/openapi-parser": "^0.0.7", "@mintlify/validation": "0.1.424", "@sindresorhus/slugify": "^2.1.1", "acorn": "^8.11.2", "acorn-jsx": "^5.3.2", "estree-util-to-js": "^2.0.0", "estree-walker": "^3.0.3", "gray-matter": "^4.0.3", "hast-util-from-html": "^2.0.3", "hast-util-to-html": "^9.0.4", "hast-util-to-text": "^4.0.2", "js-yaml": "^4.1.0", "lodash": "^4.17.21", "mdast": "^3.0.0", "mdast-util-from-markdown": "^2.0.2", "mdast-util-mdx": "^3.0.0", "mdast-util-mdx-jsx": "^3.1.3", "micromark-extension-mdx-jsx": "^3.0.1", "openapi-types": "^12.0.0", "remark": "^15.0.1", "remark-frontmatter": "^5.0.0", "remark-gfm": "^4.0.0", "remark-math": "^6.0.0", "remark-mdx": "^3.1.0", "remark-stringify": "^11.0.0", "unified": "^11.0.5", "unist-builder": "^4.0.0", "unist-util-map": "^4.0.0", "unist-util-remove": "^4.0.0", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-tDKqkB5RolG0INcUwpLjVb8BuhUt+KyJ8FH+W0UUw267olEaKnU8u8TYVVclHHvRd6jvYzaXjGdGARx2CN3pgQ=="], "@mintlify/common": ["@mintlify/common@1.0.607", "", { "dependencies": { "@asyncapi/parser": "^3.4.0", "@mintlify/mdx": "^3.0.1", "@mintlify/models": "0.0.240", "@mintlify/openapi-parser": "^0.0.8", "@mintlify/validation": "0.1.521", "@sindresorhus/slugify": "^2.1.1", "acorn": "^8.11.2", "acorn-jsx": "^5.3.2", "color-blend": "^4.0.0", "estree-util-to-js": "^2.0.0", "estree-walker": "^3.0.3", "gray-matter": "^4.0.3", "hast-util-from-html": "^2.0.3", "hast-util-to-html": "^9.0.4", "hast-util-to-text": "^4.0.2", "hex-rgb": "^5.0.0", "js-yaml": "^4.1.0", "lodash": "^4.17.21", "mdast-util-from-markdown": "^2.0.2", "mdast-util-gfm": "^3.0.0", "mdast-util-mdx": "^3.0.0", "mdast-util-mdx-jsx": "^3.1.3", "micromark-extension-gfm": "^3.0.0", "micromark-extension-mdx-jsx": "^3.0.1", "micromark-extension-mdxjs": "^3.0.0", "openapi-types": "^12.0.0", "postcss": "^8.5.6", "remark": "^15.0.1", "remark-frontmatter": "^5.0.0", "remark-gfm": "^4.0.0", "remark-math": "^6.0.0", "remark-mdx": "^3.1.0", "remark-stringify": "^11.0.0", "tailwindcss": "^3.4.4", "unified": "^11.0.5", "unist-builder": "^4.0.0", "unist-util-map": "^4.0.0", "unist-util-remove": "^4.0.0", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-9Yc8piWlOTSyapcV1MsjnsDZmaHlgP1cJ2IqtNd5+dvuy6pFz2TG8ELJxg9k9rf25KleOvI+QN3pJ8pHbnJWtg=="],
"@mintlify/link-rot": ["@mintlify/link-rot@3.0.674", "", { "dependencies": { "@mintlify/common": "1.0.537", "@mintlify/prebuild": "1.0.661", "@mintlify/previewing": "4.0.710", "@mintlify/validation": "0.1.471", "fs-extra": "^11.1.0", "unist-util-visit": "^4.1.1" } }, "sha512-QzbMAva0GdbBBG6R+pWmHrVzNdR08ug6e6a5Tnxk1NaNr8+/YR7cehu5xdAX31aO6n+CIv8Ot5HxzsqsY2vwtA=="], "@mintlify/link-rot": ["@mintlify/link-rot@3.0.750", "", { "dependencies": { "@mintlify/common": "1.0.607", "@mintlify/prebuild": "1.0.736", "@mintlify/previewing": "4.0.786", "@mintlify/validation": "0.1.521", "fs-extra": "^11.1.0", "unist-util-visit": "^4.1.1" } }, "sha512-IkrpTs29C+ouUgyp9p2zGE80AfRlgOgkffzKcDi+UBkSrSSVA82ub3CaA+Lk5Zyrhr5t5aI1wx2Z9FnLmvuNwg=="],
"@mintlify/mdx": ["@mintlify/mdx@2.0.3", "", { "dependencies": { "@shikijs/transformers": "^3.6.0", "hast-util-to-string": "^3.0.1", "mdast-util-mdx-jsx": "^3.2.0", "next-mdx-remote-client": "^1.0.3", "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.0", "remark-math": "^6.0.0", "remark-smartypants": "^3.0.2", "shiki": "^3.6.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "react": "^18.3.1", "react-dom": "^18.3.1" } }, "sha512-UGlwavma8QooWAlhtXpTAG5MAUZTTUKI8Qu25Wqfp1HMOPrYGvo5YQPmlqqogbMsqDMcFPLP/ZYnaZsGUYBspQ=="], "@mintlify/mdx": ["@mintlify/mdx@3.0.3", "", { "dependencies": { "@shikijs/transformers": "^3.11.0", "@shikijs/twoslash": "^3.12.2", "arktype": "^2.1.26", "hast-util-to-string": "^3.0.1", "mdast-util-from-markdown": "^2.0.2", "mdast-util-gfm": "^3.1.0", "mdast-util-mdx-jsx": "^3.2.0", "mdast-util-to-hast": "^13.2.0", "next-mdx-remote-client": "^1.0.3", "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.0", "remark-math": "^6.0.0", "remark-smartypants": "^3.0.2", "shiki": "^3.11.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "@radix-ui/react-popover": "^1.1.15", "react": "^18.3.1", "react-dom": "^18.3.1" } }, "sha512-YwvZZ/2CJG+MT2sWyKOXAEk/nS5lzq3ACUerqD8xtPtnMMCgqoSQ/Y8pA32OfTAHFMsiIwqI3NNWYFLEftyrWg=="],
"@mintlify/models": ["@mintlify/models@0.0.229", "", { "dependencies": { "axios": "^1.8.3", "openapi-types": "^12.0.0" } }, "sha512-1P3R6dQFNzjTbmVDCQf/vAGFGOEUdUv6sCaJAmZCNWY2mhwgvDU/Oa2YLiNmVrAqnWDH1Pkz5nq+i7gClrdXgA=="], "@mintlify/models": ["@mintlify/models@0.0.240", "", { "dependencies": { "axios": "^1.8.3", "openapi-types": "^12.0.0" } }, "sha512-9j8UfcYw+pD5D5qhB/iPywSpnB/sgwft+mUc08mWS+Tol19smROa901Myy0yLT0NZPfoZVfSSLp8J5LTloljpA=="],
"@mintlify/openapi-parser": ["@mintlify/openapi-parser@0.0.7", "", { "dependencies": { "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "jsonpointer": "^5.0.1", "leven": "^4.0.0", "yaml": "^2.4.5" } }, "sha512-3ecbkzPbsnkKVZJypVL0H5pCTR7a4iLv4cP7zbffzAwy+vpH70JmPxNVpPPP62yLrdZlfNcMxu5xKeT7fllgMg=="], "@mintlify/openapi-parser": ["@mintlify/openapi-parser@0.0.8", "", { "dependencies": { "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "jsonpointer": "^5.0.1", "leven": "^4.0.0", "yaml": "^2.4.5" } }, "sha512-9MBRq9lS4l4HITYCrqCL7T61MOb20q9IdU7HWhqYMNMM1jGO1nHjXasFy61yZ8V6gMZyyKQARGVoZ0ZrYN48Og=="],
"@mintlify/prebuild": ["@mintlify/prebuild@1.0.661", "", { "dependencies": { "@mintlify/common": "1.0.537", "@mintlify/openapi-parser": "^0.0.7", "@mintlify/scraping": "4.0.396", "@mintlify/validation": "0.1.471", "chalk": "^5.3.0", "favicons": "^7.2.0", "fs-extra": "^11.1.0", "gray-matter": "^4.0.3", "js-yaml": "^4.1.0", "mdast": "^3.0.0", "openapi-types": "^12.0.0", "unist-util-visit": "^4.1.1" } }, "sha512-hcYLxhf53RV6hecJLIEdG7ajQuTtDj3vuoueeLmhcVXEDYkvKKa36EOk9/olLUDvjqsENnK601NmdmHPd80pJA=="], "@mintlify/prebuild": ["@mintlify/prebuild@1.0.736", "", { "dependencies": { "@mintlify/common": "1.0.607", "@mintlify/openapi-parser": "^0.0.8", "@mintlify/scraping": "4.0.467", "@mintlify/validation": "0.1.521", "chalk": "^5.3.0", "favicons": "^7.2.0", "fs-extra": "^11.1.0", "gray-matter": "^4.0.3", "js-yaml": "^4.1.0", "openapi-types": "^12.0.0", "sharp": "^0.33.1", "sharp-ico": "^0.1.5", "unist-util-visit": "^4.1.1", "uuid": "^11.1.0" } }, "sha512-ih38FjriVUpujDqrc6v9Yt4H1eC4ByGL5MVDSWy++RIkjRZLyPULnEOh1IcGqJ9zSYJOPtPgoIsTIta71/yZsw=="],
"@mintlify/previewing": ["@mintlify/previewing@4.0.710", "", { "dependencies": { "@mintlify/common": "1.0.537", "@mintlify/prebuild": "1.0.661", "@mintlify/validation": "0.1.471", "better-opn": "^3.0.2", "chalk": "^5.1.0", "chokidar": "^3.5.3", "express": "^4.18.2", "fs-extra": "^11.1.0", "got": "^13.0.0", "gray-matter": "^4.0.3", "ink": "^6.0.1", "ink-spinner": "^5.0.0", "is-online": "^10.0.0", "js-yaml": "^4.1.0", "mdast": "^3.0.0", "openapi-types": "^12.0.0", "react": "^19.1.0", "socket.io": "^4.7.2", "tar": "^6.1.15", "unist-util-visit": "^4.1.1", "yargs": "^17.6.0" } }, "sha512-3SyO58i7kmR4W+UCcP9gq/wTKsJ0Vs+pudFnfJt5Qk1QIMTXbZGjC/ojaYDbGqSt+VOmIPwbGchQzZrAp1aXbA=="], "@mintlify/previewing": ["@mintlify/previewing@4.0.786", "", { "dependencies": { "@mintlify/common": "1.0.607", "@mintlify/prebuild": "1.0.736", "@mintlify/validation": "0.1.521", "better-opn": "^3.0.2", "chalk": "^5.1.0", "chokidar": "^3.5.3", "express": "^4.18.2", "fs-extra": "^11.1.0", "got": "^13.0.0", "gray-matter": "^4.0.3", "ink": "^6.0.1", "ink-spinner": "^5.0.0", "is-online": "^10.0.0", "js-yaml": "^4.1.0", "openapi-types": "^12.0.0", "react": "^19.1.0", "socket.io": "^4.7.2", "tar": "^6.1.15", "unist-util-visit": "^4.1.1", "yargs": "^17.6.0" } }, "sha512-OPVm66QdNdNjDcyv2iWA3/fJUfyBGvm6XKiORDhlW+dqiFX5aTDLEithsclmKz5r2JEpIqIJNSeiudpY8uGHAg=="],
"@mintlify/scraping": ["@mintlify/scraping@4.0.317", "", { "dependencies": { "@mintlify/common": "1.0.461", "@mintlify/openapi-parser": "^0.0.7", "fs-extra": "^11.1.1", "hast-util-to-mdast": "^10.1.0", "js-yaml": "^4.1.0", "mdast-util-mdx-jsx": "^3.1.3", "neotraverse": "^0.6.18", "puppeteer": "^22.14.0", "rehype-parse": "^9.0.0", "remark-gfm": "^4.0.0", "remark-mdx": "^3.0.1", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", "yargs": "^17.6.0", "zod": "^3.20.6" }, "bin": { "mintlify-scrape": "bin/cli.js" } }, "sha512-WVgReuvckQMgWkbR8JrGQp5l1cs4WB+V8+ZWjQHhCmyjp+Kv+2zVKs9dw2/yiWjZYiYytu6if7Dise9sbLuMbQ=="], "@mintlify/scraping": ["@mintlify/scraping@4.0.467", "", { "dependencies": { "@mintlify/common": "1.0.607", "@mintlify/openapi-parser": "^0.0.8", "fs-extra": "^11.1.1", "hast-util-to-mdast": "^10.1.0", "js-yaml": "^4.1.0", "mdast-util-mdx-jsx": "^3.1.3", "neotraverse": "^0.6.18", "puppeteer": "^22.14.0", "rehype-parse": "^9.0.0", "remark-gfm": "^4.0.0", "remark-mdx": "^3.0.1", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", "yargs": "^17.6.0", "zod": "^3.20.6" }, "bin": { "mintlify-scrape": "bin/cli.js" } }, "sha512-UnanSRzG5gDef9NFlSO6F00JznCAEEb1onbuidfzQ9sBPzG/a+g+0K4Z6VdbeNCFKmirx2t1qm1gujkh8XIfog=="],
"@mintlify/validation": ["@mintlify/validation@0.1.471", "", { "dependencies": { "@mintlify/models": "0.0.229", "arktype": "^2.1.20", "lcm": "^0.0.3", "lodash": "^4.17.21", "openapi-types": "^12.0.0", "zod": "^3.20.6", "zod-to-json-schema": "^3.20.3" } }, "sha512-lf4zp9sJspXmDA9HH9VaJfK4ll+BaaH9XxuU2SVNuploKjRKmpHYFfN9YI42pA2bda/X32rkqDZSRI+JHdQcNg=="], "@mintlify/validation": ["@mintlify/validation@0.1.521", "", { "dependencies": { "@mintlify/mdx": "^3.0.1", "@mintlify/models": "0.0.240", "arktype": "^2.1.20", "js-yaml": "^4.1.0", "lcm": "^0.0.3", "lodash": "^4.17.21", "object-hash": "^3.0.0", "openapi-types": "^12.0.0", "uuid": "^11.1.0", "zod": "^3.20.6", "zod-to-json-schema": "^3.20.3" } }, "sha512-8icZULy+5CXGucFNo7mTWEgzif/73Lvbb8pNuo7MBL81kbvixPWKX3j5TiSLkVCUjwUGIglr9IKc9+RXxwN09A=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
@ -203,21 +209,21 @@
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
"@shikijs/core": ["@shikijs/core@3.8.1", "", { "dependencies": { "@shikijs/types": "3.8.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-uTSXzUBQ/IgFcUa6gmGShCHr4tMdR3pxUiiWKDm8pd42UKJdYhkAYsAmHX5mTwybQ5VyGDgTjW4qKSsRvGSang=="], "@shikijs/core": ["@shikijs/core@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-3P8rGsg2Eh2qIHekwuQjzWhKI4jV97PhvYjYUzGqjvJfqdQPz+nMlfWahU24GZAyW1FxFI1sYjyhfh5CoLmIUA=="],
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.8.1", "", { "dependencies": { "@shikijs/types": "3.8.1", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-rZRp3BM1llrHkuBPAdYAzjlF7OqlM0rm/7EWASeCcY7cRYZIrOnGIHE9qsLz5TCjGefxBFnwgIECzBs2vmOyKA=="], "@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-Ty7xv32XCp8u0eQt8rItpMs6rU9Ki6LJ1dQOW3V/56PKDcpvfHPnYFbsx5FFUP2Yim34m/UkazidamMNVR4vKg=="],
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.8.1", "", { "dependencies": { "@shikijs/types": "3.8.1", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-KGQJZHlNY7c656qPFEQpIoqOuC4LrxjyNndRdzk5WKB/Ie87+NJCF1xo9KkOUxwxylk7rT6nhlZyTGTC4fCe1g=="], "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-O42rBGr4UDSlhT2ZFMxqM7QzIU+IcpoTMzb3W7AlziI1ZF7R8eS2M0yt5Ry35nnnTX/LTLXFPUjRFCIW+Operg=="],
"@shikijs/langs": ["@shikijs/langs@3.8.1", "", { "dependencies": { "@shikijs/types": "3.8.1" } }, "sha512-TjOFg2Wp1w07oKnXjs0AUMb4kJvujML+fJ1C5cmEj45lhjbUXtziT1x2bPQb9Db6kmPhkG5NI2tgYW1/DzhUuQ=="], "@shikijs/langs": ["@shikijs/langs@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0" } }, "sha512-672c3WAETDYHwrRP0yLy3W1QYB89Hbpj+pO4KhxK6FzIrDI2FoEXNiNCut6BQmEApYLfuYfpgOZaqbY+E9b8wQ=="],
"@shikijs/themes": ["@shikijs/themes@3.8.1", "", { "dependencies": { "@shikijs/types": "3.8.1" } }, "sha512-Vu3t3BBLifc0GB0UPg2Pox1naTemrrvyZv2lkiSw3QayVV60me1ujFQwPZGgUTmwXl1yhCPW8Lieesm0CYruLQ=="], "@shikijs/themes": ["@shikijs/themes@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0" } }, "sha512-Vxw1Nm1/Od8jyA7QuAenaV78BG2nSr3/gCGdBkLpfLscddCkzkL36Q5b67SrLLfvAJTOUzW39x4FHVCFriPVgg=="],
"@shikijs/transformers": ["@shikijs/transformers@3.8.1", "", { "dependencies": { "@shikijs/core": "3.8.1", "@shikijs/types": "3.8.1" } }, "sha512-nmTyFfBrhJk6HJi118jes0wuWdfKXeVUq1Nq+hm8h6wbk1KUfvtg+LY/uDfxZD2VDItHO3QoINIs3NtoKBmgxw=="], "@shikijs/transformers": ["@shikijs/transformers@3.13.0", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/types": "3.13.0" } }, "sha512-833lcuVzcRiG+fXvgslWsM2f4gHpjEgui1ipIknSizRuTgMkNZupiXE5/TVJ6eSYfhNBFhBZKkReKWO2GgYmqA=="],
"@shikijs/twoslash": ["@shikijs/twoslash@3.13.0", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/types": "3.13.0", "twoslash": "^0.3.4" }, "peerDependencies": { "typescript": ">=5.5.0" } }, "sha512-OmNKNoZ8Hevt4VKQHfJL+hrsrqLSnW/Nz7RMutuBqXBCIYZWk80HnF9pcXEwRmy9MN0MGRmZCW2rDDP8K7Bxkw=="], "@shikijs/twoslash": ["@shikijs/twoslash@3.13.0", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/types": "3.13.0", "twoslash": "^0.3.4" }, "peerDependencies": { "typescript": ">=5.5.0" } }, "sha512-OmNKNoZ8Hevt4VKQHfJL+hrsrqLSnW/Nz7RMutuBqXBCIYZWk80HnF9pcXEwRmy9MN0MGRmZCW2rDDP8K7Bxkw=="],
"@shikijs/types": ["@shikijs/types@3.8.1", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-5C39Q8/8r1I26suLh+5TPk1DTrbY/kn3IdWA5HdizR0FhlhD05zx5nKCqhzSfDHH3p4S0ZefxWd77DLV+8FhGg=="], "@shikijs/types": ["@shikijs/types@3.13.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw=="],
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
@ -291,8 +297,6 @@
"@types/node": ["@types/node@18.19.120", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-WtCGHFXnVI8WHLxDAt5TbnCM4eSE+nI0QN2NJtwzcgMhht2eNz6V9evJrk+lwC8bCY8OWV5Ym8Jz7ZEyGnKnMA=="], "@types/node": ["@types/node@18.19.120", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-WtCGHFXnVI8WHLxDAt5TbnCM4eSE+nI0QN2NJtwzcgMhht2eNz6V9evJrk+lwC8bCY8OWV5Ym8Jz7ZEyGnKnMA=="],
"@types/node-fetch": ["@types/node-fetch@2.6.12", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.0" } }, "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA=="],
"@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="], "@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
@ -315,9 +319,9 @@
"address": ["address@1.2.2", "", {}, "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA=="], "address": ["address@1.2.2", "", {}, "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA=="],
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], "adm-zip": ["adm-zip@0.5.16", "", {}, "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ=="],
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"aggregate-error": ["aggregate-error@4.0.1", "", { "dependencies": { "clean-stack": "^4.0.0", "indent-string": "^5.0.0" } }, "sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w=="], "aggregate-error": ["aggregate-error@4.0.1", "", { "dependencies": { "clean-stack": "^4.0.0", "indent-string": "^5.0.0" } }, "sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w=="],
@ -345,7 +349,9 @@
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
"arktype": ["arktype@2.1.22", "", { "dependencies": { "@ark/schema": "0.49.0", "@ark/util": "0.49.0" } }, "sha512-xdzl6WcAhrdahvRRnXaNwsipCgHuNoLobRqhiP8RjnfL9Gp947abGlo68GAIyLtxbD+MLzNyH2YR4kEqioMmYQ=="], "arkregex": ["arkregex@0.0.3", "", { "dependencies": { "@ark/util": "0.55.0" } }, "sha512-bU21QJOJEFJK+BPNgv+5bVXkvRxyAvgnon75D92newgHxkBJTgiFwQxusyViYyJkETsddPlHyspshDQcCzmkNg=="],
"arktype": ["arktype@2.1.27", "", { "dependencies": { "@ark/schema": "0.55.0", "@ark/util": "0.55.0", "arkregex": "0.0.3" } }, "sha512-enctOHxI4SULBv/TDtCVi5M8oLd4J5SVlPUblXDzSsOYQNMzmVbUosGBnJuZDKmFlN5Ie0/QVEuTE+Z5X1UhsQ=="],
"array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="],
@ -425,7 +431,7 @@
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
"chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
"character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
@ -435,7 +441,7 @@
"character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
"chardet": ["chardet@0.7.0", "", {}, "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="], "chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="],
"chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
@ -463,6 +469,8 @@
"color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
"color-blend": ["color-blend@4.0.0", "", {}, "sha512-fYODTHhI/NG+B5GnzvuL3kiFrK/UnkUezWFTgEPBTY5V+kpyfAn95Vn9sJeeCX6omrCOdxnqCL3CvH+6sXtIbw=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
@ -507,6 +515,10 @@
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
"decode-bmp": ["decode-bmp@0.2.1", "", { "dependencies": { "@canvas/image-data": "^1.0.0", "to-data-view": "^1.1.0" } }, "sha512-NiOaGe+GN0KJqi2STf24hfMkFitDUaIoUU3eKvP/wAbLe8o6FuW5n/x7MHPR0HKvBokp6MQY/j7w8lewEeVCIA=="],
"decode-ico": ["decode-ico@0.4.1", "", { "dependencies": { "@canvas/image-data": "^1.0.0", "decode-bmp": "^0.2.0", "to-data-view": "^1.1.0" } }, "sha512-69NZfbKIzux1vBOd31al3XnMnH+2mqDhEgLdpygErm4d60N+UwA5Sq5WFjmEDQzumgB9fElojGwWG0vybVfFmA=="],
"decode-named-character-reference": ["decode-named-character-reference@1.2.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q=="], "decode-named-character-reference": ["decode-named-character-reference@1.2.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q=="],
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
@ -665,12 +677,10 @@
"form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="], "form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="],
"form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="], "form-data-encoder": ["form-data-encoder@2.1.4", "", {}, "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw=="],
"format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="], "format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="],
"formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="],
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
@ -769,7 +779,7 @@
"hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="],
"honcho-ai": ["honcho-ai@0.0.11", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-SUl/PnMldTCz8G4S8faP00M2iFd9qWDkI5U8w0FQ7OC6SgKzTf1nJ/j3gyzctzR2IZ6LrOz/2d5OwO4f/PCMww=="], "hex-rgb": ["hex-rgb@5.0.0", "", {}, "sha512-NQO+lgVUCtHxZ792FodgW0zflK+ozS9X9dwGp9XvvmPlH7pyxd588cn24TD3rmPm/N0AIRXF10Otah8yKqGw4w=="],
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
@ -783,9 +793,9 @@
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], "ico-endec": ["ico-endec@0.1.6", "", {}, "sha512-ZdLU38ZoED3g1j3iEyzcQj+wAkY2xfWNkymszfJPoxucIUhK7NayQ+/C4Kv0nDFMIsbtbEHldv3V8PU494/ueQ=="],
"iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], "iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
@ -819,7 +829,7 @@
"is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
"is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="],
"is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="],
@ -953,8 +963,6 @@
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"mdast": ["mdast@3.0.0", "", {}, "sha512-xySmf8g4fPKMeC07jXGz971EkLbWAJ83s4US2Tj9lEdnZ142UP5grN73H1Xd3HzrdbU5o9GYYP/y8F9ZSwLE9g=="],
"mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="], "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="],
@ -1091,7 +1099,7 @@
"minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="], "minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="],
"mint": ["mint@4.2.123", "", { "dependencies": { "@mintlify/cli": "4.0.727" }, "bin": { "mint": "index.js", "mintlify": "index.js" } }, "sha512-md52nrIkMZdtFwWVxpa1vu9msyBMtDePRuMFsTOcdWqYq11JO07L4lyOTVcMe6IPtRbvhjbbKiWihKB2xLZecQ=="], "mint": ["mint@4.2.204", "", { "dependencies": { "@mintlify/cli": "4.0.808" }, "bin": { "mint": "index.js", "mintlify": "index.js" } }, "sha512-qOfwgnDKmhzAV+y1b787P1Lv2vYIrurvZs0Q7Kwx7zpJ+uiXjzEhYAzlm0Rj/SITlhwllJrv30axEaSq37sYMA=="],
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
@ -1117,9 +1125,7 @@
"nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="], "nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="],
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], "node-fetch": ["node-fetch@2.6.7", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ=="],
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
@ -1373,11 +1379,13 @@
"sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], "sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="],
"sharp-ico": ["sharp-ico@0.1.5", "", { "dependencies": { "decode-ico": "*", "ico-endec": "*", "sharp": "*" } }, "sha512-a3jODQl82NPp1d5OYb0wY+oFaPk7AvyxipIowCHk7pBsZCWgbe0yAkU2OOXdoH0ENyANhyOQbs9xkAiRHcF02Q=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"shiki": ["shiki@3.8.1", "", { "dependencies": { "@shikijs/core": "3.8.1", "@shikijs/engine-javascript": "3.8.1", "@shikijs/engine-oniguruma": "3.8.1", "@shikijs/langs": "3.8.1", "@shikijs/themes": "3.8.1", "@shikijs/types": "3.8.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-+MYIyjwGPCaegbpBeFN9+oOifI8CKiKG3awI/6h3JeT85c//H2wDW/xCJEGuQ5jPqtbboKNqNy+JyX9PYpGwNg=="], "shiki": ["shiki@3.13.0", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/engine-javascript": "3.13.0", "@shikijs/engine-oniguruma": "3.13.0", "@shikijs/langs": "3.13.0", "@shikijs/themes": "3.13.0", "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-aZW4l8Og16CokuCLf8CF8kq+KK2yOygapU5m3+hoGw0Mdosc6fPitjM+ujYarppj5ZIKGyPDPP1vqmQhr+5/0g=="],
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
@ -1467,6 +1475,8 @@
"tmp": ["tmp@0.0.33", "", { "dependencies": { "os-tmpdir": "~1.0.2" } }, "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw=="], "tmp": ["tmp@0.0.33", "", { "dependencies": { "os-tmpdir": "~1.0.2" } }, "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw=="],
"to-data-view": ["to-data-view@1.1.0", "", {}, "sha512-1eAdufMg6mwgmlojAx3QeMnzB/BTVp7Tbndi3U7ftcT2zCZadjxkkmLmd97zmaxWi+sgGcgWrokmpEoy0Dn0vQ=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
@ -1553,6 +1563,8 @@
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
"uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="],
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
@ -1565,8 +1577,6 @@
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
"web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="],
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
@ -1607,7 +1617,7 @@
"yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="], "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
"yoctocolors-cjs": ["yoctocolors-cjs@2.1.2", "", {}, "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA=="], "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="],
"yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="],
@ -1621,9 +1631,15 @@
"@asyncapi/parser/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], "@asyncapi/parser/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
"@asyncapi/parser/node-fetch": ["node-fetch@2.6.7", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ=="], "@inquirer/checkbox/@inquirer/core": ["@inquirer/core@10.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A=="],
"@inquirer/checkbox/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], "@inquirer/checkbox/@inquirer/type": ["@inquirer/type@3.0.10", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="],
"@inquirer/confirm/@inquirer/core": ["@inquirer/core@10.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A=="],
"@inquirer/confirm/@inquirer/type": ["@inquirer/type@3.0.10", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="],
"@inquirer/core/@inquirer/figures": ["@inquirer/figures@1.0.13", "", {}, "sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw=="],
"@inquirer/core/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], "@inquirer/core/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="],
@ -1631,9 +1647,39 @@
"@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], "@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
"@inquirer/password/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], "@inquirer/core/yoctocolors-cjs": ["yoctocolors-cjs@2.1.2", "", {}, "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA=="],
"@inquirer/select/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], "@inquirer/editor/@inquirer/core": ["@inquirer/core@10.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A=="],
"@inquirer/editor/@inquirer/type": ["@inquirer/type@3.0.10", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="],
"@inquirer/expand/@inquirer/core": ["@inquirer/core@10.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A=="],
"@inquirer/expand/@inquirer/type": ["@inquirer/type@3.0.10", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="],
"@inquirer/input/@inquirer/core": ["@inquirer/core@10.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A=="],
"@inquirer/input/@inquirer/type": ["@inquirer/type@3.0.10", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="],
"@inquirer/number/@inquirer/core": ["@inquirer/core@10.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A=="],
"@inquirer/number/@inquirer/type": ["@inquirer/type@3.0.10", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="],
"@inquirer/password/@inquirer/core": ["@inquirer/core@10.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A=="],
"@inquirer/password/@inquirer/type": ["@inquirer/type@3.0.10", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="],
"@inquirer/rawlist/@inquirer/core": ["@inquirer/core@10.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A=="],
"@inquirer/rawlist/@inquirer/type": ["@inquirer/type@3.0.10", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="],
"@inquirer/search/@inquirer/core": ["@inquirer/core@10.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A=="],
"@inquirer/search/@inquirer/type": ["@inquirer/type@3.0.10", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="],
"@inquirer/select/@inquirer/core": ["@inquirer/core@10.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A=="],
"@inquirer/select/@inquirer/type": ["@inquirer/type@3.0.10", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="],
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
@ -1641,38 +1687,18 @@
"@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
"@mintlify/cli/@mintlify/common": ["@mintlify/common@1.0.537", "", { "dependencies": { "@asyncapi/parser": "^3.4.0", "@mintlify/mdx": "2.0.11", "@mintlify/models": "0.0.229", "@mintlify/openapi-parser": "^0.0.7", "@mintlify/validation": "0.1.471", "@sindresorhus/slugify": "^2.1.1", "acorn": "^8.11.2", "acorn-jsx": "^5.3.2", "estree-util-to-js": "^2.0.0", "estree-walker": "^3.0.3", "gray-matter": "^4.0.3", "hast-util-from-html": "^2.0.3", "hast-util-to-html": "^9.0.4", "hast-util-to-text": "^4.0.2", "js-yaml": "^4.1.0", "lodash": "^4.17.21", "mdast": "^3.0.0", "mdast-util-from-markdown": "^2.0.2", "mdast-util-gfm": "^3.0.0", "mdast-util-mdx": "^3.0.0", "mdast-util-mdx-jsx": "^3.1.3", "micromark-extension-gfm": "^3.0.0", "micromark-extension-mdx-jsx": "^3.0.1", "micromark-extension-mdxjs": "^3.0.0", "openapi-types": "^12.0.0", "postcss": "^8.5.6", "remark": "^15.0.1", "remark-frontmatter": "^5.0.0", "remark-gfm": "^4.0.0", "remark-math": "^6.0.0", "remark-mdx": "^3.1.0", "remark-stringify": "^11.0.0", "tailwindcss": "^3.4.4", "unified": "^11.0.5", "unist-builder": "^4.0.0", "unist-util-map": "^4.0.0", "unist-util-remove": "^4.0.0", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-Mqm9OuXhaL0mVxkbPZHTIYNH8cVZdh9lsi5GHSGl8U7Vc+qHfv0CS+fempV1RAg6zRBjdSwD5rh43RMrPJSl/Q=="],
"@mintlify/common/@mintlify/models": ["@mintlify/models@0.0.213", "", { "dependencies": { "axios": "^1.8.3", "openapi-types": "^12.0.0" } }, "sha512-fiAVlRwUJxeI8ikpuXdcQLapHGoFHdUebIQMrZEt/UB74fMEnmzvLU01edCKLClPfw+DsceVXD7E8inWfpZSnA=="],
"@mintlify/common/@mintlify/validation": ["@mintlify/validation@0.1.424", "", { "dependencies": { "@mintlify/models": "0.0.213", "lcm": "^0.0.3", "lodash": "^4.17.21", "openapi-types": "^12.0.0", "zod": "^3.20.6", "zod-to-json-schema": "^3.20.3" } }, "sha512-mA9MoYT78KtVf34jXh01j/eqbj5agYplUYt+YYVPzGXnWM/lISlNMvQVwXekZYOPbcxATIy68Rn/Zk9ARgMWcQ=="],
"@mintlify/link-rot/@mintlify/common": ["@mintlify/common@1.0.537", "", { "dependencies": { "@asyncapi/parser": "^3.4.0", "@mintlify/mdx": "2.0.11", "@mintlify/models": "0.0.229", "@mintlify/openapi-parser": "^0.0.7", "@mintlify/validation": "0.1.471", "@sindresorhus/slugify": "^2.1.1", "acorn": "^8.11.2", "acorn-jsx": "^5.3.2", "estree-util-to-js": "^2.0.0", "estree-walker": "^3.0.3", "gray-matter": "^4.0.3", "hast-util-from-html": "^2.0.3", "hast-util-to-html": "^9.0.4", "hast-util-to-text": "^4.0.2", "js-yaml": "^4.1.0", "lodash": "^4.17.21", "mdast": "^3.0.0", "mdast-util-from-markdown": "^2.0.2", "mdast-util-gfm": "^3.0.0", "mdast-util-mdx": "^3.0.0", "mdast-util-mdx-jsx": "^3.1.3", "micromark-extension-gfm": "^3.0.0", "micromark-extension-mdx-jsx": "^3.0.1", "micromark-extension-mdxjs": "^3.0.0", "openapi-types": "^12.0.0", "postcss": "^8.5.6", "remark": "^15.0.1", "remark-frontmatter": "^5.0.0", "remark-gfm": "^4.0.0", "remark-math": "^6.0.0", "remark-mdx": "^3.1.0", "remark-stringify": "^11.0.0", "tailwindcss": "^3.4.4", "unified": "^11.0.5", "unist-builder": "^4.0.0", "unist-util-map": "^4.0.0", "unist-util-remove": "^4.0.0", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-Mqm9OuXhaL0mVxkbPZHTIYNH8cVZdh9lsi5GHSGl8U7Vc+qHfv0CS+fempV1RAg6zRBjdSwD5rh43RMrPJSl/Q=="],
"@mintlify/link-rot/unist-util-visit": ["unist-util-visit@4.1.2", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0", "unist-util-visit-parents": "^5.1.1" } }, "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg=="], "@mintlify/link-rot/unist-util-visit": ["unist-util-visit@4.1.2", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0", "unist-util-visit-parents": "^5.1.1" } }, "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg=="],
"@mintlify/mdx/react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], "@mintlify/mdx/react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="],
"@mintlify/prebuild/@mintlify/common": ["@mintlify/common@1.0.537", "", { "dependencies": { "@asyncapi/parser": "^3.4.0", "@mintlify/mdx": "2.0.11", "@mintlify/models": "0.0.229", "@mintlify/openapi-parser": "^0.0.7", "@mintlify/validation": "0.1.471", "@sindresorhus/slugify": "^2.1.1", "acorn": "^8.11.2", "acorn-jsx": "^5.3.2", "estree-util-to-js": "^2.0.0", "estree-walker": "^3.0.3", "gray-matter": "^4.0.3", "hast-util-from-html": "^2.0.3", "hast-util-to-html": "^9.0.4", "hast-util-to-text": "^4.0.2", "js-yaml": "^4.1.0", "lodash": "^4.17.21", "mdast": "^3.0.0", "mdast-util-from-markdown": "^2.0.2", "mdast-util-gfm": "^3.0.0", "mdast-util-mdx": "^3.0.0", "mdast-util-mdx-jsx": "^3.1.3", "micromark-extension-gfm": "^3.0.0", "micromark-extension-mdx-jsx": "^3.0.1", "micromark-extension-mdxjs": "^3.0.0", "openapi-types": "^12.0.0", "postcss": "^8.5.6", "remark": "^15.0.1", "remark-frontmatter": "^5.0.0", "remark-gfm": "^4.0.0", "remark-math": "^6.0.0", "remark-mdx": "^3.1.0", "remark-stringify": "^11.0.0", "tailwindcss": "^3.4.4", "unified": "^11.0.5", "unist-builder": "^4.0.0", "unist-util-map": "^4.0.0", "unist-util-remove": "^4.0.0", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-Mqm9OuXhaL0mVxkbPZHTIYNH8cVZdh9lsi5GHSGl8U7Vc+qHfv0CS+fempV1RAg6zRBjdSwD5rh43RMrPJSl/Q=="],
"@mintlify/prebuild/@mintlify/scraping": ["@mintlify/scraping@4.0.396", "", { "dependencies": { "@mintlify/common": "1.0.537", "@mintlify/openapi-parser": "^0.0.7", "fs-extra": "^11.1.1", "hast-util-to-mdast": "^10.1.0", "js-yaml": "^4.1.0", "mdast-util-mdx-jsx": "^3.1.3", "neotraverse": "^0.6.18", "puppeteer": "^22.14.0", "rehype-parse": "^9.0.0", "remark-gfm": "^4.0.0", "remark-mdx": "^3.0.1", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", "yargs": "^17.6.0", "zod": "^3.20.6" }, "bin": { "mintlify-scrape": "bin/cli.js" } }, "sha512-cPavXt7yrnyGLNb5QEY8C8anPfw9Tj8TL7CVE76Ey/+l7sae9inLd2f5E3vKYfWrqyIouxGmvA+pFuenOt66aw=="],
"@mintlify/prebuild/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
"@mintlify/prebuild/unist-util-visit": ["unist-util-visit@4.1.2", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0", "unist-util-visit-parents": "^5.1.1" } }, "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg=="], "@mintlify/prebuild/unist-util-visit": ["unist-util-visit@4.1.2", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0", "unist-util-visit-parents": "^5.1.1" } }, "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg=="],
"@mintlify/previewing/@mintlify/common": ["@mintlify/common@1.0.537", "", { "dependencies": { "@asyncapi/parser": "^3.4.0", "@mintlify/mdx": "2.0.11", "@mintlify/models": "0.0.229", "@mintlify/openapi-parser": "^0.0.7", "@mintlify/validation": "0.1.471", "@sindresorhus/slugify": "^2.1.1", "acorn": "^8.11.2", "acorn-jsx": "^5.3.2", "estree-util-to-js": "^2.0.0", "estree-walker": "^3.0.3", "gray-matter": "^4.0.3", "hast-util-from-html": "^2.0.3", "hast-util-to-html": "^9.0.4", "hast-util-to-text": "^4.0.2", "js-yaml": "^4.1.0", "lodash": "^4.17.21", "mdast": "^3.0.0", "mdast-util-from-markdown": "^2.0.2", "mdast-util-gfm": "^3.0.0", "mdast-util-mdx": "^3.0.0", "mdast-util-mdx-jsx": "^3.1.3", "micromark-extension-gfm": "^3.0.0", "micromark-extension-mdx-jsx": "^3.0.1", "micromark-extension-mdxjs": "^3.0.0", "openapi-types": "^12.0.0", "postcss": "^8.5.6", "remark": "^15.0.1", "remark-frontmatter": "^5.0.0", "remark-gfm": "^4.0.0", "remark-math": "^6.0.0", "remark-mdx": "^3.1.0", "remark-stringify": "^11.0.0", "tailwindcss": "^3.4.4", "unified": "^11.0.5", "unist-builder": "^4.0.0", "unist-util-map": "^4.0.0", "unist-util-remove": "^4.0.0", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-Mqm9OuXhaL0mVxkbPZHTIYNH8cVZdh9lsi5GHSGl8U7Vc+qHfv0CS+fempV1RAg6zRBjdSwD5rh43RMrPJSl/Q=="],
"@mintlify/previewing/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
"@mintlify/previewing/unist-util-visit": ["unist-util-visit@4.1.2", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0", "unist-util-visit-parents": "^5.1.1" } }, "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg=="], "@mintlify/previewing/unist-util-visit": ["unist-util-visit@4.1.2", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0", "unist-util-visit-parents": "^5.1.1" } }, "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg=="],
"@shikijs/twoslash/@shikijs/core": ["@shikijs/core@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-3P8rGsg2Eh2qIHekwuQjzWhKI4jV97PhvYjYUzGqjvJfqdQPz+nMlfWahU24GZAyW1FxFI1sYjyhfh5CoLmIUA=="],
"@shikijs/twoslash/@shikijs/types": ["@shikijs/types@3.13.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw=="],
"@stoplight/better-ajv-errors/leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="], "@stoplight/better-ajv-errors/leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="],
"@stoplight/json-ref-readers/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
"@stoplight/json-ref-readers/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], "@stoplight/json-ref-readers/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="],
"@stoplight/spectral-core/@stoplight/types": ["@stoplight/types@13.6.0", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-dzyuzvUjv3m1wmhPfq82lCVYGcXG0xUYgqnWfCq3PCVR4BKFhjdkHrnJ+jIDoMKvXb05AZP/ObQF6+NpDo29IQ=="], "@stoplight/spectral-core/@stoplight/types": ["@stoplight/types@13.6.0", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-dzyuzvUjv3m1wmhPfq82lCVYGcXG0xUYgqnWfCq3PCVR4BKFhjdkHrnJ+jIDoMKvXb05AZP/ObQF6+NpDo29IQ=="],
@ -1683,10 +1709,14 @@
"@stoplight/spectral-parsers/@stoplight/types": ["@stoplight/types@14.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g=="], "@stoplight/spectral-parsers/@stoplight/types": ["@stoplight/types@14.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g=="],
"@stoplight/spectral-runtime/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
"@stoplight/yaml/@stoplight/types": ["@stoplight/types@14.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g=="], "@stoplight/yaml/@stoplight/types": ["@stoplight/types@14.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g=="],
"body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"chromium-bidi/zod": ["zod@3.23.8", "", {}, "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g=="], "chromium-bidi/zod": ["zod@3.23.8", "", {}, "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g=="],
@ -1705,10 +1735,16 @@
"engine.io/ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="], "engine.io/ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="],
"error-ex/is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="],
"escodegen/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "escodegen/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"external-editor/chardet": ["chardet@0.7.0", "", {}, "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="],
"external-editor/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
"extract-zip/get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], "extract-zip/get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
@ -1723,14 +1759,12 @@
"glob/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], "glob/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
"got/form-data-encoder": ["form-data-encoder@2.1.4", "", {}, "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw=="],
"gray-matter/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], "gray-matter/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="],
"ink/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
"ink/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], "ink/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
"inquirer/@inquirer/prompts": ["@inquirer/prompts@7.7.1", "", { "dependencies": { "@inquirer/checkbox": "^4.2.0", "@inquirer/confirm": "^5.1.14", "@inquirer/editor": "^4.2.15", "@inquirer/expand": "^4.0.17", "@inquirer/input": "^4.2.1", "@inquirer/number": "^3.0.17", "@inquirer/password": "^4.0.17", "@inquirer/rawlist": "^4.1.5", "@inquirer/search": "^3.0.17", "@inquirer/select": "^4.3.1" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-XDxPrEWeWUBy8scAXzXuFY45r/q49R0g72bUzgQXZ1DY/xEFX+ESDMkTQolcb5jRBzaNJX2W8XQl6krMNDTjaA=="],
"inquirer/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], "inquirer/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="],
"ip-address/sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], "ip-address/sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
@ -1751,6 +1785,8 @@
"public-ip/got": ["got@12.6.1", "", { "dependencies": { "@sindresorhus/is": "^5.2.0", "@szmarczak/http-timer": "^5.0.1", "cacheable-lookup": "^7.0.0", "cacheable-request": "^10.2.8", "decompress-response": "^6.0.0", "form-data-encoder": "^2.1.2", "get-stream": "^6.0.1", "http2-wrapper": "^2.1.10", "lowercase-keys": "^3.0.0", "p-cancelable": "^3.0.0", "responselike": "^3.0.0" } }, "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ=="], "public-ip/got": ["got@12.6.1", "", { "dependencies": { "@sindresorhus/is": "^5.2.0", "@szmarczak/http-timer": "^5.0.1", "cacheable-lookup": "^7.0.0", "cacheable-request": "^10.2.8", "decompress-response": "^6.0.0", "form-data-encoder": "^2.1.2", "get-stream": "^6.0.1", "http2-wrapper": "^2.1.10", "lowercase-keys": "^3.0.0", "p-cancelable": "^3.0.0", "responselike": "^3.0.0" } }, "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ=="],
"raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
"react-dom/react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], "react-dom/react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="],
"react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], "react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
@ -1759,8 +1795,6 @@
"send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], "send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
"simple-swizzle/is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="],
"slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.0.0", "", { "dependencies": { "get-east-asian-width": "^1.0.0" } }, "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA=="], "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.0.0", "", { "dependencies": { "get-east-asian-width": "^1.0.0" } }, "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA=="],
"socket.io/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], "socket.io/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
@ -1781,48 +1815,66 @@
"wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"@inquirer/checkbox/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], "@inquirer/checkbox/@inquirer/core/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"@inquirer/checkbox/@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
"@inquirer/confirm/@inquirer/core/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"@inquirer/confirm/@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
"@inquirer/core/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], "@inquirer/core/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
"@inquirer/core/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "@inquirer/core/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"@inquirer/password/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], "@inquirer/editor/@inquirer/core/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"@inquirer/select/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], "@inquirer/editor/@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
"@inquirer/expand/@inquirer/core/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"@inquirer/expand/@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
"@inquirer/input/@inquirer/core/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"@inquirer/input/@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
"@inquirer/number/@inquirer/core/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"@inquirer/number/@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
"@inquirer/password/@inquirer/core/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"@inquirer/password/@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
"@inquirer/rawlist/@inquirer/core/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"@inquirer/rawlist/@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
"@inquirer/search/@inquirer/core/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"@inquirer/search/@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
"@inquirer/select/@inquirer/core/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"@inquirer/select/@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"@mintlify/cli/@mintlify/common/@mintlify/mdx": ["@mintlify/mdx@2.0.11", "", { "dependencies": { "@shikijs/transformers": "^3.11.0", "@shikijs/twoslash": "^3.12.2", "hast-util-to-string": "^3.0.1", "mdast-util-from-markdown": "^2.0.2", "mdast-util-gfm": "^3.1.0", "mdast-util-mdx-jsx": "^3.2.0", "mdast-util-to-hast": "^13.2.0", "next-mdx-remote-client": "^1.0.3", "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.0", "remark-math": "^6.0.0", "remark-smartypants": "^3.0.2", "shiki": "^3.11.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "@radix-ui/react-popover": "^1.1.15", "react": "^18.3.1", "react-dom": "^18.3.1" } }, "sha512-yXwuM0BNCxNaJetPrh89c5Q2lhzU2al4QrOM3zLUdrPOdjOpPmv8ewcdiXV/qIhZDpl5Ll9k47dsz33bZjVWTg=="],
"@mintlify/link-rot/@mintlify/common/@mintlify/mdx": ["@mintlify/mdx@2.0.11", "", { "dependencies": { "@shikijs/transformers": "^3.11.0", "@shikijs/twoslash": "^3.12.2", "hast-util-to-string": "^3.0.1", "mdast-util-from-markdown": "^2.0.2", "mdast-util-gfm": "^3.1.0", "mdast-util-mdx-jsx": "^3.2.0", "mdast-util-to-hast": "^13.2.0", "next-mdx-remote-client": "^1.0.3", "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.0", "remark-math": "^6.0.0", "remark-smartypants": "^3.0.2", "shiki": "^3.11.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "@radix-ui/react-popover": "^1.1.15", "react": "^18.3.1", "react-dom": "^18.3.1" } }, "sha512-yXwuM0BNCxNaJetPrh89c5Q2lhzU2al4QrOM3zLUdrPOdjOpPmv8ewcdiXV/qIhZDpl5Ll9k47dsz33bZjVWTg=="],
"@mintlify/link-rot/@mintlify/common/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="],
"@mintlify/link-rot/unist-util-visit/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], "@mintlify/link-rot/unist-util-visit/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
"@mintlify/link-rot/unist-util-visit/unist-util-is": ["unist-util-is@5.2.1", "", { "dependencies": { "@types/unist": "^2.0.0" } }, "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw=="], "@mintlify/link-rot/unist-util-visit/unist-util-is": ["unist-util-is@5.2.1", "", { "dependencies": { "@types/unist": "^2.0.0" } }, "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw=="],
"@mintlify/link-rot/unist-util-visit/unist-util-visit-parents": ["unist-util-visit-parents@5.1.3", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0" } }, "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg=="], "@mintlify/link-rot/unist-util-visit/unist-util-visit-parents": ["unist-util-visit-parents@5.1.3", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0" } }, "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg=="],
"@mintlify/prebuild/@mintlify/common/@mintlify/mdx": ["@mintlify/mdx@2.0.11", "", { "dependencies": { "@shikijs/transformers": "^3.11.0", "@shikijs/twoslash": "^3.12.2", "hast-util-to-string": "^3.0.1", "mdast-util-from-markdown": "^2.0.2", "mdast-util-gfm": "^3.1.0", "mdast-util-mdx-jsx": "^3.2.0", "mdast-util-to-hast": "^13.2.0", "next-mdx-remote-client": "^1.0.3", "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.0", "remark-math": "^6.0.0", "remark-smartypants": "^3.0.2", "shiki": "^3.11.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "@radix-ui/react-popover": "^1.1.15", "react": "^18.3.1", "react-dom": "^18.3.1" } }, "sha512-yXwuM0BNCxNaJetPrh89c5Q2lhzU2al4QrOM3zLUdrPOdjOpPmv8ewcdiXV/qIhZDpl5Ll9k47dsz33bZjVWTg=="],
"@mintlify/prebuild/@mintlify/common/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="],
"@mintlify/prebuild/@mintlify/scraping/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="],
"@mintlify/prebuild/unist-util-visit/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], "@mintlify/prebuild/unist-util-visit/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
"@mintlify/prebuild/unist-util-visit/unist-util-is": ["unist-util-is@5.2.1", "", { "dependencies": { "@types/unist": "^2.0.0" } }, "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw=="], "@mintlify/prebuild/unist-util-visit/unist-util-is": ["unist-util-is@5.2.1", "", { "dependencies": { "@types/unist": "^2.0.0" } }, "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw=="],
"@mintlify/prebuild/unist-util-visit/unist-util-visit-parents": ["unist-util-visit-parents@5.1.3", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0" } }, "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg=="], "@mintlify/prebuild/unist-util-visit/unist-util-visit-parents": ["unist-util-visit-parents@5.1.3", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0" } }, "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg=="],
"@mintlify/previewing/@mintlify/common/@mintlify/mdx": ["@mintlify/mdx@2.0.11", "", { "dependencies": { "@shikijs/transformers": "^3.11.0", "@shikijs/twoslash": "^3.12.2", "hast-util-to-string": "^3.0.1", "mdast-util-from-markdown": "^2.0.2", "mdast-util-gfm": "^3.1.0", "mdast-util-mdx-jsx": "^3.2.0", "mdast-util-to-hast": "^13.2.0", "next-mdx-remote-client": "^1.0.3", "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.0", "remark-math": "^6.0.0", "remark-smartypants": "^3.0.2", "shiki": "^3.11.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "@radix-ui/react-popover": "^1.1.15", "react": "^18.3.1", "react-dom": "^18.3.1" } }, "sha512-yXwuM0BNCxNaJetPrh89c5Q2lhzU2al4QrOM3zLUdrPOdjOpPmv8ewcdiXV/qIhZDpl5Ll9k47dsz33bZjVWTg=="],
"@mintlify/previewing/@mintlify/common/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="],
"@mintlify/previewing/unist-util-visit/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], "@mintlify/previewing/unist-util-visit/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
"@mintlify/previewing/unist-util-visit/unist-util-is": ["unist-util-is@5.2.1", "", { "dependencies": { "@types/unist": "^2.0.0" } }, "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw=="], "@mintlify/previewing/unist-util-visit/unist-util-is": ["unist-util-is@5.2.1", "", { "dependencies": { "@types/unist": "^2.0.0" } }, "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw=="],
@ -1851,12 +1903,28 @@
"ink/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], "ink/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
"inquirer/@inquirer/prompts/@inquirer/checkbox": ["@inquirer/checkbox@4.2.0", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/figures": "^1.0.13", "@inquirer/type": "^3.0.8", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-fdSw07FLJEU5vbpOPzXo5c6xmMGDzbZE2+niuDHX5N6mc6V0Ebso/q3xiHra4D73+PMsC8MJmcaZKuAAoaQsSA=="],
"inquirer/@inquirer/prompts/@inquirer/confirm": ["@inquirer/confirm@5.1.14", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/type": "^3.0.8" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-5yR4IBfe0kXe59r1YCTG8WXkUbl7Z35HK87Sw+WUyGD8wNUx7JvY7laahzeytyE1oLn74bQnL7hstctQxisQ8Q=="],
"inquirer/@inquirer/prompts/@inquirer/editor": ["@inquirer/editor@4.2.15", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/type": "^3.0.8", "external-editor": "^3.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-wst31XT8DnGOSS4nNJDIklGKnf+8shuauVrWzgKegWUe28zfCftcWZ2vktGdzJgcylWSS2SrDnYUb6alZcwnCQ=="],
"inquirer/@inquirer/prompts/@inquirer/expand": ["@inquirer/expand@4.0.17", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/type": "^3.0.8", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PSqy9VmJx/VbE3CT453yOfNa+PykpKg/0SYP7odez1/NWBGuDXgPhp4AeGYYKjhLn5lUUavVS/JbeYMPdH50Mw=="],
"inquirer/@inquirer/prompts/@inquirer/input": ["@inquirer/input@4.2.1", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/type": "^3.0.8" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-tVC+O1rBl0lJpoUZv4xY+WGWY8V5b0zxU1XDsMsIHYregdh7bN5X5QnIONNBAl0K765FYlAfNHS2Bhn7SSOVow=="],
"inquirer/@inquirer/prompts/@inquirer/number": ["@inquirer/number@3.0.17", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/type": "^3.0.8" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-GcvGHkyIgfZgVnnimURdOueMk0CztycfC8NZTiIY9arIAkeOgt6zG57G+7vC59Jns3UX27LMkPKnKWAOF5xEYg=="],
"inquirer/@inquirer/prompts/@inquirer/password": ["@inquirer/password@4.0.17", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/type": "^3.0.8", "ansi-escapes": "^4.3.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-DJolTnNeZ00E1+1TW+8614F7rOJJCM4y4BAGQ3Gq6kQIG+OJ4zr3GLjIjVVJCbKsk2jmkmv6v2kQuN/vriHdZA=="],
"inquirer/@inquirer/prompts/@inquirer/rawlist": ["@inquirer/rawlist@4.1.5", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/type": "^3.0.8", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-R5qMyGJqtDdi4Ht521iAkNqyB6p2UPuZUbMifakg1sWtu24gc2Z8CJuw8rP081OckNDMgtDCuLe42Q2Kr3BolA=="],
"inquirer/@inquirer/prompts/@inquirer/search": ["@inquirer/search@3.0.17", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/figures": "^1.0.13", "@inquirer/type": "^3.0.8", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-CuBU4BAGFqRYors4TNCYzy9X3DpKtgIW4Boi0WNkm4Ei1hvY9acxKdBdyqzqBCEe4YxSdaQQsasJlFlUJNgojw=="],
"inquirer/@inquirer/prompts/@inquirer/select": ["@inquirer/select@4.3.1", "", { "dependencies": { "@inquirer/core": "^10.1.15", "@inquirer/figures": "^1.0.13", "@inquirer/type": "^3.0.8", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Gfl/5sqOF5vS/LIrSndFgOh7jgoe0UXEizDqahFRkq5aJBLegZ6WjuMh/hVEJwlFQjyLq1z9fRtvUMkb7jM1LA=="],
"inquirer/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], "inquirer/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
"is-online/got/form-data-encoder": ["form-data-encoder@2.1.4", "", {}, "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw=="],
"public-ip/got/form-data-encoder": ["form-data-encoder@2.1.4", "", {}, "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw=="],
"send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"widest-line/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], "widest-line/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
@ -1867,98 +1935,46 @@
"wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"@mintlify/cli/@mintlify/common/@mintlify/mdx/@shikijs/transformers": ["@shikijs/transformers@3.13.0", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/types": "3.13.0" } }, "sha512-833lcuVzcRiG+fXvgslWsM2f4gHpjEgui1ipIknSizRuTgMkNZupiXE5/TVJ6eSYfhNBFhBZKkReKWO2GgYmqA=="], "@inquirer/checkbox/@inquirer/core/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"@mintlify/cli/@mintlify/common/@mintlify/mdx/react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], "@inquirer/confirm/@inquirer/core/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"@mintlify/cli/@mintlify/common/@mintlify/mdx/shiki": ["shiki@3.13.0", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/engine-javascript": "3.13.0", "@shikijs/engine-oniguruma": "3.13.0", "@shikijs/langs": "3.13.0", "@shikijs/themes": "3.13.0", "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-aZW4l8Og16CokuCLf8CF8kq+KK2yOygapU5m3+hoGw0Mdosc6fPitjM+ujYarppj5ZIKGyPDPP1vqmQhr+5/0g=="], "@inquirer/editor/@inquirer/core/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"@mintlify/link-rot/@mintlify/common/@mintlify/mdx/@shikijs/transformers": ["@shikijs/transformers@3.13.0", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/types": "3.13.0" } }, "sha512-833lcuVzcRiG+fXvgslWsM2f4gHpjEgui1ipIknSizRuTgMkNZupiXE5/TVJ6eSYfhNBFhBZKkReKWO2GgYmqA=="], "@inquirer/expand/@inquirer/core/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"@mintlify/link-rot/@mintlify/common/@mintlify/mdx/react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], "@inquirer/input/@inquirer/core/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"@mintlify/link-rot/@mintlify/common/@mintlify/mdx/shiki": ["shiki@3.13.0", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/engine-javascript": "3.13.0", "@shikijs/engine-oniguruma": "3.13.0", "@shikijs/langs": "3.13.0", "@shikijs/themes": "3.13.0", "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-aZW4l8Og16CokuCLf8CF8kq+KK2yOygapU5m3+hoGw0Mdosc6fPitjM+ujYarppj5ZIKGyPDPP1vqmQhr+5/0g=="], "@inquirer/number/@inquirer/core/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"@mintlify/prebuild/@mintlify/common/@mintlify/mdx/@shikijs/transformers": ["@shikijs/transformers@3.13.0", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/types": "3.13.0" } }, "sha512-833lcuVzcRiG+fXvgslWsM2f4gHpjEgui1ipIknSizRuTgMkNZupiXE5/TVJ6eSYfhNBFhBZKkReKWO2GgYmqA=="], "@inquirer/password/@inquirer/core/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"@mintlify/prebuild/@mintlify/common/@mintlify/mdx/react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], "@inquirer/rawlist/@inquirer/core/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"@mintlify/prebuild/@mintlify/common/@mintlify/mdx/shiki": ["shiki@3.13.0", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/engine-javascript": "3.13.0", "@shikijs/engine-oniguruma": "3.13.0", "@shikijs/langs": "3.13.0", "@shikijs/themes": "3.13.0", "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-aZW4l8Og16CokuCLf8CF8kq+KK2yOygapU5m3+hoGw0Mdosc6fPitjM+ujYarppj5ZIKGyPDPP1vqmQhr+5/0g=="], "@inquirer/search/@inquirer/core/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"@mintlify/previewing/@mintlify/common/@mintlify/mdx/@shikijs/transformers": ["@shikijs/transformers@3.13.0", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/types": "3.13.0" } }, "sha512-833lcuVzcRiG+fXvgslWsM2f4gHpjEgui1ipIknSizRuTgMkNZupiXE5/TVJ6eSYfhNBFhBZKkReKWO2GgYmqA=="], "@inquirer/select/@inquirer/core/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"@mintlify/previewing/@mintlify/common/@mintlify/mdx/react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="],
"@mintlify/previewing/@mintlify/common/@mintlify/mdx/shiki": ["shiki@3.13.0", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/engine-javascript": "3.13.0", "@shikijs/engine-oniguruma": "3.13.0", "@shikijs/langs": "3.13.0", "@shikijs/themes": "3.13.0", "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-aZW4l8Og16CokuCLf8CF8kq+KK2yOygapU5m3+hoGw0Mdosc6fPitjM+ujYarppj5ZIKGyPDPP1vqmQhr+5/0g=="],
"cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], "cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"ink/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], "ink/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"inquirer/@inquirer/prompts/@inquirer/checkbox/@inquirer/figures": ["@inquirer/figures@1.0.13", "", {}, "sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw=="],
"inquirer/@inquirer/prompts/@inquirer/checkbox/yoctocolors-cjs": ["yoctocolors-cjs@2.1.2", "", {}, "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA=="],
"inquirer/@inquirer/prompts/@inquirer/expand/yoctocolors-cjs": ["yoctocolors-cjs@2.1.2", "", {}, "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA=="],
"inquirer/@inquirer/prompts/@inquirer/rawlist/yoctocolors-cjs": ["yoctocolors-cjs@2.1.2", "", {}, "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA=="],
"inquirer/@inquirer/prompts/@inquirer/search/@inquirer/figures": ["@inquirer/figures@1.0.13", "", {}, "sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw=="],
"inquirer/@inquirer/prompts/@inquirer/search/yoctocolors-cjs": ["yoctocolors-cjs@2.1.2", "", {}, "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA=="],
"inquirer/@inquirer/prompts/@inquirer/select/@inquirer/figures": ["@inquirer/figures@1.0.13", "", {}, "sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw=="],
"inquirer/@inquirer/prompts/@inquirer/select/yoctocolors-cjs": ["yoctocolors-cjs@2.1.2", "", {}, "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA=="],
"widest-line/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], "widest-line/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"@mintlify/cli/@mintlify/common/@mintlify/mdx/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-3P8rGsg2Eh2qIHekwuQjzWhKI4jV97PhvYjYUzGqjvJfqdQPz+nMlfWahU24GZAyW1FxFI1sYjyhfh5CoLmIUA=="],
"@mintlify/cli/@mintlify/common/@mintlify/mdx/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.13.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw=="],
"@mintlify/cli/@mintlify/common/@mintlify/mdx/shiki/@shikijs/core": ["@shikijs/core@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-3P8rGsg2Eh2qIHekwuQjzWhKI4jV97PhvYjYUzGqjvJfqdQPz+nMlfWahU24GZAyW1FxFI1sYjyhfh5CoLmIUA=="],
"@mintlify/cli/@mintlify/common/@mintlify/mdx/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-Ty7xv32XCp8u0eQt8rItpMs6rU9Ki6LJ1dQOW3V/56PKDcpvfHPnYFbsx5FFUP2Yim34m/UkazidamMNVR4vKg=="],
"@mintlify/cli/@mintlify/common/@mintlify/mdx/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-O42rBGr4UDSlhT2ZFMxqM7QzIU+IcpoTMzb3W7AlziI1ZF7R8eS2M0yt5Ry35nnnTX/LTLXFPUjRFCIW+Operg=="],
"@mintlify/cli/@mintlify/common/@mintlify/mdx/shiki/@shikijs/langs": ["@shikijs/langs@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0" } }, "sha512-672c3WAETDYHwrRP0yLy3W1QYB89Hbpj+pO4KhxK6FzIrDI2FoEXNiNCut6BQmEApYLfuYfpgOZaqbY+E9b8wQ=="],
"@mintlify/cli/@mintlify/common/@mintlify/mdx/shiki/@shikijs/themes": ["@shikijs/themes@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0" } }, "sha512-Vxw1Nm1/Od8jyA7QuAenaV78BG2nSr3/gCGdBkLpfLscddCkzkL36Q5b67SrLLfvAJTOUzW39x4FHVCFriPVgg=="],
"@mintlify/cli/@mintlify/common/@mintlify/mdx/shiki/@shikijs/types": ["@shikijs/types@3.13.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw=="],
"@mintlify/link-rot/@mintlify/common/@mintlify/mdx/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-3P8rGsg2Eh2qIHekwuQjzWhKI4jV97PhvYjYUzGqjvJfqdQPz+nMlfWahU24GZAyW1FxFI1sYjyhfh5CoLmIUA=="],
"@mintlify/link-rot/@mintlify/common/@mintlify/mdx/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.13.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw=="],
"@mintlify/link-rot/@mintlify/common/@mintlify/mdx/shiki/@shikijs/core": ["@shikijs/core@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-3P8rGsg2Eh2qIHekwuQjzWhKI4jV97PhvYjYUzGqjvJfqdQPz+nMlfWahU24GZAyW1FxFI1sYjyhfh5CoLmIUA=="],
"@mintlify/link-rot/@mintlify/common/@mintlify/mdx/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-Ty7xv32XCp8u0eQt8rItpMs6rU9Ki6LJ1dQOW3V/56PKDcpvfHPnYFbsx5FFUP2Yim34m/UkazidamMNVR4vKg=="],
"@mintlify/link-rot/@mintlify/common/@mintlify/mdx/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-O42rBGr4UDSlhT2ZFMxqM7QzIU+IcpoTMzb3W7AlziI1ZF7R8eS2M0yt5Ry35nnnTX/LTLXFPUjRFCIW+Operg=="],
"@mintlify/link-rot/@mintlify/common/@mintlify/mdx/shiki/@shikijs/langs": ["@shikijs/langs@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0" } }, "sha512-672c3WAETDYHwrRP0yLy3W1QYB89Hbpj+pO4KhxK6FzIrDI2FoEXNiNCut6BQmEApYLfuYfpgOZaqbY+E9b8wQ=="],
"@mintlify/link-rot/@mintlify/common/@mintlify/mdx/shiki/@shikijs/themes": ["@shikijs/themes@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0" } }, "sha512-Vxw1Nm1/Od8jyA7QuAenaV78BG2nSr3/gCGdBkLpfLscddCkzkL36Q5b67SrLLfvAJTOUzW39x4FHVCFriPVgg=="],
"@mintlify/link-rot/@mintlify/common/@mintlify/mdx/shiki/@shikijs/types": ["@shikijs/types@3.13.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw=="],
"@mintlify/prebuild/@mintlify/common/@mintlify/mdx/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-3P8rGsg2Eh2qIHekwuQjzWhKI4jV97PhvYjYUzGqjvJfqdQPz+nMlfWahU24GZAyW1FxFI1sYjyhfh5CoLmIUA=="],
"@mintlify/prebuild/@mintlify/common/@mintlify/mdx/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.13.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw=="],
"@mintlify/prebuild/@mintlify/common/@mintlify/mdx/shiki/@shikijs/core": ["@shikijs/core@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-3P8rGsg2Eh2qIHekwuQjzWhKI4jV97PhvYjYUzGqjvJfqdQPz+nMlfWahU24GZAyW1FxFI1sYjyhfh5CoLmIUA=="],
"@mintlify/prebuild/@mintlify/common/@mintlify/mdx/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-Ty7xv32XCp8u0eQt8rItpMs6rU9Ki6LJ1dQOW3V/56PKDcpvfHPnYFbsx5FFUP2Yim34m/UkazidamMNVR4vKg=="],
"@mintlify/prebuild/@mintlify/common/@mintlify/mdx/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-O42rBGr4UDSlhT2ZFMxqM7QzIU+IcpoTMzb3W7AlziI1ZF7R8eS2M0yt5Ry35nnnTX/LTLXFPUjRFCIW+Operg=="],
"@mintlify/prebuild/@mintlify/common/@mintlify/mdx/shiki/@shikijs/langs": ["@shikijs/langs@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0" } }, "sha512-672c3WAETDYHwrRP0yLy3W1QYB89Hbpj+pO4KhxK6FzIrDI2FoEXNiNCut6BQmEApYLfuYfpgOZaqbY+E9b8wQ=="],
"@mintlify/prebuild/@mintlify/common/@mintlify/mdx/shiki/@shikijs/themes": ["@shikijs/themes@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0" } }, "sha512-Vxw1Nm1/Od8jyA7QuAenaV78BG2nSr3/gCGdBkLpfLscddCkzkL36Q5b67SrLLfvAJTOUzW39x4FHVCFriPVgg=="],
"@mintlify/prebuild/@mintlify/common/@mintlify/mdx/shiki/@shikijs/types": ["@shikijs/types@3.13.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw=="],
"@mintlify/previewing/@mintlify/common/@mintlify/mdx/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-3P8rGsg2Eh2qIHekwuQjzWhKI4jV97PhvYjYUzGqjvJfqdQPz+nMlfWahU24GZAyW1FxFI1sYjyhfh5CoLmIUA=="],
"@mintlify/previewing/@mintlify/common/@mintlify/mdx/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.13.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw=="],
"@mintlify/previewing/@mintlify/common/@mintlify/mdx/shiki/@shikijs/core": ["@shikijs/core@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-3P8rGsg2Eh2qIHekwuQjzWhKI4jV97PhvYjYUzGqjvJfqdQPz+nMlfWahU24GZAyW1FxFI1sYjyhfh5CoLmIUA=="],
"@mintlify/previewing/@mintlify/common/@mintlify/mdx/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-Ty7xv32XCp8u0eQt8rItpMs6rU9Ki6LJ1dQOW3V/56PKDcpvfHPnYFbsx5FFUP2Yim34m/UkazidamMNVR4vKg=="],
"@mintlify/previewing/@mintlify/common/@mintlify/mdx/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-O42rBGr4UDSlhT2ZFMxqM7QzIU+IcpoTMzb3W7AlziI1ZF7R8eS2M0yt5Ry35nnnTX/LTLXFPUjRFCIW+Operg=="],
"@mintlify/previewing/@mintlify/common/@mintlify/mdx/shiki/@shikijs/langs": ["@shikijs/langs@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0" } }, "sha512-672c3WAETDYHwrRP0yLy3W1QYB89Hbpj+pO4KhxK6FzIrDI2FoEXNiNCut6BQmEApYLfuYfpgOZaqbY+E9b8wQ=="],
"@mintlify/previewing/@mintlify/common/@mintlify/mdx/shiki/@shikijs/themes": ["@shikijs/themes@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0" } }, "sha512-Vxw1Nm1/Od8jyA7QuAenaV78BG2nSr3/gCGdBkLpfLscddCkzkL36Q5b67SrLLfvAJTOUzW39x4FHVCFriPVgg=="],
"@mintlify/previewing/@mintlify/common/@mintlify/mdx/shiki/@shikijs/types": ["@shikijs/types@3.13.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw=="],
} }
} }

View File

@ -4,38 +4,51 @@ description: "Compatibility guide for Honcho's SDKs and API"
icon: "shield-check" icon: "shield-check"
--- ---
This guide helps you understand which versions of Honcho's API are compatible with which SDK versions. This guide helps you match the right SDK version to your Honcho API version. Newer SDK patch versions are always backward-compatible within the same major version — install the latest patch for your range.
## Version Compatibility ## Current Versions
### Honcho API v2.4.2 (Current)
<CardGroup cols={2}> <CardGroup cols={2}>
<Card title="TypeScript SDK" icon="js"> <Card title="TypeScript SDK" icon="js">
**Compatible Version:** v1.5.0 **Latest:** v2.4.0
Install with:
```bash ```bash
npm install @honcho-ai/sdk@1.5.0 npm install @honcho-ai/sdk
``` ```
</Card> </Card>
<Card title="Python SDK" icon="python"> <Card title="Python SDK" icon="python">
**Compatible Version:** v1.5.0 **Latest:** v2.4.0
Install with:
```bash ```bash
pip install honcho-ai==1.5.0 pip install honcho-ai
``` ```
</Card> </Card>
</CardGroup> </CardGroup>
## Version Compatibility Table ## Version Compatibility Table
| Honcho API Version | TypeScript SDK | Python SDK | | Honcho API Version | TypeScript SDK | Python SDK |
|-------------------|---------------|------------| |-------------------|---------------|------------|
| v2.4.2 (Current) | v1.5.0 | v1.5.0 | | v3.1.1 (Current) | v2.4.0 | v2.4.0 |
| v3.1.0 | v2.4.0 | v2.4.0 |
| v3.0.12 | v2.3.0 | v2.3.0 |
| v3.0.11 | 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 |
| v3.0.5 | v2.1.0 | v2.1.0 |
| v3.0.4 | v2.1.0 | v2.1.0 |
| v3.0.3 | v2.1.0 | v2.1.0 |
| v3.0.2 | v2.0.0+ | v2.0.0+ |
| v3.0.1 | v2.0.0+ | v2.0.0+ |
| v3.0.0 | v2.0.0+ | v2.0.0+ |
| v2.5.1 | v1.6.0 | v1.6.0 |
| v2.5.0 | v1.6.0 | v1.6.0 |
| v2.4.3 | v1.5.0 | v1.5.0 |
| v2.4.2 | v1.5.0 | v1.5.0 |
| v2.4.1 | v1.5.0 | v1.5.0 | | v2.4.1 | v1.5.0 | v1.5.0 |
| v2.4.0 | v1.5.0 | v1.5.0 | | v2.4.0 | v1.5.0 | v1.5.0 |
| v2.3.3 | v1.4.1 | v1.4.1 | | v2.3.3 | v1.4.1 | v1.4.1 |

View File

@ -27,7 +27,442 @@ Welcome to the Honcho changelog! This section documents all notable changes to t
### Honcho API and SDK Changelogs ### Honcho API and SDK Changelogs
<Tabs> <Tabs>
<Tab title="Honcho API"> <Tab title="Honcho API">
<Update label="v2.4.2 (Current)"> <Update label="v3.1.1 (Current)">
### Changed
- Server `requires-python` is `>=3.13`, matching the production image. Self-hosters on 3.10–3.12 need to upgrade; SDK and CLI floors are unchanged (#1090)
### Fixed
- Concurrent `create_documents` writers to the same collection deadlocked on `times_derived` reinforcement UPDATEs issued in batch order; the error was swallowed per-document, the batch was lost, and the queue item was marked processed. Writers now lock target rows with `SELECT ... ORDER BY id FOR UPDATE` before applying, abort the batch on `SQLAlchemyError` instead of continuing through a dead session, and retry transient errors (deadlock, serialization failure, lock/statement timeout, lost connection) up to `MAX_RETRYABLE_ATTEMPTS` instead of burning the item (#1033)
- Scope backfill no longer embeds, writes, and syncs every planned copy at once. A 14k-document session is ~580MB of vectors; several concurrent backfills OOM-killed the deriver at its 1000Mi limit and crash-looped because the work units never completed. Phases 2–4 now run per chunk of 500 specs, reload source embeddings per chunk, and drop them once synced. Membership is locked across chunk writes so a concurrent leave cannot commit between the check and the inserts (#1104)
- Model-generated observations with NUL bytes (`\u0000`) no longer fail the exact-content dedup pre-fetch with a Postgres `DataError` that dropped the whole observer batch. Ingress already stripped NUL from user content; the deriver now strips it so stored text matches embedded text. All-NUL content is dropped rather than stored empty (#1095)
- `search_messages` no longer forwards `top_k=0` to Turbopuffer (which requires 1..10000). Zero/negative limits short-circuit to empty results; tool limits are floored at 1. The documents path was already guarded (#970); this closes the message path (#1084)
- OpenAI-compatible tool-call turns with `content=null` keep null through history replay instead of being coerced to `""`. Providers that bind reasoning state to the exact assistant message shape were breaking on the empty string. Tool-less null still becomes `""` (#1064)
- The production image now ships `pyproject.toml` in the runtime stage, so the service reports its real version instead of `unknown` in OpenAPI and telemetry (#1074)
</Update>
<Update label="v3.1.0">
### Added
- Scopes: a named grouping of sessions that acts as a visibility boundary on recall, implemented as a facade over an observer peer (`scope.{name}` with `{"kind": "scope"}`). Developers manage them exclusively through `/v3/workspaces/{workspace_id}/scopes` (create-or-get, list, get, add/list/remove session membership) and an optional `scopes` field on session create — never through the observer/observed mechanics. Scope peers cannot author messages, cannot be a chat or representation `target`, are excluded from `peers.list` by default (`PeerGet.kind` = `"scope"` / `"all"` switches the view), and are rejected on the generic session-peer routes. Workspace-level key required; peer- and session-scoped keys get 401. Legacy peers occupying a reserved `scope.` name without the kind flag are refused with 409, never adopted (#884)
- `scope` read option on chat, representation, session context, and workspace search. A single scope swaps the observer to the backing scope peer so conclusion recall, peer cards, and message tools stay inside that scope's membership. A list of scopes takes the union of member sessions (capped at `MAX_SESSION_ALLOWLIST_ENTRIES`) and executes via the session-allowlist path. Empty scopes fail closed. `scope` is mutually exclusive with `filters` and `session_id`. Workspace- or admin-level key required (403 otherwise). Scope peers are also rejected as `peer_target` / `peer_perspective` on session context and as the path peer or `target` on `GET /peers/{id}/context` (#897)
- Scope backfill-by-copy and removal reconciliation. Adding a session that already has messages copies its explicit-level documents into the scope's collections (no LLM re-derivation; idempotent via `copied_from`). Removing a session soft-deletes those copies and fail-closed cascades to derived documents whose `source_ids` intersect anything removed, then enqueues a `card_refresh` dream with `rebuild=True` plus an omni dream. `GET /v3/workspaces/{workspace_id}/scopes/{scope_id}/status` reports per-session backfill state (`pending` / `completed` / `failed`, plus `docs_copied`) (#904)
- Workspace-level chat at `POST /v3/workspaces/{workspace_id}/chat`: agentic dialectic over the whole workspace instead of a single (observer, observed) pair. Prefetches workspace stats and the top active peers' self cards, then searches pair-scoped memory with `[observer->observed]` attribution. Supports `session_id`, `scope`, `reasoning_level`, `response_format`, and SSE streaming (#931)
- MCP workspace discovery: tools accept `workspace_id`, the worker honors an optional `X-Honcho-Workspace-ID` connection header, and `list_workspace` / `create_workspace` tools let clients pick or create a workspace instead of relying on the SDK default (#1020)
- MCP `search` also queries conclusions in parallel with messages when `peer_id` is given, returning `{messages, conclusions}`. The conclusions leg degrades to `[]` on error so search never gets worse than before (#974)
- Prometheus metrics for physical DB connections, visible even under `DB_POOL_CLASS=null`: `db_connections_open` (gauge) and `db_connections_established` (counter), hooked to SQLAlchemy connection-lifecycle events and registered on both the API and the deriver (#1055)
- Bounded-label Prometheus series are zero-initialized at process start so an absent series means a broken scrape rather than "nothing happened" (#927)
### Changed
- Workspace and pair chat system prompts now describe Honcho, peers, and the harness on their own terms, and render only the tools the request actually offers. The pair prompt no longer advertises a write tool that is not in the loadout (#1066)
- Deriver idle polling backoff is longer and no longer reset by periodic reconciler work, so downstream connection pools can cull idle DB connections (#1015)
- LLM provider SDKs are lazy-loaded so idle API and deriver processes no longer pay for every provider at import time (#1011)
- Production image is a multi-stage build: LanceDB/PyArrow move behind an optional `lancedb` extra (`INSTALL_LANCEDB=true` to restore them), FastAPI's unused cloud CLI is dropped, and the venv is copied into the runtime image with final ownership so Docker does not double the layer. Default unpacked image is about 663 MB (was 1.7 GB) (#1014)
- Redis Cluster cache keys hash-tag the namespace so one deployment's keys land on a single shard instead of opening a connection to every node. No behaviour change on a non-cluster backend; existing keys age out by TTL (#1058)
- Deriver extraction prompt no longer leaks its own few-shot examples into extracted conclusions (#1028)
### Fixed
- Observer-scoped `get_observation_context` no longer materializes every session the observer has ever joined into a `session_name IN (...)` list (twice in one statement). Past ~32k sessions that hit psycopg's bind-parameter ceiling and 500'd. The observer half is now a correlated `EXISTS` over `session_peers`, two bind parameters regardless of membership size (#1065)
- Re-adding an already-active session peer no longer advances `joined_at`, so `peer_perspective` search keeps messages from the original join. Genuine leave-and-rejoin still starts a new window (#1059)
- Transient embedding-provider errors (for example an OpenAI-compatible 200 with empty `data: []`) were relabeled as token-limit errors. Only genuine oversize input raises `EmbeddingTokenLimitError`; other provider errors propagate unchanged (#791)
- The filter DSL now fails closed with a 422 instead of a 500 on bad shapes, coerces operands by column type (so `{"session_id": {"ne": "abc"}}` is a string inequality rather than "invalid numeric"), and treats `NOT` / `ne` as null-safe (`IS NOT TRUE` / `IS DISTINCT FROM`) so negation no longer drops rows whose field is unset. Closed-set columns like `level` reject unknown values. Session-allowlist entries must be well-formed ids (`*` is 422, not a silent widen) (#947)
- `ne` on JSONB metadata keys is null-safe: a missing key is not equal to the compared value, so `{"metadata": {"foo": {"ne": "bar"}}}` includes rows where `foo` is unset (#1036)
- Oversized texts in `simple_batch_embed` are truncated to the embedding token cap instead of failing the whole batch. Representation processing reports failed observer saves in `RepresentationCompletedEvent` and raises when every observer save fails (#1019)
- Assistant `reasoning_content` (DeepSeek / some OpenRouter models) is preserved across tool-loop turns. Previously the tool loop dropped thinking content before building the next assistant history message, so continuation requests failed. `reasoning_details` still takes precedence when both are present (#1034)
- `create_observations` now honors `DERIVER_DEDUPLICATE` instead of hardcoding `deduplicate=True`, matching the representation write path (#1018)
- `provider_params.timeout` is forwarded to the OpenAI-compatible embedding client, not just the LLM client (#1024)
- Conclusions semantic-search validation errors name the field and the constraint instead of returning a generic 422 (#960)
- OpenAI-compatible embedding calls request `encoding_format=float` so providers that default to base64 do not break pgvector inserts (#938)
- Gemini batch embedding works for `gemini-embedding-2*` models, which rejected the previous request shape (#745)
- MCP OAuth with no advertised scopes no longer defaults to read-only (which 403'd chat and search POSTs). Protected-resource metadata advertises read and write (#1004)
</Update>
<Update label="v3.0.12">
### Added
- Session allowlist on the Dialectic and representation via a constrained `filters` body on `POST /peers/{peer_id}/chat` and `/representation`, supporting only the `session_id` key (a session id, a bare list, or `{"in": [...]}`). Unsupported keys and shapes are rejected with 422 rather than silently ignored, it composes with `session_id` (which must be included in the allowlist when both are given), and it is capped at 1,000 sessions per request. Enforcement is uniform and fail-closed at every recall chokepoint: scoped conclusion recall is restricted to `level == "explicit"` (dream-derived conclusions carry a single `session_name` but are synthesized across all sessions, so that stamp can't be scoped on), `get_reasoning_chain` is unavailable under an allowlist, and an empty allowlist short-circuits to empty results everywhere. Workspace keys pass the allowlist as-given; peer-scoped JWTs must be an active member of every allowlisted session (401 otherwise) (#882)
- Bare-list membership sugar in the filter DSL: `{"session_id": ["s1", "s2"]}` is now shorthand for `{"session_id": {"in": [...]}}` on regular columns generically. JSONB metadata columns are excluded and keep containment semantics. Strictly additive, since a bare list on a regular column previously compiled to a type-mismatched equality that matched nothing (#881)
- Optional structured outputs on the Dialectic: `response_format` (a JSON Schema with root type `object`) on peer chat makes `content` a JSON string conforming to that schema. Only a conservative subset of JSON Schema is supported, with DoS guards and non-recursive `$ref` support (#896)
- Combined tool calling and structured output in the LLM transport layer, with per-backend request shaping: OpenAI routes tool-carrying structured requests through `create()` with an explicit `json_schema` response format (`parse()` 500s on non-strict function tools), Anthropic skips the `{` JSON prefill when tools are present so `tool_use` blocks stay reachable, and Gemini injects a schema instruction into the final turn instead of using native `response_schema` (rejected alongside function calling before Gemini 3). All backends skip structured-output parsing on tool-call turns, which carry no consumable content (#907)
- `card_refresh` dream type: a lightweight dream that runs only the peer-card update, for event-driven refreshes such as membership changes and cold starts. Handled by a new `CardRefreshSpecialist` restricted to `get_recent_observations`, `search_memory`, and `update_peer_card` (no observation-mutating tools) with a tool-iteration cap of `min(6, DREAM.MAX_TOOL_ITERATIONS)`. `POST /v3/workspaces/{workspace_id}/schedule_dream` accepts `dream_type=card_refresh` plus a `rebuild` flag, which omits the existing card from the prompt so the specialist rebuilds it solely from observations present in the collection. Card refreshes never advance the omni dream guard pair (`last_dream_at` / `last_dream_document_count`) (#883)
- Full-fidelity LLM trace stream, with Langfuse as one projection over it: each call is captured once (`CapturedLLMCall`) and fanned out to a CloudEvents trace stream (`llm.call.traced` / `trace.content`) and a Langfuse exporter, both reconstructing trace → run → step → generation from the same source of truth. Adds `TELEMETRY_TRACE_PAYLOADS_ENABLED` (default `false`), `TELEMETRY_TRACE_MAX_BYTES` (default 262144, per-message cap with oversized content clipped), `TELEMETRY_TRACE_PURPOSES` (JSON list of `CallPurpose` values; empty means all), and `LANGFUSE_EXPORTER_MODE` (`exporter` by default; `inline` is kept for one release for side-by-side validation). Embedding calls are traced, dreamer branches nest under one dream trace, tool calls become spans under their step, and high-volume events are sampled deterministically. `TRACE_ENDPOINT` is dropped (#845)
- Redis Cluster support via `CACHE_CLUSTER` (for example GCP Memorystore for Redis Cluster), alongside a new `CACHE_LOCK_WAIT_CHECK_INTERVAL_SECONDS` (#905)
- `EMBEDDING_MODEL_CONFIG__MAX_BATCH_SIZE` caps texts per embedding request for OpenAI-compatible providers with smaller limits than OpenAI's, such as DashScope `text-embedding-v4` (10) and Alibaba Bailian `qwen3.7-text-embedding` (20). When unset, native provider defaults are preserved (OpenAI 2048, Gemini 100) (#983)
- Per-request provider timeouts via `provider_params.timeout` on any model config, validated at config load so a bad value fails at startup with the exact config path instead of surfacing per-request as a retried 500. Good values normalize to float seconds; Gemini's is converted to milliseconds (#832)
- `RepresentationCompletedEvent` now reports deduplication counts: `exact_dup_in_batch_count`, `exact_dup_existing_count`, `semantic_dup_rejected_count`, and `semantic_dup_replaced_count` (#910)
- OAuth discovery for MCP clients: the MCP worker serves `/.well-known/oauth-protected-resource` (RFC 9728) without auth so clients can discover the authorization server, and a 401 now carries `WWW-Authenticate: Bearer resource_metadata="..."` (exposed cross-origin) to start the flow (#923)
- Prometheus metrics for the immediate-embed fast path: tasks shed because `EMBEDDING_MAX_PENDING_EMBED_TASKS` was reached, and the current in-flight task count (#892)
- Docs: a detailed system architecture diagram, a Codex integration guide (#879), a structured-outputs page (#896), a section on filtering conclusions by reasoning level (#851), a health-check endpoint reference, and SDK updates (#867)
### Changed
- **Breaking config change:** `DERIVER_REPRESENTATION_BATCH_MAX_TOKENS` is split into two settings that were previously conflated — `DERIVER_REPRESENTATION_BATCH_WORK_UNIT_TARGET_TOKENS` (default 512), the producer-side minimum a work unit accumulates before the deriver claims it, where `0` disables the gate; and `DERIVER_REPRESENTATION_BATCH_TARGET_INPUT_TOKENS` (default 1024), the consumer-side maximum context-window tokens per deriver LLM call. Deployments setting the old name must migrate (#889)
- The immediate-embed fast path now applies backpressure: `EMBEDDING_MAX_PENDING_EMBED_TASKS` (default 50) caps in-flight embed tasks, and once saturated, message creation skips the fast path entirely and the reconciler embeds on its next cycle. `0` disables the fast path (#892)
- Explicit-level documents are now kept session-pure, so memory can be built by copying explicit documents between collections. Enforcement refuses rather than rewrites: `create_documents` rejects explicit documents with a null `session_name`, exact dedup keys on (content, level, session-for-explicit), semantic dedup scopes candidate search to the same level and — for explicit documents — the same session, and the generic `create_observations` tool rejects `level='explicit'` outside message-ingestion (deriver) context. Derived levels keep cross-session consolidation (#883)
- Sentry's `before_send` filter is centralized as `default_before_send` in `src/telemetry/sentry.py` instead of living only in the API's `main.py`, so the deriver gets the same non-actionable-exception filtering. All Sentry events also carry a `namespace` tag for correlation (#934, #870)
- The minimal deriver's extraction examples no longer teach inferences its own output schema forbids. The `EXAMPLES` block demonstrated deriving a specific birthday from "I just had my 25th birthday last Saturday", deriving residence from a single visit ("I took my dog for a walk in NYC" → "alice lives in NYC"), and a "+ general knowledge" deductive output the deriver has no channel for. The replacements stay inside the schema's contract and teach the boundary: the dog/NYC message is kept and shown extracting correctly, and a separate example shows "lives in NYC" is valid when actually stated (#985)
- Dreamer specialists are instructed not to output summaries (#894)
- `session_name` is deprecated for scoping in favor of the session allowlist. It is not removed and not aliased: it also pins the query to one session, bypasses observer scoping, and drives session-history injection into the dialectic prompt, so it has no drop-in replacement (#882)
- The MCP worker no longer requires the `X-Honcho-User-Name` or `X-Honcho-Assistant-Name` headers (#923)
### Fixed
- Session scoping was applied to only one of the three working-representation query paths: `session_name` reached the recent-documents query, but the semantic and most-derived paths ignored it, so `limit_to_session` leaked cross-session conclusions into perspectives. The allowlist is now threaded uniformly through all three paths and pushed down to pgvector and external vector stores (#881)
- Empty membership lists failed open in the vector-store filter builders, silently widening scope: LanceDB dropped empty `IN` clauses and Turbopuffer emitted a bare `In []` with undocumented semantics. Both now emit an explicit always-false predicate, and `_build_filter_conditions` checks `is not None` rather than truthiness so an empty list is no longer treated like `None` (#881, #882)
- Session-scoped CRUD helpers ignored the session allowlist entirely, so a caller could read a session the allowlist forbids. The API routes guarded this with a 422, but the dialectic tools call these CRUD functions directly and bypassed it. `_semantic_search_messages` (covering `search_messages` and `search_messages_temporal`), `grep_messages`, `get_messages_by_date_range`, `get_recent_history`, and `get_observation_context` now return `[]` when `session_name` is set and outside the allowlist (#882)
- The cache client logged the full Redis URL — including the password — at INFO and WARNING on every connection attempt and failure, exposing the live credential in container logs and downstream aggregation. Credentials are now redacted across userinfo, the `?password=` (redis-py) and `?secret=` (cashews) query params, scheme-less URLs whose password is invisible to `.port`/`.password` parsing, and malformed URLs, whose fallback previously echoed the raw input verbatim (#869)
- A `top_k` of `0` reached the vector store, where Turbopuffer rejects it with a 400 (`top_k must be between 1 and 10000`). A non-positive `top_k` now returns `[]` before the embedding call, and the semantic budget floors at 1 so an explicitly requested search isn't silently allocated zero (#970)
- Gemini clients had no HTTP timeout, so a stalled socket wedged the deriver worker's uvloop event loop, which the in-process reconciler shares. A 10-minute timeout is now set on both the Gemini LLM client and the Gemini embedding client (#903)
- Dreamer conclusions were dated to ingestion time rather than their latest source observation, and their timestamps are now normalized (#890)
- Langfuse I/O annotation was gated on `LANGFUSE_PUBLIC_KEY` instead of `langfuse_inline_enabled`, so in the default `exporter` mode it called `update_current_generation()` with no active span — logging "No active span in current context" roughly 14 times per dialectic run and building throwaway `model_dump` payloads on every LLM call. Separately, `AgentToolSummaryCreatedEvent` hardcoded `run_id="deriver"` / `iteration=0`, polluting `run_id` grouping in the CloudEvents stream with a phantom run; both fields are now optional and the resource id is keyed on `message_id:summary_type` (schema_version 2 → 3) (#845)
- Assistant tool calls were dropped from the captured trace stream for OpenAI and Gemini: `build_captured_messages` read only `{role, content, tool_call_id}`, but those providers keep tool calls outside `content`, so replayed tool-call turns landed as empty content and Gemini lost its text and tool results entirely. Tool calls are now normalized per provider into a unified `tool_calls` field and folded into the content hash. Gemini's `thought_signature` is bytes, so `model_dump(mode="json")` raised `UnicodeDecodeError` inside `emit_trace`, silently dropping whole tool-calling iterations from the trace stream (billing and Langfuse were unaffected); it is now base64-encoded on the telemetry path while replay keeps the raw bytes (#845)
- `EmbeddingClient.encoding` forced full client construction, raising "OpenAI API key is required" even though tiktoken needs no credentials. The document dedup tie-break only needs `.encoding` for token counting, so any test hitting that path failed in environments without embedding keys — notably CI for pull requests from forks. The encoding is now resolved from the configured model directly, falling back to `cl100k_base`, and the underlying client's encoding is reused only when it has already been constructed (#955)
- The Docker build failed under Podman because the uv build inputs weren't copied (#878)
- LanceDB was installed on macOS Intel, where it doesn't work. A PEP 508 marker excludes `darwin/x86_64` and the LanceDB vector-store import is wrapped so a misconfiguration surfaces as a clear config error (#496)
- Prompt checks requiring the literal token "json" for `json_object` mode are now satisfied in lowercase (#887)
- Reverted an unintended `RepresentationCompletedEvent` schema-version increment
- Documented preinstalling pgvector as a privileged role for deployments where the `DB_CONNECTION_URI` role deliberately cannot create extensions (managed Postgres, Kubernetes operators, NixOS). `CREATE EXTENSION IF NOT EXISTS vector` does not help there, because Postgres checks the privilege before checking whether the extension exists. Docker Compose is unaffected, since the bundled stack connects as the `postgres` superuser (#984)
</Update>
<Update label="v3.0.11">
### 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)
- Conclusion responses now include a `level` field (`explicit`, `deductive`, `inductive`, `contradiction`); list/query endpoints support filtering by `level` via `filters`, with reserved filter keys protected from being overridden by user-supplied filters (#851)
### 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)
- Document creation now performs exact (case-insensitive, whitespace-trimmed) content deduplication before the existing semantic dedup step: exact duplicates within a batch collapse to a single insert, and an exact match against a live document reinforces it (atomic `times_derived` increment) instead of creating a new row (#861)
- The OpenAI backend passed `tool_choice` through raw while the Anthropic and Gemini backends translate Honcho's canonical vocabulary to their native form, so on a mixed-provider fallback chain (for example Gemini primary → OpenAI backup) a canonical `"any"` reached OpenAI unchanged and was rejected as an invalid param. The OpenAI backend now converts it, mirroring the others: `any`/`required` → `required`, `auto`/`none` pass through, and a tool-name string or `{"name": ...}` dict becomes a function selection (#850)
- Langfuse `@observe` auto-capture serialized every argument of `honcho_llm_call_inner` into the generation span input, including `client_override` (a live `AsyncOpenAI`/`genai` client) and `selected_config` (which carries `api_key`). Auto-capture deep-copied the client into a half-constructed object whose teardown raised (`AsyncHttpxClientWrapper ... no attribute '_state'` on OpenAI, flooding stderr; `BaseApiClient ... no attribute '_http_options'` on Gemini), and it leaked `ModelConfig.api_key` into traces. Capture is now an explicit allowlist: `capture_input`/`capture_output` are disabled and curated, serializable input and output are stamped instead, with tuning knobs surfaced as `model_parameters` via a secret-bearing denylist and per-call token usage mirrored as `usage_details` (#849)
</Update>
<Update label="v3.0.10">
### Added
- Messages are now embedded via a background task rather than blocking API request
- Read-only DB session mode (`get_read_db` / `tracked_db(..., read_only=True)`) so reads don't hold a transaction open across the work
- `CORS_ORIGINS` env var to configure CORS allowed origins without editing source; defaults match the prior hardcoded list, so self-hosted deployments behind custom domains can whitelist their frontend (#697)
- `scripts/generate_jwt.py` — utility for minting scoped or admin Honcho JWTs (`--admin`, `--workspace`/`--peer`/`--session`, `--expires` with human-friendly durations, `--print-only`) without calling the keys API (#757)
- `STALE_WORK_UNIT_CLEANUP_INTERVAL_SECONDS` (default 60s) — minimum jittered spacing between deriver stale-work-unit cleanup runs, so cleanup no longer runs on every seconds-scale poll (`0.0` keeps the legacy every-poll behavior) (#773)
### Changed
- Optimized the deriver and dreamer prompt cache prefixes to improve prompt-cache hit rates (#806)
### Fixed
- `times_derived` is now properly reinforced when a duplicate conclusion is detected. It had been pinned at 1 for nearly every conclusion (the reject-new branch dropped the increment and the new-wins branch reset the count to 1), so `ORDER BY times_derived DESC` fell back to arbitrary heap order and froze stale conclusions to the front of injected context. Reinforcement is now an atomic increment and both most-derived queries gained a `created_at DESC` recency tiebreaker (#768)
- Webhook creation now correctly rejects private/internal IP addresses (#793)
</Update>
<Update label="v3.0.9">
### Changed
- Connection acquisition is now a single attempt with no server-side retry, on a vanilla `AsyncSession`. A new `DB_CONNECT_TIMEOUT_SECONDS` (default 2s) bounds the attempt so a saturated or unreachable pooler fails fast instead of holding a client connection open to re-knock. A saturated DB now surfaces to the caller — the API returns an error and the deriver backs off and retries on a later poll — which lets the pooler drain rather than amplifying saturation.
### Added
- Deriver poll jitter so instances that start together don't poll in lockstep: `DERIVER_POLLING_STARTUP_JITTER_SECONDS` (random delay before the first poll, default 30s) and `DERIVER_POLLING_JITTER_RATIO` (±fraction applied to every poll sleep, default 0.5). Both disable at `0.0`; the underlying backoff schedule is unchanged.
### Removed
- Reverted the connection-checkout retry and `HonchoAsyncSession` custom session introduced in 3.0.8. Removed the `DB_CONNECTION_RETRY_ENABLED` / `DB_CONNECTION_RETRY_MAX_DELAY_SECONDS` / `DB_CONNECTION_RETRY_BACKOFF_INITIAL_SECONDS` / `DB_CONNECTION_RETRY_BACKOFF_MAX_SECONDS` settings, the `db_connection_acquisitions{outcome=...}` Prometheus counter, and the `db.pool.acquire` Sentry span. Alerting built on `db_connection_acquisitions` should migrate to `db_pool_connections` / `db_queries_in_flight`.
</Update>
<Update label="v3.0.8">
### Added
- Connection-checkout retry with bounded exponential backoff (tenacity) on `get_db`/`tracked_db`: transient transaction-pooler (Supavisor) rejections — SQLAlchemy `TimeoutError` and `OperationalError` — now retry with backoff instead of surfacing as 500s under client-connection saturation. Gated by
`DB_CONNECTION_RETRY_ENABLED` with configurable delay/backoff knobs; ~10s default budget (#758)
- `HonchoAsyncSession` — a lazy `AsyncSession` that checks out its pooled connection (with retry) on the first DB-touching call rather than at construction. Request handlers doing non-DB work (embedding, file, LLM) before their first query no longer pin a pooler connection across it. Only the checkout is retried;
the statement still runs exactly once, so writes are never duplicated (#758)
- Adaptive deriver queue polling: the poll interval backs off when the queue is idle or erroring (base → max, doubling each cycle) and snaps back to base the moment work is claimed, cutting steady-state query load against the DB. Gated by `DERIVER_POLLING_BACKOFF_ENABLED` with configurable max/multiplier (#758)
- New Prometheus `db_pool_connections` gauge (checked_out / checked_in / size / overflow), labeled `api`|`deriver`, registered in both the API lifespan and the deriver metrics server (#758)
- New Prometheus `db_connection_acquisitions{outcome=ok|retried|exhausted}` counter — the alertable early-warning signal that connection checkouts are retrying through pooler rejection, before requests start failing (#758)
- New Prometheus `db_queries_in_flight` gauge — statements actually executing on the wire (via SQLAlchemy cursor-execute events). Paired with `checked_out`, the gap reveals connections held but parked (the "idle in transaction during an external call" antipattern). Gated on `METRICS.ENABLED` for zero overhead when
off (#758)
- Explicit `SqlalchemyIntegration` in both the API and deriver Sentry inits; connection acquisition wrapped in a `db.pool.acquire` span with live pool stats captured on retry exhaustion (#758)
### Changed
- Default `POOL_TIMEOUT` lowered to 5s, with validation that it stays under the connection-retry budget when a pooled (non-null) `POOL_CLASS` is configured; `config.toml.example` and the v2/v3 configuration docs updated to match (#758)
- `HonchoAsyncSession` wraps every DB-touching session method (execute / scalar / scalars / flush / merge / refresh / commit / get / get_one / stream / stream_scalars / delete) so the lazy-checkout-with-retry guarantee has no holes; the acquired flag resets on `close()`/`reset()` so a reused session re-acquires on
next use (#758)
### Fixed
- Roll the session back on a retryable checkout failure before retrying — a failed autobegin could otherwise leave it pending-rollback, making the next connection attempt raise instead of cleanly re-checking-out (#758)
- Guard `DBPoolCollector.collect()` so a pool-read/import hiccup can't raise and abort the entire `/metrics` scrape (Prometheus drops all metrics if any collector raises) (#758)
- Clamp the pool overflow gauge to ≥ 0 (it could report negative before the pool fills) (#758)
- Removed a double-sleep in the deriver idle poll so the backoff cap is a true cap rather than 2× (#758)
</Update>
<Update label="v3.0.7">
### Added
- New `src/llm/` package as the single owner of provider runtime: clients, backends, history adapters, tool loop, request builder, credentials, and caching policy (#459)
- New cloudevent `LLMCallCompletedEvent` (`llm.call.completed`) fires once per provider hit with full cost-attribution context: transport/provider_label, model, token counts with cache breakdown, finish_reason, outcome, retry/fallback state, duration, tool-call shape, streaming flag, and agent correlation (`run_id` + iteration) (#637)
- `RepresentationCompletedEvent` now carries `total_input_tokens` for full-trace cost attribution; per-emitter `honcho_version` injection; deterministic per-`run_id` high-volume sampler via `TelemetrySettings.HIGH_VOLUME_SAMPLE_RATE` (#637)
- Deriver custom instructions: per-workspace/peer guidance threaded into the deriver prompt with a `MAX_CUSTOM_INSTRUCTIONS_TOKENS` budget (default 2000); deriver `MAX_INPUT_TOKENS` raised 23000 → 25000 (#609)
- Configurable embedding dimensions: `EMBEDDING_MODEL_CONFIG__DIMENSIONS_MODE` (`auto`/`always`/`never`) controls whether OpenAI `dimensions=` is forwarded (#678)
- New `honcho-cli` package — Python CLI for inspecting and managing peers, sessions, and configuration against a Honcho deployment (#424)
- `HONCHO_API_URL` env var support in the MCP Worker for self-hosted deployments (#575)
- API ID `max_length` increased from 100 to 512 across `WorkspaceCreate`, `PeerCreate`, and `SessionCreate` to align with the DB schema (#684)
- `AttemptPlan` dataclass pins per-retry provider selection across stream-final retries so streaming doesn't bounce back to primary after the tool loop has settled on fallback (#459)
- Gemini JSON-schema sanitizer for `function_declarations` — strips keywords Gemini's validator rejects while preserving semantics for other backends (#459)
### Changed
- All LLM orchestration moved out of `src/utils/clients.py` into `src/llm/` with modules split by responsibility (#459)
- Default `ModelConfig` factories (deriver, summary, dreamer specialists, dialectic levels) normalized with no extra parameters set by default; operators add transport/thinking overrides explicitly (#459)
- OpenAI reasoning-model routing widened to cover `gpt-5.x` and `o1/o3/o4` — these models receive `max_completion_tokens` instead of `max_tokens` (#459)
- Peer card prompts reframed as stable identity markers; induction specialist now opts out of peer card writes so only deduction touches the card (#686)
- Vector store queries no longer fetch embedding vectors — only document metadata is returned, reducing payload size and DB load (pgvector, lancedb, turbopuffer) (#682)
- Langfuse trace metadata now includes `namespace`, `model`, and `provider` so traces can be filtered by deployment slice (#565)
- Deriver: model-aware tokenizer (replaces the previously hardcoded encoding) and explicit guard on empty message content (#647)
- Dialectic level defaults now merge correctly with per-level overrides (#656)
- Default dialectic tool choice switched to `auto` (#630)
- Vector sync given a substantial retry budget to tolerate transient embedding provider outages (#604)
- `AgentToolConclusionsDeletedEvent` payload now carries `levels` (#612)
- Turbopuffer: `InternalServerError` caught and surfaced as a warning rather than a hard failure; vector store sync errors downgraded to warnings (#561)
### Fixed
- `reverse` query parameter is now honored on the v3 workspace list, peer list, workspace-scoped session list, and peer-scoped session list. Honcho SDKs at 2.1.0+ were already sending `reverse=true` for these routes but the server silently ignored it. Ties on `created_at` now fall back to the internal nanoid `id` for stable ordering across pages (#685)
- LLM client factories now receive `base_url` from `LLMSettings` for default providers — operators pointing at OpenAI-compatible proxies via `LLM__OPENAI_BASE_URL` were previously ignored on the default path (#643, fixes #641)
- Internal N+1 query in dialectic agent tool execution — collapsed per-iteration DB lookups into a single fetch (#652)
- Dreamer threshold and time-guard semantics: count filter now includes only `documents.level == 'explicit'` (was inflating threshold via dreamer-created levels and creating a feedback loop); `last_dream_at` write relocated from enqueue to process so duplicate enqueues or failed runs no longer reset the 8-hour time guard (#573)
- Deriver: blank observations are filtered out before embedding (previously triggered noisy embedding calls and persisted empty rows) (#615)
- Surprisal module: filter format corrected from `{"level": levels}` to `{"level": {"in": levels}}` — the prior call silently returned 0 results and made the entire Surprisal phase of the Dream cycle a no-op (#581, fixes #559)
- Removed hardcoded `stop_sequences` override from Deriver `ModelConfig` (was clobbering operator-configured stop sequences) (#587)
- Embedding client: `embed()` now wraps single-string input in an array, restoring compatibility with OpenAI-compatible third-party providers that reject scalar input (#586)
- Docker Compose: deriver service startup gated on the API service healthcheck — prevents races where the deriver starts before the API has run migrations (#689)
- Docker image: `HEALTHCHECK` directive removed from the shared base image; service-level health checks now belong in each service's own configuration (#530)
- Removed strict parameter validation for thinking params on Anthropic and OpenAI transports — was rejecting valid per-transport configs (#686)
- Stream-final retries pin to the `AttemptPlan` that succeeded rather than re-running provider selection through the outer `current_attempt` ContextVar (#459)
- Gemini `cached_content` reuse keys now include `system_instruction` and `tool_config` so cache hits don't cross configurations (#459)
- CrewAI example updated for the latest CrewAI protocol (#631)
### Removed
- `src/utils/clients.py` deleted; its responsibilities are split across `src/llm/registry.py`, `src/llm/credentials.py`, and the backend-specific modules (#459)
- `HEALTHCHECK` directive from the shared Docker image (#530)
</Update>
<Update label="v3.0.6">
### Changed
- Tightened transaction scopes across search, agent tools, queue manager, and webhook delivery to minimize DB connection hold time during external operations (#525)
- Search operations refactored to two-phase pattern — external work (embeddings, LLM calls) completes before opening a transaction (#525)
- Agent tool executor performs external operations before acquiring DB sessions (#525)
- Queue manager transaction scope reduced to only the critical section (#525)
- Webhook delivery no longer holds a DB session parameter (#525)
### Fixed
- Session leakage in non-session-scoped dialectic chat calls (#526)
### Added
- Health check endpoint (`/health`) for container orchestration and load balancer probes (#510)
</Update>
<Update label="v3.0.5">
### Fixed
- explicit rollback on all transactions to force connection closed
</Update>
<Update label="v3.0.4">
### Added
- JSONB metadata validation enforces 100 key limit and max depth of 5 (#419)
### Changed
- Schemas refactored from single `schemas.py` into `schemas/api.py`, `schemas/configuration.py`, and `schemas/internal.py` with backwards-compatible re-exports (#419)
### Fixed
- Missing `deleted_at` filter on `RepresentationManager._query_documents_recent()` and `._query_documents_most_derived()` allowed soft-deleted documents to leak into the deriver's working representation (#456)
- `CleanupStaleItemsCompletedEvent` emitted spuriously when no queue item was actually deleted (#454)
- Empty JSON file uploads caused unhandled errors; now returns normalized error responses (#434)
- Memory leak: `_observation_locks` switched to `WeakValueDictionary` to prevent unbounded growth (#419)
- SQL injection in `dependencies.py`: parameterized `set_config` calls to prevent injection via request context (#419)
- NUL byte crashes: string inputs (message content, queries, peer cards) now stripped at schema level (#419)
- Filter recursion depth capped at 5 to prevent stack overflow (#419)
- Dedup-skipped observations now correctly reflected in created counts (#477)
- External vector store support for message search — routes queries through configured external vector store with oversampling and
deduplication to handle chunked embeddings (#479)
- Dialectic agent no longer holds a DB connection during LLM calls — embeddings are pre-computed before tool execution, DB sessions isolated in `extract_preferences`, `query_documents` no longer accepts a DB session parameter (#477)
</Update>
<Update label="v3.0.3">
### Added
- Consolidated session context into a single DB session with 40/60 token budget allocation between summary and messages
- Observation validation via `ObservationInput` Pydantic schema with partial-success support and batch embedding with per-observation fallback
- Peer card hard cap of 40 facts with case-insensitive deduplication and whitespace normalization
- Safe integer coercion (`_safe_int`) for all LLM tool inputs to handle non-integer values like `"Infinity"`
- Embedding pre-computation and reuse across multiple search calls in dialectic and representation flows
- Peer existence validation in dialectic chat endpoints — raises ResourceNotFoundException instead of silently failing
- Logging filter to suppress noisy `GET /metrics` access logs
- Oolong long-context aggregation benchmark (synth and real variants, 1K–4M token context windows)
- MolecularBench fact quality evaluation (ambiguity, decontextuality, minimality scoring)
- CoverageBench information recall evaluation (gold fact extraction, coverage matching, QA verification)
- LoCoMo summary-as-context baseline evaluation
- Webhook delivery tests, dependency lifecycle tests, queue cleanup tests, summarizer fallback tests
- Parallel test execution via pytest-xdist with worker-specific databases
- `test_reasoning_levels.py` script for LOCOM dataset testing across reasoning levels
### Changed
- Workspace deletion is now async — returns 202 Accepted, validates no active sessions (409 Conflict), cascade-deletes in background
- Redis caching layer now stores plain-dict instead of ORM objects, with v2-prefixed keys, storage, resilient `safe_cache_set`/`safe_cache_delete` helpers, and deferred post-commit cache invalidation
- All `get_or_create_*` CRUD operations now use savepoints (`db.begin_nested()`) instead of commit/rollback for race condition prevention
- Reconciler vector sync uses direct ORM mutation instead of batch parameterized UPDATE statements
- Summarizer enforces hard word limit in prompt and creates fallback text for empty summaries with `summary_tokens = 0`
- Blocked Gemini responses (SAFETY, RECITATION, PROHIBITED_CONTENT, BLOCKLIST) now raise `LLMError` to trigger retry/backup-provider logic
- Gemini client explicitly sets `max_output_tokens` from `max_tokens` parameter
- All deriver and metrics collector logging replaced with structured `logging.getLogger(__name__)` calls
- Dreamer specialist prompts updated to enforce durable-facts-only peer cards with max 40 entries and deduplication
- `GetOrCreateResult` changed from `NamedTuple` to `dataclass` with `async post_commit()` method
- FastAPI upgraded from 0.111.0 to 0.131.0; added pyarrow dependency
- Queue status filtering to only show user-facing tasks (representation, summary, dream); excludes internal infrastructure tasks
### Fixed
- JWT timestamp bug — `JWTParams.t` was evaluated once at class definition time instead of per-instance
- Session cache invalidation on deletion was missing
- `get_peer_card()` now properly propagates `ResourceNotFoundException` instead of swallowing it
- `set_peer_card()` ensures peer exists via `get_or_create_peers()` before updating
- Backup provider failover with proper tool input type safety
- Removed `setup_admin_jwt()` from server startup
- Sentry coroutine detection switched from `asyncio.iscoroutinefunction` to `inspect.iscoroutinefunction`
### Removed
- `explicit.py` and `obex.py` benchmarks replaced by coverage.py and molecular.py
- Claude Code review automation workflow (`.github/workflows/claude.yml`)
- Coverage reporting from default pytest configuration
</Update>
<Update label="v3.0.2">
### Added
- Documentation for reasoning_level and Claude Code plugin
### Changed
- Gave dreaming sub-agents better prompting around peer card creation, tweaked overall prompts
### Fixed
- Added message-search fallback for memory search tool, necessary in fresh sessions
- Made FLUSH_ENABLED a config value
- Removed N+1 query in search_messages
</Update>
<Update label="v3.0.1">
### Fixed
- Token counting in Explicit Agent Loop
- Backwards compatibility of queue items
</Update>
<Update label="v3.0.0">
### Added
- Agentic Dreamer for intelligent memory consolidation using LLM agents
- Agentic Dialectic for query answering using LLM agents with tool use
- Reasoning levels configuration for dialectic (`minimal`, `low`, `medium`, `high`, `max`)
- Prometheus token tracking for deriver and dialectic operations
- n8n integration
- Cloud Events for auditable telemetry
- External Vector Store support for turbopuffer and lancedb with reconciliation flow
### Changed
- API route renaming for consistency
- Dreamer and dialectic now respect peer card configuration settings
- Observations renamed to Conclusions across API and SDKs
- Deriver to buffer representation tasks to normalize workloads
- Local Representation tasks to create singular QueueItems
- getContext endpoint to use `search_query` rather than force `last_user_message`
### Fixed
- Dream scheduling bugs
- Summary creation when start_message_id > end_message_id
- Cashews upgrade to prevent NoScriptError
- Memory leak in `accumulate_metric` call
### Removed
- Peer card configuration from message configuration; peer cards no longer created/updated in deriver process
</Update>
<Update label="v2.5.1">
### Fixed
- Backwards compatibility for `message_ids` field in documents to handle legacy tuple format
</Update>
<Update label="v2.5.0">
### Added
- Message level configurations
- CRUD operations for observations
- Comprehensive test cases for harness
- Peer level get_context
- Set Peer Card Method
- Manual dreaming trigger endpoint
### Changed
- Configurations to support more flags for fine-grained control of the deriver, peer cards, summaries, etc.
- Working Representations to support more fine-grained parameters
### Fixed
- File uploads to match `MessageCreate` structure
- Cache invalidation strategy
</Update>
<Update label="v2.4.3">
### Added
- Redis caching to improve DB IO
- Backup LLM provider to avoid failures when a provider is down
### Changed
- QueueItems to use standardized columns
- Improved Deduplication logic for Representation Tasks
- More finegrained metrics for representation, summary, and peer card tasks
- DB constraint to follow standard naming conventions
</Update>
<Update label="v2.4.2">
### Fixed ### Fixed
- Langfuse tracing to have readable waterfalls - Langfuse tracing to have readable waterfalls
@ -101,7 +536,7 @@ Welcome to the Honcho changelog! This section documents all notable changes to t
<Update label="v2.3.2"> <Update label="v2.3.2">
### Added ### Added
- Get peer cards endpoint (`GET /v2/peers/{peer_id}/peer-card`) for retrieving targeted peer context information - Get peer cards endpoint (`GET /v2/peers/{peer_id}/card`) for retrieving targeted peer context information
### Changed ### Changed
@ -331,7 +766,7 @@ Welcome to the Honcho changelog! This section documents all notable changes to t
### Changed ### Changed
- `/list` endpoints to not require a request body - `/list` endpoints to not require a request body
- `metamessage_type` to `label` with backwards compatability - `metamessage_type` to `label` with backwards compatibility
- Database Provisioning to rely on alembic - Database Provisioning to rely on alembic
- Database Session Manager to explicitly rollback transactions before closing - Database Session Manager to explicitly rollback transactions before closing
the connection the connection
@ -365,6 +800,111 @@ Welcome to the Honcho changelog! This section documents all notable changes to t
<Tab title="Python SDK"> <Tab title="Python SDK">
[Python SDK](https://pypi.org/project/honcho-ai/) [Python SDK](https://pypi.org/project/honcho-ai/)
<Update label="v2.4.0">
### Added
- Scopes: `Honcho.scope()` / `HonchoAio.scope()` get-or-create a named visibility boundary, `Honcho.scopes()` lists them, and a `Scope` object adds/removes sessions, lists membership, and reads backfill `status()`. `Honcho.session(..., scopes=[...])` joins a new session to scopes at creation. Requires a Honcho server with the matching API support (Honcho v3.1.0+).
- `scope` option on `Peer.chat()` / `chat_stream()`, representation, session context, and workspace search. A single scope answers from that scope's collection and card; a list of scopes restricts recall to the union of their member sessions (explicit-only). Mutually exclusive with `session` / `sessions` / `filters`.
- Workspace-level chat: `Honcho.chat()` / `HonchoAio.chat()` and `chat_stream()` ask a question across every peer in the workspace, with the same `session`, `scope`, `reasoning_level`, and `response_format` options as `Peer.chat()`. Requires a Honcho server with the matching API support (Honcho v3.1.0+).
### Changed
- `ConclusionScope` is renamed to `ConclusionsView`. The old name remains as a deprecated alias for one more minor version. "Scope" now means a named set of sessions (`Scope`); these objects are views over one observer/observed pair.
</Update>
<Update label="v2.3.0">
### Added
- `response_format` on `Peer.chat()` / `PeerAio.chat()` and `Peer.chat_stream()` / `PeerAio.chat_stream()`, for constraining a dialectic answer to a schema. Pass a Pydantic model class to get a validated instance back (parsed via `model_validate_json`), or a raw JSON Schema dict to get the JSON string as-is. Overloads type the return precisely, so a model class narrows to that model and a dict narrows to `str`. On the streaming variants, chunks stay raw text that accumulates to a JSON string — parse it after the stream completes. Requires a Honcho server with the matching API support (Honcho v3.0.12+).
- `response_format` field on `DialecticParams`.
</Update>
<Update label="v2.2.0">
### Added
- `ConclusionLevel` type (`explicit`, `deductive`, `inductive`, `contradiction`) and a `level` field on `Conclusion`, exposing the reasoning level the server already tracked but previously stripped from responses.
- `filters` parameter on `ConclusionScope.list()` and `ConclusionScope.query()` (sync and async), passed through to the same dynamic server-side filter logic as `peers()`/`sessions()`/`messages()`. Filter explicit-only conclusions with `filters={"level": "explicit"}`, or by any other supported field/operator. Requires a Honcho server with the matching API support (Honcho v3.0.11+).
### Fixed
- Scope-managed filter keys (`observer`, `observed`, `session`) are now rejected with a clear `ValueError` if passed in `filters`, instead of silently overriding the scope and returning conclusions from a different peer pair. Use `peer.conclusions` / `conclusions_of(target)` and the `session=` parameter instead. `session_id` remains a valid filter on `query()`.
</Update>
<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.
- `peers` parameter on `Honcho.session()` and `HonchoAio.session()` — attach peers to a session at creation time instead of needing a follow-up `session.add_peers()` call. Accepts the same shapes as `Session.add_peers` (peer ID string, `Peer` object, list of either, or tuples with `SessionPeerConfig`).
### Changed
- `WorkspaceCreateParams`, `PeerCreateParams`, and `SessionCreateParams` now accept IDs up to 512 characters (was 100), matching the server-side schema change in Honcho v3.0.7.
</Update>
<Update label="v2.1.1">
### Fixed
- Broadened HTTP retry logic to cover `httpx.NetworkError` and `httpx.RemoteProtocolError` in addition to `httpx.TimeoutException` and `httpx.ConnectError`, improving resilience against transient network failures
</Update>
<Update label="v2.1.0">
### Added
- `created_at` property on `Peer` and `Session` objects
- `is_active` property on `Session` objects
- `get_message(message_id)` method on `Session` (sync and async) to fetch a single message by ID
- `page`, `size`, and `reverse` pagination parameters on all list methods
### Changed
- **Breaking**: `peer()` and `session()` now always make a get-or-create API call — no more lazy initialization
- Response configuration models now tolerate unknown fields from newer servers for forward compatibility
### Fixed
- Sync and async `Session.get_metadata()`, `get_configuration()`, and `refresh()` now refresh cached `created_at` and `is_active` values along with metadata and configuration
- `honcho.__version__` now derives from package metadata, with a source-checkout fallback, so it stays aligned with released package versions
</Update>
<Update label="v2.0.2">
### Changed
- All input models now reject unknown fields via strict Pydantic validation (`extra="forbid"`). Previously, misspelled or extraneous fields were silently ignored. Now a `ValidationError` is raised with the unrecognized field name.
</Update>
<Update label="v2.0.1">
### Added
- `set_peer_card` method
### Changed
- `card` is now `get_card` with `card` kept for backwards compatibility and marked as deprecated
</Update>
<Update label="v2.0.0">
### Added
- `ConclusionScope` object for CRUD operations on conclusions (renamed from observations)
- Representation configuration support
### Changed
- Observations renamed to Conclusions across the SDK
- Major SDK refactoring and cleanup
- Simplified method signatures throughout
- Representation endpoints now return `string` instead of old Representation object
### Removed
- Standalone types module (now uses honcho-core types)
- Representation object
</Update>
<Update label="v1.6.0">
### Added
- metadata and configuration fields to Workspace, Peer, Session, and Message objects
- Session Clone methods
- Peer level get_context method
- `ObservationScope` object to perform CRUD operations on observations
- Representation object for WorkingRepresentations
### Changed
- methods that take IDs, can all optionally take an object of the same type
</Update>
<Update label="v1.5.0"> <Update label="v1.5.0">
### Added ### Added
@ -439,6 +979,141 @@ Welcome to the Honcho changelog! This section documents all notable changes to t
<Tab title="TypeScript SDK"> <Tab title="TypeScript SDK">
[TypeScript SDK](https://www.npmjs.com/package/@honcho-ai/sdk) [TypeScript SDK](https://www.npmjs.com/package/@honcho-ai/sdk)
<Update label="v2.4.0">
### Added
- Scopes: `honcho.scope()` get-or-creates a named visibility boundary, `honcho.scopes()` lists them, and a `Scope` object adds/removes sessions, lists membership, and reads backfill `status()`. `honcho.session({ scopes: [...] })` joins a new session to scopes at creation. Requires a Honcho server with the matching API support (Honcho v3.1.0+).
- `scope` option on `peer.chat()` / `chatStream()`, representation, session context, and workspace search. A single scope answers from that scope's collection and card; a list of scopes restricts recall to the union of their member sessions (explicit-only). Mutually exclusive with `session` / `sessions` / `filters`.
- Workspace-level chat: `honcho.chat()` / `honcho.chatStream()` ask a question across every peer in the workspace, with the same `session`, `scope`, `reasoningLevel`, and `responseFormat` options as `peer.chat()`. Requires a Honcho server with the matching API support (Honcho v3.1.0+).
### Changed
- `ConclusionScope` is renamed to `ConclusionsView`. The old name remains as a deprecated alias for one more minor version. "Scope" now means a named set of sessions (`Scope`); these objects are views over one observer/observed pair.
</Update>
<Update label="v2.3.0">
### Added
- `responseFormat` option on `peer.chat()` and `peer.chatStream()`, for constraining a dialectic answer to a schema. Pass a Zod schema to get a parsed, validated result back, or a raw JSON Schema object to get the JSON string as-is. Overloads type the return precisely, so a Zod schema narrows to its inferred type and a plain object narrows to `string`. On `chatStream()`, chunks stay raw text that accumulates to a JSON string — parse it after the stream completes. Requires a Honcho server with the matching API support (Honcho v3.0.12+).
</Update>
<Update label="v2.2.0">
### Added
- `ConclusionLevel` type (`explicit`, `deductive`, `inductive`, `contradiction`) and a `level` field on `Conclusion`, exposing the reasoning level the server already tracked but previously stripped from responses.
- `filters` option on `conclusions.list()` and `conclusions.query()`, passed through to the same dynamic server-side filter logic as the other list endpoints. Filter explicit-only conclusions with `{ filters: { level: 'explicit' } }`, or by any other supported field/operator. Requires a Honcho server with the matching API support (Honcho v3.0.11+).
### Fixed
- Scope-managed filter keys (`observer`, `observed`, `session`) are now rejected with a clear error if passed in `filters`, instead of silently overriding the scope and returning conclusions from a different peer pair. Use `peer.conclusions` / `peer.conclusionsOf(target)` and the dedicated `session` option instead. `session_id` remains a valid filter on `query()`.
</Update>
<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).
### Changed
- ID validation in `validation.ts` now accepts workspace, peer, and session IDs up to 512 characters (was 100), matching the server-side schema change in Honcho v3.0.7.
### Fixed
- `Honcho.workspaces()` now actually forwards the `reverse` option to the server. The 2.1.0 changelog listed `workspaces()` among the list methods that gained `reverse`, but `client.ts` was missing the field on the params type and request builder, so the option was silently dropped. 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.
</Update>
<Update label="v2.1.1">
### Fixed
- Broadened fetch error retry logic to catch all `TypeError` network failures (connection resets, DNS errors, etc.) instead of only those with `'fetch'` in the message, improving resilience across runtimes (Node, Bun, browsers)
</Update>
<Update label="v2.1.0">
### Added
- `createdAt` property on `Peer` and `Session` wrapper objects
- `isActive` property on `Session` wrapper objects
- `getMessage(messageId)` method on `Session` to fetch a single message by ID
- `Peer.representation()`, `Session.representation()`, and `Session.context()` now accept `Message` objects for `searchQuery`
- `page`, `size`, and `reverse` pagination controls on all list methods
### Changed
- **Breaking**: `searchQuery` removed from top-level `context()` options — use `representationOptions.searchQuery` instead:
```typescript
// Before (v2.0.x)
await session.context({ searchQuery: "..." });
// After (v2.1.0)
await session.context({ representationOptions: { searchQuery: "..." } });
```
- List methods (`peers()`, `sessions()`, `messages()`, `workspaces()`) support both the new options object and the legacy raw-filter form
- Representation search options now accept strings and content-like objects, including `Message` instances, while rejecting whitespace-only or invalid runtime inputs
- **Breaking**: `peer()` and `session()` now always make a get-or-create API call — no more lazy initialization. If you relied on constructing SDK objects without triggering a network request, note that every `peer()` and `session()` call now hits the API:
```typescript
// Before (v2.0.x) — no API call
const session = honcho.session("my-session");
// After (v2.1.0) — makes a get-or-create API call
const session = await honcho.session("my-session");
```
- Response configuration models now tolerate unknown fields from newer servers for forward compatibility
- Moved `@types/node` from `dependencies` to `devDependencies`
### Fixed
- `uploadFile()` now rejects unsupported top-level binary/object inputs and only validates inputs the serializer can actually upload
- `uploadFile()` now serializes message configuration using API field names, matching `addMessages()`
- Session fetch methods now refresh cached `createdAt` and `isActive` values alongside metadata and configuration
</Update>
<Update label="v2.0.2">
### Changed
- Client constructor now rejects unknown options via `.strict()` Zod validation. Previously, misspelled options (e.g., `baseUrl` instead of `baseURL`) were silently ignored, causing the SDK to fall back to defaults. Now a `ZodError` is thrown with the unrecognized key name.
- All input schemas now use `.strict()` validation to reject unknown fields.
- `FileUploadSchema.configuration` now uses `MessageConfigurationSchema` instead of open record type.
### Fixed
- README example used `baseUrl` instead of `baseURL`.
</Update>
<Update label="v2.0.1">
### Added
- `setPeerCard` method
### Changed
- `card` is now `getCard` with `card` kept for backwards compatibility and marked as deprecated
</Update>
<Update label="v2.0.0">
### Added
- `ConclusionScope` object for CRUD operations on conclusions (renamed from observations)
- Representation configuration support
### Changed
- Observations renamed to Conclusions across the SDK
- Major SDK refactoring and cleanup
- Simplified method signatures throughout
- Representation endpoints now return `string` instead of old Representation object
### Fixed
- Pagination `this` binding issue
### Removed
- Representation object
- Stainless "core" SDK -- this SDK is now standalone
</Update>
<Update label="v1.6.0">
### Added
- metadata and configuration fields to Workspace, Peer, Session, and Message objects
- Session Clone methods
- Peer level get_context method
- `ObservationScope` object to perform CRUD operations on observations
- Representation object for WorkingRepresentations
### Changed
- methods that take IDs, can all optionally take an object of the same type
</Update>
<Update label="v1.5.0"> <Update label="v1.5.0">
### Added ### Added
@ -508,6 +1183,54 @@ Welcome to the Honcho changelog! This section documents all notable changes to t
- Simplified Honcho client import path - Simplified Honcho client import path
</Update> </Update>
</Tab> </Tab>
<Tab title="Honcho CLI">
[Honcho CLI](https://pypi.org/project/honcho-cli/)
<Update label="v0.1.4">
### Added
- A TTY notice when a newer `honcho-cli` is on PyPI (`uv tool upgrade honcho-cli`). Skipped in JSON mode; disable with `HONCHO_NO_UPDATE_CHECK`
### Fixed
- `--setup` for openai-compatible writes `EMBEDDING_MODEL_CONFIG__OVERRIDES__BASE_URL` into the profile `.env` alongside `LLM_OPENAI_BASE_URL` (#1068)
- `--setup` API key prompts echo `*` per character so a paste is visibly received instead of a blank getpass field
</Update>
<Update label="v0.1.3">
### Added
- `honcho start`, `honcho stop`, and `honcho status` — run a personal Honcho stack in Docker (API, deriver, Postgres, Redis). Profiles live under `~/.honcho/profiles/`. First start pins `ghcr.io/plastic-labs/honcho:latest` by digest and copies the image `config.toml`. Optional `--setup basic` / `--setup advanced` wizard writes LLM overrides to `.env` (#1029)
- `honcho session view` — session transcript table (`--last N`, `--page N --size M`, `--all`, `--reverse`, `--ids`, peer filter via `-p`). Content is shown verbatim, timestamps are normalized to UTC, and the command is read-only: unlike the other session commands it never get-or-creates the session (#1006)
### Fixed
- `honcho message list --last N` no longer stops at the first page of 50 — it walks pages to fill the requested window (#1006)
</Update>
<Update label="v0.1.2">
### Added
- Device-code OAuth login for managed Honcho servers. `honcho init` now offers browser-based login (RFC 8628 device authorization grant) when the host advertises the device grant in its OAuth authorization-server metadata; tokens are persisted to `~/.honcho/config.json` and auto-refreshed (#891)
- `HONCHO_CONFIG_DIR` environment variable for pointing the CLI at an alternate config directory (#891)
### Changed
- An OAuth grant now records the host it was minted against and is ignored — neither used nor refreshed — when `base_url` points elsewhere, so a staging grant is never sent to production. A live OAuth token takes precedence over a stored `apiKey`, and a dead grant degrades to the saved key with a warning instead of aborting. Device login no longer deletes the shared `apiKey`, which sibling tools read from the same config file (#891)
</Update>
<Update label="v0.1.1">
### Fixed
- Declare `click` as an explicit dependency. The CLI imported `click` directly but relied on it being pulled in transitively, so installs without it on the path could fail at runtime (#787)
</Update>
<Update label="v0.1.0">
### Added
- Initial release of `honcho-cli` — a terminal for inspecting and managing a Honcho deployment (#424)
- `workspace`, `peer`, `session`, `message`, `conclusion`, and `config` command groups for managing resources against any Honcho server
- `init` onboarding flow that prompts for and persists connection settings, with flag/env-var pre-seeding for non-interactive use
- Per-command flags, environment variables, and a config file for pointing the CLI at different servers (local, self-hosted, or hosted)
- Rich terminal output and an agent-usage mode for scripting against the CLI
- Documentation and an agent skill for the CLI (#589)
</Update>
</Tab>
</Tabs> </Tabs>
## Getting Help ## Getting Help
@ -515,4 +1238,4 @@ Welcome to the Honcho changelog! This section documents all notable changes to t
If you encounter issues using the Honcho API or its SDKs: If you encounter issues using the Honcho API or its SDKs:
1. Open an issue on [GitHub](https://github.com/plastic-labs/honcho/issues) 1. Open an issue on [GitHub](https://github.com/plastic-labs/honcho/issues)
2. Join our [Discord community](http://discord.gg/plasticlabs) for support 2. Join our [Discord community](http://discord.gg/honcho) for support

View File

@ -2,29 +2,284 @@
"$schema": "https://mintlify.com/docs.json", "$schema": "https://mintlify.com/docs.json",
"theme": "mint", "theme": "mint",
"name": "Honcho", "name": "Honcho",
"redirects": [
{
"source": "/",
"destination": "/v3/documentation/introduction/overview"
},
{
"source": "/v3/guides/integrations/claudecode",
"destination": "/v3/guides/integrations/claude-code"
},
{
"source": "/v3/documentation/features/advanced/representation-scopes",
"destination": "/v3/documentation/features/advanced/directional-representations"
}
],
"colors": { "colors": {
"primary": "#86BCF2", "primary": "#66AAFF",
"dark": "#151E27", "dark": "#151E27",
"light": "#B5D9FD" "light": "#86BCF2"
}, },
"favicon": "/favicon.svg", "favicon": "/favicon.svg",
"contextual": { "contextual": {
"options": [ "options": ["copy", "view", "chatgpt", "claude"]
"copy",
"view",
"chatgpt",
"claude"
]
}, },
"navigation": { "navigation": {
"versions": [ "versions": [
{ {
"version": "v2.4.2", "version": "v3.1.1",
"api": { "api": {
"openapi": [ "openapi": ["v3/openapi.json"]
"openapi.documented.yml" },
"tabs": [
{
"tab": "Documentation",
"groups": [
{
"group": "Introduction",
"pages": [
"v3/documentation/introduction/overview",
"v3/documentation/introduction/quickstart",
"v3/documentation/introduction/vibecoding"
] ]
}, },
{
"group": "Core Concepts",
"pages": [
"v3/documentation/core-concepts/architecture",
"v3/documentation/core-concepts/reasoning",
"v3/documentation/core-concepts/representation",
"v3/documentation/core-concepts/design-patterns"
]
},
{
"group": "Features",
"pages": [
"v3/documentation/features/storing-data",
"v3/documentation/features/get-context",
"v3/documentation/features/chat",
{
"group": "Advanced",
"pages": [
"v3/documentation/features/advanced/overview",
"v3/documentation/features/advanced/reasoning-configuration",
"v3/documentation/features/advanced/summarizer",
"v3/documentation/features/advanced/peer-card",
"v3/documentation/features/advanced/directional-representations",
"v3/documentation/features/advanced/scopes",
"v3/documentation/features/advanced/dreaming",
"v3/documentation/features/advanced/queue-status",
"v3/documentation/features/advanced/webhooks",
"v3/documentation/features/advanced/search",
"v3/documentation/features/advanced/using-filters",
"v3/documentation/features/advanced/structured-outputs",
"v3/documentation/features/advanced/streaming-response",
"v3/documentation/features/advanced/file-uploads",
"v3/documentation/features/advanced/deleting-data"
]
}
]
},
{
"group": "Reference",
"pages": [
"v3/documentation/reference/platform",
"v3/documentation/reference/sdk",
"v3/documentation/reference/cli"
]
}
]
},
{
"tab": "Guides",
"groups": [
{
"group": "Overview",
"pages": ["v3/guides/overview"]
},
{
"group": "Integrations",
"pages": [
"v3/guides/integrations/claude-code",
"v3/guides/integrations/opencode",
"v3/guides/integrations/codex",
"v3/guides/integrations/deepseek-harness",
"v3/guides/integrations/vercel-ai-sdk",
"v3/guides/integrations/crewai",
"v3/guides/integrations/langgraph",
"v3/guides/integrations/mcp",
"v3/guides/integrations/n8n",
"v3/guides/integrations/openclaw",
"v3/guides/integrations/hermes",
"v3/guides/integrations/zo-computer",
"v3/guides/integrations/paperclip",
"v3/guides/integrations/sillytavern"
]
},
{
"group": "Tutorials",
"pages": [
"v3/guides/recipes/unified-memory-setup",
"v3/guides/discord",
"v3/guides/granola",
"v3/guides/telegram",
"v3/guides/integrations/reachy-mini",
"v3/guides/gmail"
]
},
{
"group": "Community Integrations",
"pages": [
"v3/guides/community/agent0",
"v3/guides/community/pi-honcho-memory"
]
},
{
"group": "Migrations",
"pages": ["v3/guides/migrations/mem0"]
}
]
},
{
"tab": "Open Source",
"groups": [
{
"group": "Self-Hosting",
"pages": [
"v3/contributing/self-hosting",
"v3/contributing/configuration",
"v3/contributing/changing-embeddings",
"v3/contributing/troubleshooting"
]
},
{
"group": "Contributing",
"pages": [
"v3/contributing/guidelines",
"v3/contributing/license"
]
}
]
},
{
"tab": "API Reference",
"groups": [
{
"group": "API Documentation",
"pages": ["v3/api-reference/introduction"]
},
{
"group": "workspaces",
"pages": [
"v3/api-reference/endpoint/workspaces/get-or-create-workspace",
"v3/api-reference/endpoint/workspaces/get-all-workspaces",
"v3/api-reference/endpoint/workspaces/update-workspace",
"v3/api-reference/endpoint/workspaces/delete-workspace",
"v3/api-reference/endpoint/workspaces/search-workspace",
"v3/api-reference/endpoint/workspaces/get-queue-status",
"v3/api-reference/endpoint/workspaces/schedule-dream"
]
},
{
"group": "peers",
"pages": [
"v3/api-reference/endpoint/peers/get-peers",
"v3/api-reference/endpoint/peers/get-or-create-peer",
"v3/api-reference/endpoint/peers/update-peer",
"v3/api-reference/endpoint/peers/get-sessions-for-peer",
"v3/api-reference/endpoint/peers/chat",
"v3/api-reference/endpoint/peers/get-representation",
"v3/api-reference/endpoint/peers/get-peer-card",
"v3/api-reference/endpoint/peers/set-peer-card",
"v3/api-reference/endpoint/peers/get-peer-context",
"v3/api-reference/endpoint/peers/search-peer"
]
},
{
"group": "sessions",
"pages": [
"v3/api-reference/endpoint/sessions/get-or-create-session",
"v3/api-reference/endpoint/sessions/get-sessions",
"v3/api-reference/endpoint/sessions/update-session",
"v3/api-reference/endpoint/sessions/delete-session",
"v3/api-reference/endpoint/sessions/clone-session",
"v3/api-reference/endpoint/sessions/get-session-peers",
"v3/api-reference/endpoint/sessions/set-session-peers",
"v3/api-reference/endpoint/sessions/add-peers-to-session",
"v3/api-reference/endpoint/sessions/remove-peers-from-session",
"v3/api-reference/endpoint/sessions/get-peer-config",
"v3/api-reference/endpoint/sessions/set-peer-config",
"v3/api-reference/endpoint/sessions/get-session-context",
"v3/api-reference/endpoint/sessions/get-session-summaries",
"v3/api-reference/endpoint/sessions/search-session"
]
},
{
"group": "scopes",
"pages": [
"v3/api-reference/endpoint/scopes/get-or-create-scope",
"v3/api-reference/endpoint/scopes/get-scopes",
"v3/api-reference/endpoint/scopes/get-scope",
"v3/api-reference/endpoint/scopes/add-sessions-to-scope",
"v3/api-reference/endpoint/scopes/get-scope-sessions",
"v3/api-reference/endpoint/scopes/remove-session-from-scope",
"v3/api-reference/endpoint/scopes/get-scope-status"
]
},
{
"group": "messages",
"pages": [
"v3/api-reference/endpoint/messages/create-messages-for-session",
"v3/api-reference/endpoint/messages/get-messages",
"v3/api-reference/endpoint/messages/get-message",
"v3/api-reference/endpoint/messages/update-message",
"v3/api-reference/endpoint/messages/create-messages-with-file"
]
},
{
"group": "conclusions",
"pages": [
"v3/api-reference/endpoint/conclusions/create-conclusions",
"v3/api-reference/endpoint/conclusions/list-conclusions",
"v3/api-reference/endpoint/conclusions/query-conclusions",
"v3/api-reference/endpoint/conclusions/delete-conclusion"
]
},
{
"group": "webhooks",
"pages": [
"v3/api-reference/endpoint/webhooks/list-webhook-endpoints",
"v3/api-reference/endpoint/webhooks/get-or-create-webhook-endpoint",
"v3/api-reference/endpoint/webhooks/delete-webhook-endpoint",
"v3/api-reference/endpoint/webhooks/test-emit"
]
},
{
"group": "miscellaneous",
"pages": ["v3/api-reference/endpoint/keys/create-key"]
}
]
},
{
"tab": "Changelog",
"groups": [
{
"group": "Overview",
"pages": [
"changelog/introduction",
"changelog/compatibility-guide"
]
}
]
}
]
},
{
"version": "v2.5.1",
"api": {
"openapi": ["v2/openapi.json"]
},
"tabs": [ "tabs": [
{ {
"tab": "Documentation", "tab": "Documentation",
@ -41,10 +296,19 @@
"group": "Core Concepts", "group": "Core Concepts",
"pages": [ "pages": [
"v2/documentation/core-concepts/architecture", "v2/documentation/core-concepts/architecture",
"v2/documentation/core-concepts/glossary", "v2/documentation/core-concepts/features/storing-data",
"v2/documentation/core-concepts/features", "v2/documentation/core-concepts/features/dialectic-endpoint",
"v2/documentation/core-concepts/features/get-context",
"v2/documentation/core-concepts/features/search",
"v2/documentation/core-concepts/features/working-rep",
"v2/documentation/core-concepts/features/streaming-response",
"v2/documentation/core-concepts/features/using-filters",
"v2/documentation/core-concepts/features/file-uploads",
"v2/documentation/core-concepts/features/queue-status",
"v2/documentation/core-concepts/features/local-vs-global",
"v2/documentation/core-concepts/configuration", "v2/documentation/core-concepts/configuration",
"v2/documentation/core-concepts/summarizer" "v2/documentation/core-concepts/summarizer",
"v2/documentation/core-concepts/glossary"
] ]
}, },
{ {
@ -61,43 +325,27 @@
"groups": [ "groups": [
{ {
"group": "Getting Started", "group": "Getting Started",
"pages": ["v2/guides/overview"]
},
{
"group": "Migrations",
"pages": ["v2/migrations/from-mem0"]
},
{
"group": "Integrations",
"pages": [ "pages": [
"v2/guides/overview", "v2/integrations/crewai",
"v2/guides/mcp" "v2/integrations/langgraph",
"v2/integrations/mcp"
] ]
}, },
{ {
"group": "Application Interfaces", "group": "Application Interfaces",
"pages": [ "pages": [
"v2/guides/discord", "v2/guides/discord",
"v2/guides/n8n",
"v2/guides/telegram" "v2/guides/telegram"
] ]
},
{
"group": "Design Patterns",
"pages": [
"v2/guides/dialectic-endpoint",
"v2/guides/get-context",
"v2/guides/search",
"v2/guides/working-rep",
"v2/guides/streaming-response",
"v2/guides/using-filters",
"v2/guides/file-uploads"
]
}
]
},
{
"tab": "Contributing",
"groups": [
{
"group": "Contributing",
"pages": [
"v2/contributing/guidelines",
"v2/contributing/self-hosting",
"v2/contributing/configuration",
"v2/contributing/license"
]
} }
] ]
}, },
@ -106,9 +354,7 @@
"groups": [ "groups": [
{ {
"group": "API Documentation", "group": "API Documentation",
"pages": [ "pages": ["v2/api-reference/introduction"]
"v2/api-reference/introduction"
]
}, },
{ {
"group": "workspaces", "group": "workspaces",
@ -118,7 +364,8 @@
"v2/api-reference/endpoint/workspaces/update-workspace", "v2/api-reference/endpoint/workspaces/update-workspace",
"v2/api-reference/endpoint/workspaces/delete-workspace", "v2/api-reference/endpoint/workspaces/delete-workspace",
"v2/api-reference/endpoint/workspaces/search-workspace", "v2/api-reference/endpoint/workspaces/search-workspace",
"v2/api-reference/endpoint/workspaces/get-deriver-status" "v2/api-reference/endpoint/workspaces/get-deriver-status",
"v2/api-reference/endpoint/workspaces/trigger-dream"
] ]
}, },
{ {
@ -130,8 +377,10 @@
"v2/api-reference/endpoint/peers/get-sessions-for-peer", "v2/api-reference/endpoint/peers/get-sessions-for-peer",
"v2/api-reference/endpoint/peers/chat", "v2/api-reference/endpoint/peers/chat",
"v2/api-reference/endpoint/peers/get-working-representation", "v2/api-reference/endpoint/peers/get-working-representation",
"v2/api-reference/endpoint/peers/search-peer", "v2/api-reference/endpoint/peers/get-peer-card",
"v2/api-reference/endpoint/peers/get-peer-card" "v2/api-reference/endpoint/peers/set-peer-card",
"v2/api-reference/endpoint/peers/get-peer-context",
"v2/api-reference/endpoint/peers/search-peer"
] ]
}, },
{ {
@ -163,6 +412,15 @@
"v2/api-reference/endpoint/messages/create-messages-with-file" "v2/api-reference/endpoint/messages/create-messages-with-file"
] ]
}, },
{
"group": "observations",
"pages": [
"v2/api-reference/endpoint/observations/create-observations",
"v2/api-reference/endpoint/observations/list-observations",
"v2/api-reference/endpoint/observations/query-observations",
"v2/api-reference/endpoint/observations/delete-observation"
]
},
{ {
"group": "webhooks", "group": "webhooks",
"pages": [ "pages": [
@ -182,13 +440,15 @@
] ]
}, },
{ {
"tab": "Changelog", "tab": "Contributing",
"groups": [ "groups": [
{ {
"group": "Overview", "group": "Contributing",
"pages": [ "pages": [
"changelog/introduction", "v2/contributing/guidelines",
"changelog/compatibility-guide" "v2/contributing/self-hosting",
"v2/contributing/configuration",
"v2/contributing/license"
] ]
} }
] ]
@ -198,9 +458,7 @@
{ {
"version": "v1.1.0", "version": "v1.1.0",
"api": { "api": {
"openapi": [ "openapi": ["openapi.json"]
"openapi.json"
]
}, },
"tabs": [ "tabs": [
{ {
@ -230,23 +488,15 @@
"groups": [ "groups": [
{ {
"group": "Getting Started", "group": "Getting Started",
"pages": [ "pages": ["v1/guides/overview", "v1/guides/streaming-response"]
"v1/guides/overview",
"v1/guides/streaming-response"
]
}, },
{ {
"group": "Application Interfaces", "group": "Application Interfaces",
"pages": [ "pages": ["v1/guides/discord", "v1/guides/honcho-mcp"]
"v1/guides/discord",
"v1/guides/honcho-mcp"
]
}, },
{ {
"group": "Personal Memory", "group": "Personal Memory",
"pages": [ "pages": ["v1/guides/dialectic-endpoint"]
"v1/guides/dialectic-endpoint"
]
} }
] ]
}, },
@ -255,9 +505,7 @@
"groups": [ "groups": [
{ {
"group": "API Documentation", "group": "API Documentation",
"pages": [ "pages": ["v1/api-reference/introduction"]
"v1/api-reference/introduction"
]
}, },
{ {
"group": "apps", "group": "apps",
@ -305,9 +553,7 @@
}, },
{ {
"group": "keys", "group": "keys",
"pages": [ "pages": ["v1/api-reference/endpoint/keys/create-key"]
"v1/api-reference/endpoint/keys/create-key"
]
}, },
{ {
"group": "metamessages", "group": "metamessages",
@ -341,41 +587,10 @@
] ]
} }
] ]
},
{
"tab": "Changelog",
"groups": [
{
"group": "Overview",
"pages": [
"changelog/introduction",
"changelog/compatibility-guide"
]
} }
] ]
} }
] ]
}
],
"global": {
"anchors": [
{
"anchor": "Managed Platform",
"href": "https://app.honcho.dev",
"icon": "book-open-cover"
},
{
"anchor": "Community",
"href": "https://discord.gg/honcho",
"icon": "discord"
},
{
"anchor": "Blog",
"href": "https://blog.plasticlabs.ai",
"icon": "newspaper"
}
]
}
}, },
"logo": { "logo": {
"light": "/logo/honcho-dark.svg", "light": "/logo/honcho-dark.svg",
@ -389,14 +604,16 @@
}, },
"footer": { "footer": {
"socials": { "socials": {
"twitter": "https://twitter.com/plastic_labs", "twitter": "https://x.com/honchodotdev",
"github": "https://github.com/plastic-labs", "github": "https://github.com/plastic-labs/honcho",
"linkedin": "https://www.linkedin.com/company/plasticlabs" "discord": "https://discord.gg/honcho",
"linkedin": "https://www.linkedin.com/company/plasticlabs",
"youtube": "https://www.youtube.com/@plasticlabs"
} }
}, },
"integrations": { "integrations": {
"posthog": { "gtm": {
"apiKey": "phc_1yrzzcgywqXGcerkkI4g7C0YfyPMcAKNOOvGcjTCiUk" "tagId": "GTM-NSPT9PJF"
} }
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

BIN
docs/images/reasoning.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

View File

@ -5,16 +5,15 @@
"main": ".pnp.js", "main": ".pnp.js",
"scripts": { "scripts": {
"dev": "mint dev", "dev": "mint dev",
"openapi": "npx @mintlify/scraping openapi-file openapi.documented.yml -o api-reference/endpoint", "openapi": "npx @mintlify/scraping openapi-file v3/openapi.json -o v3/api-reference/endpoint",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@mintlify/scraping": "^4.0.284", "@mintlify/scraping": "^4.0.467"
"honcho-ai": "^0.0.11"
}, },
"devDependencies": { "devDependencies": {
"mint": "^4.2.123" "mint": "^4.2.204"
} }
} }

69
docs/posthog-consent.js Normal file
View File

@ -0,0 +1,69 @@
// Loads PostHog only when the CookieConsent cookie grants Statistics; the
// cookie is host-scoped, so a landing-page answer covers the docs.
;(function () {
var KEY = 'phc_1yrzzcgywqXGcerkkI4g7C0YfyPMcAKNOOvGcjTCiUk'
var loaded = false
function granted() {
var m = document.cookie.match(/(?:^|;\s*)CookieConsent=([^;]*)/)
if (!m) return false
var v = decodeURIComponent(m[1])
// "-1" is Cookiebot's consent-not-required marker.
return v === '-1' || /statistics\s*:\s*true/.test(v)
}
function loadPosthog() {
if (loaded) return
loaded = true
var s = document.createElement('script')
s.src = 'https://us-assets.i.posthog.com/static/array.js'
s.async = true
s.onerror = function () {
loaded = false
}
s.onload = function () {
// Consent withdrawn while array.js was downloading: skip init, allow a retry on re-grant.
if (!granted()) {
loaded = false
return
}
window.posthog.init(KEY, {
api_host: 'https://us.i.posthog.com',
ui_host: 'https://us.posthog.com',
cross_subdomain_cookie: true,
person_profiles: 'identified_only',
capture_pageview: 'history_change',
})
}
document.head.appendChild(s)
}
function sync() {
if (granted()) {
if (!loaded) {
loadPosthog()
} else if (
window.posthog &&
window.posthog.has_opted_out_capturing &&
window.posthog.has_opted_out_capturing()
) {
window.posthog.opt_in_capturing()
}
return
}
// Withdrawal mid-session: an already running instance must stop.
if (loaded && window.posthog && window.posthog.opt_out_capturing) {
window.posthog.opt_out_capturing()
}
}
sync()
var events = [
'CookiebotOnConsentReady',
'CookiebotOnAccept',
'CookiebotOnDecline',
]
for (var i = 0; i < events.length; i++) {
window.addEventListener(events[i], sync)
}
})()

View File

@ -0,0 +1,652 @@
{/*
GENERATED by honcho-cli/scripts/generate_cli_docs.py — do not edit.
Re-generate with: uv run --package honcho-cli python honcho-cli/scripts/generate_cli_docs.py
Source of truth: honcho-cli/src/honcho_cli/commands/
*/}
## honcho conclusion
List, search, create, and delete peer conclusions (Honcho's memory atoms).
<AccordionGroup>
<Accordion title="create">
Create a conclusion.
```bash
honcho conclusion create <content>
```
<ParamField path="content" type="string" required />
<ParamField path="--observer" type="string">
Observer peer ID.
</ParamField>
<ParamField path="--observed" type="string">
Observed peer ID.
</ParamField>
<ParamField path="--session" type="string">
Session context. Short alias: `-s`.
</ParamField>
</Accordion>
<Accordion title="delete">
Delete a conclusion.
```bash
honcho conclusion delete <conclusion_id>
```
<ParamField path="conclusion_id" type="string" required />
<ParamField path="--observer" type="string">
Observer peer ID.
</ParamField>
<ParamField path="--observed" type="string">
Observed peer ID.
</ParamField>
<ParamField path="--yes" type="boolean">
Skip confirmation. Short alias: `-y`.
</ParamField>
</Accordion>
<Accordion title="list">
List conclusions.
```bash
honcho conclusion list
```
<ParamField path="--observer" type="string">
Observer peer ID.
</ParamField>
<ParamField path="--observed" type="string">
Observed peer ID.
</ParamField>
<ParamField path="--limit" type="number" default="10">
Max results.
</ParamField>
</Accordion>
<Accordion title="search">
Semantic search over conclusions.
```bash
honcho conclusion search <query>
```
<ParamField path="query" type="string" required />
<ParamField path="--observer" type="string">
Observer peer ID.
</ParamField>
<ParamField path="--observed" type="string">
Observed peer ID.
</ParamField>
<ParamField path="--top-k" type="number" default="10">
Max results.
</ParamField>
</Accordion>
</AccordionGroup>
## honcho config
Inspect CLI configuration.
```bash
honcho config
```
## honcho doctor
Verify config and connectivity. Scope with -w / -p to check workspace, peer, and queue health.
```bash
honcho doctor
```
## honcho help
Show help message.
```bash
honcho help
```
## honcho init
Set API key and server URL in ~/.honcho/config.json.
Press Enter to keep the current value or type a replacement.
Workspace / peer / session scoping is per-command via -w / -p / -s
or HONCHO_* env vars — never persisted.
```bash
honcho init
```
<ParamField path="--api-key" type="string">
API key (admin JWT).
</ParamField>
<ParamField path="--base-url" type="string">
Honcho API URL (e.g. https://api.honcho.dev, http://localhost:8000).
</ParamField>
## honcho message
List, create, and get messages within a session.
<AccordionGroup>
<Accordion title="create">
Create a message in a session.
```bash
honcho message create <content>
```
<ParamField path="content" type="string" required />
<ParamField path="--peer" type="string" required>
Peer ID of the message sender. Short alias: `-p`.
</ParamField>
<ParamField path="--metadata" type="string">
JSON metadata to associate with the message.
</ParamField>
<ParamField path="--session" type="string">
Session ID. Short alias: `-s`.
</ParamField>
</Accordion>
<Accordion title="get">
Get a single message by ID.
```bash
honcho message get <message_id>
```
<ParamField path="message_id" type="string" required />
<ParamField path="--session" type="string">
Session ID. Short alias: `-s`.
</ParamField>
</Accordion>
<Accordion title="list">
List messages in a session. Scoped to a peer with -p.
```bash
honcho message list [<session_id>]
```
<ParamField path="session_id" type="string" />
<ParamField path="--last" type="number" default="20">
Number of recent messages.
</ParamField>
<ParamField path="--reverse" type="boolean">
Show oldest first (default is newest first).
</ParamField>
<ParamField path="--brief" type="boolean">
Show only IDs, peer, token count, and created_at (no content).
</ParamField>
<ParamField path="--peer" type="string">
Filter by peer ID. Short alias: `-p`.
</ParamField>
</Accordion>
</AccordionGroup>
## honcho peer
List, create, chat with, search, and manage peers and their representations.
<AccordionGroup>
<Accordion title="card">
Get raw peer card content.
```bash
honcho peer card [<peer_id>]
```
<ParamField path="peer_id" type="string" />
<ParamField path="--target" type="string">
Target peer for relationship card.
</ParamField>
</Accordion>
<Accordion title="chat">
Query the dialectic about a peer.
```bash
honcho peer chat <query>
```
<ParamField path="query" type="string" required />
<ParamField path="--target" type="string">
Target peer for perspective.
</ParamField>
<ParamField path="--reasoning" type="string">
Reasoning level: minimal, low, medium, high, max. Short alias: `-r`.
</ParamField>
</Accordion>
<Accordion title="create">
Create or get a peer.
```bash
honcho peer create <peer_id>
```
<ParamField path="peer_id" type="string" required />
<ParamField path="--observe-me" type="boolean">
Whether Honcho will form a representation of this peer. Negate with `--no-observe-me`.
</ParamField>
<ParamField path="--metadata" type="string">
JSON metadata to associate with the peer.
</ParamField>
</Accordion>
<Accordion title="get-metadata">
Get metadata for a peer.
```bash
honcho peer get-metadata [<peer_id>]
```
<ParamField path="peer_id" type="string" />
</Accordion>
<Accordion title="inspect">
Inspect a peer: card, session count, recent conclusions.
```bash
honcho peer inspect [<peer_id>]
```
<ParamField path="peer_id" type="string" />
</Accordion>
<Accordion title="list">
List all peers in the workspace.
```bash
honcho peer list
```
</Accordion>
<Accordion title="representation">
Get the formatted representation for a peer.
```bash
honcho peer representation [<peer_id>]
```
<ParamField path="peer_id" type="string" />
<ParamField path="--target" type="string">
Target peer to get representation about.
</ParamField>
<ParamField path="--search-query" type="string">
Semantic search query to filter conclusions.
</ParamField>
<ParamField path="--max-conclusions" type="number">
Maximum number of conclusions to include.
</ParamField>
</Accordion>
<Accordion title="search">
Search a peer's messages.
```bash
honcho peer search <query>
```
<ParamField path="query" type="string" required />
<ParamField path="--limit" type="number" default="10">
Max results.
</ParamField>
</Accordion>
<Accordion title="set-metadata">
Set metadata for a peer.
```bash
honcho peer set-metadata <metadata>
```
<ParamField path="metadata" type="string" required />
<ParamField path="--peer" type="string">
Peer ID (uses default if omitted). Short alias: `-p`.
</ParamField>
</Accordion>
</AccordionGroup>
## honcho session
List, inspect, view, create, delete, and manage conversation sessions and their peers.
<AccordionGroup>
<Accordion title="add-peers">
Add peers to a session.
```bash
honcho session add-peers <session_id> <peer_ids>
```
<ParamField path="session_id" type="string" required />
<ParamField path="peer_ids" type="string" required />
</Accordion>
<Accordion title="context">
Get session context (what an agent would see).
```bash
honcho session context [<session_id>]
```
<ParamField path="session_id" type="string" />
<ParamField path="--tokens" type="number">
Token budget.
</ParamField>
<ParamField path="--summary" type="boolean" default="true">
Include summary. Negate with `--no-summary`.
</ParamField>
</Accordion>
<Accordion title="create">
Create or get a session.
```bash
honcho session create <session_id>
```
<ParamField path="session_id" type="string" required />
<ParamField path="--peers" type="string">
Comma-separated peer IDs to add to the session.
</ParamField>
<ParamField path="--metadata" type="string">
JSON metadata to associate with the session.
</ParamField>
</Accordion>
<Accordion title="delete">
Delete a session and all its data. Destructive — requires --yes or interactive confirm.
```bash
honcho session delete [<session_id>]
```
<ParamField path="session_id" type="string" />
<ParamField path="--yes" type="boolean">
Skip confirmation. Short alias: `-y`.
</ParamField>
</Accordion>
<Accordion title="get-metadata">
Get metadata for a session.
```bash
honcho session get-metadata [<session_id>]
```
<ParamField path="session_id" type="string" />
</Accordion>
<Accordion title="inspect">
Inspect a session: peers, message count, summaries, config.
```bash
honcho session inspect [<session_id>]
```
<ParamField path="session_id" type="string" />
</Accordion>
<Accordion title="list">
List sessions in the workspace.
```bash
honcho session list
```
<ParamField path="--peer" type="string">
Filter by peer. Short alias: `-p`.
</ParamField>
</Accordion>
<Accordion title="peers">
List peers in a session.
```bash
honcho session peers [<session_id>]
```
<ParamField path="session_id" type="string" />
</Accordion>
<Accordion title="remove-peers">
Remove peers from a session.
```bash
honcho session remove-peers <session_id> <peer_ids>
```
<ParamField path="session_id" type="string" required />
<ParamField path="peer_ids" type="string" required />
</Accordion>
<Accordion title="representation">
Get the representation of a peer within a session.
```bash
honcho session representation <peer_id> [<session_id>]
```
<ParamField path="peer_id" type="string" required />
<ParamField path="session_id" type="string" />
<ParamField path="--target" type="string">
Target peer (what peer_id knows about target).
</ParamField>
<ParamField path="--search-query" type="string">
Semantic search query to filter conclusions.
</ParamField>
<ParamField path="--max-conclusions" type="number">
Maximum number of conclusions to include.
</ParamField>
</Accordion>
<Accordion title="search">
Search messages in a session.
```bash
honcho session search <query> [<session_id>]
```
<ParamField path="query" type="string" required />
<ParamField path="session_id" type="string" />
<ParamField path="--limit" type="number" default="10">
Max results.
</ParamField>
</Accordion>
<Accordion title="set-metadata">
Set metadata for a session.
```bash
honcho session set-metadata [<session_id>]
```
<ParamField path="session_id" type="string" />
<ParamField path="--data" type="string" required>
JSON metadata to set (e.g. '\{"key": "value"\}'). Short alias: `-d`.
</ParamField>
</Accordion>
<Accordion title="summaries">
Get session summaries (short + long).
```bash
honcho session summaries [<session_id>]
```
<ParamField path="session_id" type="string" />
</Accordion>
<Accordion title="view">
View a session transcript as a chat log.
Modes (pick one):
- default / --last N: tail of the conversation (most recent N)
- --page N [--size M]: page through the full transcript
- --all: every message
Paging follows the requested order: --page 1 starts at the oldest message,
or the newest with --reverse.
Human mode prints a row-delimited table. JSON mode emits the message list
(same shape as message list).
```bash
honcho session view [<session_id>]
```
<ParamField path="session_id" type="string" />
<ParamField path="--last" type="number">
Show only the N most recent messages (default when no --page/--all: 50).
</ParamField>
<ParamField path="--page" type="number">
1-indexed page of the full transcript. Use for page 2+.
</ParamField>
<ParamField path="--size" type="number">
Messages per page; requires --page (1-100, default: 50).
</ParamField>
<ParamField path="--all" type="boolean">
Show the full transcript (every page).
</ParamField>
<ParamField path="--reverse" type="boolean">
Newest first (default is chronological: oldest at top).
</ParamField>
<ParamField path="--ids" type="boolean">
Include message IDs in the transcript.
</ParamField>
<ParamField path="--peer" type="string">
Filter by peer ID. Short alias: `-p`.
</ParamField>
</Accordion>
</AccordionGroup>
## honcho start
Start a local Honcho stack (API, deriver, Postgres, Redis).
Requires Docker. Uses cloud LLM providers. Does not change the CLI's
configured server URL — pass HONCHO_BASE_URL to talk to this stack.
``--setup basic`` or ``--setup advanced`` runs an interactive config wizard.
```bash
honcho start
```
<ParamField path="--profile" type="string" default="local">
Local stack profile name.
</ParamField>
<ParamField path="--api-port" type="string">
Host port for the API.
</ParamField>
<ParamField path="--db-port" type="string">
Host port for Postgres.
</ParamField>
<ParamField path="--redis-port" type="string">
Host port for Redis.
</ParamField>
<ParamField path="--setup" type="string">
Interactive config wizard: basic (provider/model) or advanced (embeddings, deriver, dialectic, dreams, flush).
</ParamField>
<ParamField path="--image" type="string">
Honcho image to pull and pin by digest (default: ghcr.io/plastic-labs/honcho:latest).
</ParamField>
<ParamField path="--timeout" type="string" default="180">
Seconds to wait for /health after compose up.
</ParamField>
## honcho status
Show local stack endpoints and container health.
With no ``--profile``, lists every stack under ``~/.honcho/profiles/``.
```bash
honcho status
```
<ParamField path="--profile" type="string">
Limit to this profile. Omit to show every local stack.
</ParamField>
## honcho stop
Stop the local stack started by `honcho start`. Keeps data unless --wipe.
```bash
honcho stop
```
<ParamField path="--profile" type="string" default="local">
Local stack profile name.
</ParamField>
<ParamField path="--wipe" type="boolean">
Also delete volumes (Postgres data).
</ParamField>
## honcho workspace
List, create, inspect, delete, and search workspaces.
<AccordionGroup>
<Accordion title="create">
Create or get a workspace.
```bash
honcho workspace create <workspace_id>
```
<ParamField path="workspace_id" type="string" required />
<ParamField path="--metadata" type="string">
JSON metadata to associate with the workspace.
</ParamField>
</Accordion>
<Accordion title="delete">
Delete a workspace. Use --dry-run first to see what will be deleted.
Requires --yes to skip confirmation, or will prompt interactively.
If sessions exist, requires --cascade to delete them first.
```bash
honcho workspace delete <workspace_id>
```
<ParamField path="workspace_id" type="string" required />
<ParamField path="--yes" type="boolean">
Skip confirmation prompt (for scripted/agent use). Short alias: `-y`.
</ParamField>
<ParamField path="--cascade" type="boolean">
Delete all sessions before deleting the workspace.
</ParamField>
<ParamField path="--dry-run" type="boolean">
Show what would be deleted without deleting.
</ParamField>
</Accordion>
<Accordion title="inspect">
Inspect a workspace: peers, sessions, config.
```bash
honcho workspace inspect [<workspace_id>]
```
<ParamField path="workspace_id" type="string" />
</Accordion>
<Accordion title="list">
List all accessible workspaces.
```bash
honcho workspace list
```
</Accordion>
<Accordion title="queue-status">
Get queue processing status.
```bash
honcho workspace queue-status
```
<ParamField path="--observer" type="string">
Filter by observer peer.
</ParamField>
<ParamField path="--sender" type="string">
Filter by sender peer.
</ParamField>
</Accordion>
<Accordion title="search">
Search messages across workspace.
```bash
honcho workspace search <query>
```
<ParamField path="query" type="string" required />
<ParamField path="--limit" type="number" default="10">
Max results.
</ParamField>
</Accordion>
</AccordionGroup>

View File

@ -11,7 +11,7 @@ indicate a feature or bug fix you are working on.
Once you have finished your contribution make a PR , and it will be reviewed by Once you have finished your contribution make a PR , and it will be reviewed by
a project manager. Feel free to join us in our a project manager. Feel free to join us in our
[discord](http://discord.gg/plasticlabs) to discuss your changes or get help. [discord](http://discord.gg/honcho) to discuss your changes or get help.
Your changes will undergo a period of testing and discussion before finally Your changes will undergo a period of testing and discussion before finally
being entered into the `main` branch and being staged for release. For more being entered into the `main` branch and being staged for release. For more

View File

@ -59,4 +59,4 @@ Finally, Claude needs instructions on how to use Honcho. The Desktop app doesn't
<Note>Be sure to update the \<app_name\> and \<user_name\> variables in the instructions.txt file.</Note> <Note>Be sure to update the \<app_name\> and \<user_name\> variables in the instructions.txt file.</Note>
Claude should then query for insights before responding and write your messages to storage! If you come up with more creative ways to get Claude to manage its own memory with Honcho, feel free to [let us know](https://discord.gg/plasticlabs) or make a PR on this [repo](https://github.com/plastic-labs/honcho-mcp/tree/main)! Claude should then query for insights before responding and write your messages to storage! If you come up with more creative ways to get Claude to manage its own memory with Honcho, feel free to [let us know](https://discord.gg/honcho) or make a PR on this [repo](https://github.com/plastic-labs/honcho-mcp/tree/main)!

View File

@ -0,0 +1,3 @@
---
openapi: post /v2/workspaces/{workspace_id}/observations
---

View File

@ -0,0 +1,3 @@
---
openapi: delete /v2/workspaces/{workspace_id}/observations/{observation_id}
---

View File

@ -0,0 +1,3 @@
---
openapi: post /v2/workspaces/{workspace_id}/observations/list
---

View File

@ -0,0 +1,3 @@
---
openapi: post /v2/workspaces/{workspace_id}/observations/query
---

View File

@ -0,0 +1,3 @@
---
openapi: get /v2/workspaces/{workspace_id}/peers/{peer_id}/context
---

View File

@ -0,0 +1,3 @@
---
openapi: put /v2/workspaces/{workspace_id}/peers/{peer_id}/card
---

View File

@ -0,0 +1,3 @@
---
openapi: post /v2/workspaces/{workspace_id}/trigger_dream
---

View File

@ -46,14 +46,19 @@ cp config.toml.example config.toml
Then modify the values as needed. The TOML file is organized into sections: Then modify the values as needed. The TOML file is organized into sections:
- `[app]` - Application-level settings (log level, host, port, embedding settings) - `[app]` - Application-level settings (log level, session limits, embedding settings, Langfuse integration, local metrics collection)
- `[db]` - Database connection and pool settings - `[db]` - Database connection and pool settings (connection URI, pool size, timeouts, connection recycling)
- `[auth]` - Authentication configuration - `[auth]` - Authentication configuration (enable/disable auth, JWT secret)
- `[llm]` - LLM provider API keys and general settings - `[cache]` - Redis cache configuration (enable/disable caching, Redis URL, TTL settings, lock configuration for cache stampede prevention)
- `[dialectic]` - Dialectic API configuration (provider, model, search settings) - `[llm]` - LLM provider API keys (Anthropic, OpenAI, Gemini, Groq, OpenAI-compatible endpoints) and general LLM settings
- `[deriver]` - Background worker settings and theory of mind configuration - `[dialectic]` - Dialectic API configuration (provider, model, query generation settings, semantic search parameters, context window size)
- `[summary]` - Session summarization settings - `[deriver]` - Background worker settings (worker count, polling intervals, queue management) and theory of mind configuration (model, tokens, observation limits)
- `[sentry]` - Error tracking and monitoring settings - `[peer_card]` - Peer card generation settings (provider, model, token limits)
- `[summary]` - Session summarization settings (frequency thresholds, provider, model, token limits for short and long summaries)
- `[dream]` - Dream processing configuration (enable/disable, thresholds, idle timeouts, dream types, LLM settings)
- `[webhook]` - Webhook configuration (webhook secret, workspace limits)
- `[metrics]` - Metrics collection settings (enable/disable metrics, namespace)
- `[sentry]` - Error tracking and monitoring settings (enable/disable, DSN, environment, sample rates)
### Using Environment Variables ### Using Environment Variables
@ -91,14 +96,14 @@ If you have this in `config.toml`:
```toml ```toml
[db] [db]
CONNECTION_URI = "postgresql://localhost/honcho_dev" CONNECTION_URI = "postgresql+psycopg://localhost/honcho_dev"
POOL_SIZE = 10 POOL_SIZE = 10
``` ```
You can override just the connection URI in production: You can override just the connection URI in production:
```bash ```bash
export DB_CONNECTION_URI="postgresql://prod-server/honcho_prod" export DB_CONNECTION_URI="postgresql+psycopg://prod-server/honcho_prod"
``` ```
The application will use the production connection URI while keeping the pool size from config.toml. The application will use the production connection URI while keeping the pool size from config.toml.
@ -107,28 +112,33 @@ The application will use the production connection URI while keeping the pool si
### Application Settings ### Application Settings
Application-level settings control core behavior of the Honcho server including logging, session limits, message handling, and optional integrations.
**Basic Application Configuration:** **Basic Application Configuration:**
```bash ```bash
# Logging and server settings # Logging and server settings
LOG_LEVEL=INFO # DEBUG, INFO, WARNING, ERROR LOG_LEVEL=INFO # DEBUG, INFO, WARNING, ERROR, CRITICAL
SESSION_PEERS_LIMIT=10
GET_CONTEXT_MAX_TOKENS=100000
# Embedding settings (optional) # Session and context limits
EMBED_MESSAGES=false SESSION_OBSERVERS_LIMIT=10 # Maximum number of observers per session
MAX_EMBEDDING_TOKENS=8192 GET_CONTEXT_MAX_TOKENS=100000 # Maximum tokens for context retrieval
MAX_EMBEDDING_TOKENS_PER_REQUEST=300000 MAX_MESSAGE_SIZE=25000 # Maximum message size in characters
# Embedding settings
EMBED_MESSAGES=true # Enable vector embeddings for messages
MAX_EMBEDDING_TOKENS=8192 # Maximum tokens per embedding
MAX_EMBEDDING_TOKENS_PER_REQUEST=300000 # Batch embedding limit
``` ```
**Environment-specific settings:** **Optional Integrations:**
```bash ```bash
# Development # Langfuse integration for LLM observability
LOG_LEVEL=DEBUG LANGFUSE_HOST=https://cloud.langfuse.com
FASTAPI_HOST=127.0.0.1 LANGFUSE_PUBLIC_KEY=your-langfuse-public-key
# Production # Local metrics collection
LOG_LEVEL=WARNING COLLECT_METRICS_LOCAL=false
FASTAPI_HOST=0.0.0.0 LOCAL_METRICS_FILE=metrics.jsonl
``` ```
### Database Configuration ### Database Configuration
@ -139,7 +149,7 @@ FASTAPI_HOST=0.0.0.0
DB_CONNECTION_URI=postgresql+psycopg://username:password@host:port/database DB_CONNECTION_URI=postgresql+psycopg://username:password@host:port/database
# Example for local development # Example for local development
DB_CONNECTION_URI=postgresql+psycopg://postgres:postgres@localhost:5432/honcho DB_CONNECTION_URI=postgresql+psycopg://postgres:postgres@localhost:5432/postgres
# Example for production # Example for production
DB_CONNECTION_URI=postgresql+psycopg://honcho_user:secure_password@db.example.com:5432/honcho_prod DB_CONNECTION_URI=postgresql+psycopg://honcho_user:secure_password@db.example.com:5432/honcho_prod
@ -151,7 +161,7 @@ DB_CONNECTION_URI=postgresql+psycopg://honcho_user:secure_password@db.example.co
DB_SCHEMA=public DB_SCHEMA=public
DB_POOL_SIZE=10 DB_POOL_SIZE=10
DB_MAX_OVERFLOW=20 DB_MAX_OVERFLOW=20
DB_POOL_TIMEOUT=30 DB_POOL_TIMEOUT=5
DB_POOL_RECYCLE=300 DB_POOL_RECYCLE=300
DB_POOL_PRE_PING=true DB_POOL_PRE_PING=true
DB_SQL_DEBUG=false DB_SQL_DEBUG=false
@ -196,6 +206,36 @@ AUTH_JWT_SECRET=your-super-secret-jwt-key
python scripts/generate_jwt_secret.py python scripts/generate_jwt_secret.py
``` ```
### Cache Configuration
Honcho supports Redis caching to improve performance by caching frequently accessed data like peers, sessions, and working representations. Caching also includes lock mechanisms to prevent cache stampede scenarios.
**Redis Cache Settings:**
```bash
# Enable/disable Redis caching
CACHE_ENABLED=false # Set to true to enable caching
# Redis connection
CACHE_URL=redis://localhost:6379/0?suppress=true
# Cache namespace and TTL
CACHE_NAMESPACE=honcho # Prefix for all cache keys
CACHE_DEFAULT_TTL_SECONDS=300 # How long items stay in cache (5 minutes)
# Lock settings for preventing cache stampede
CACHE_DEFAULT_LOCK_TTL_SECONDS=5 # Lock duration when fetching from DB on cache miss
```
**When to Enable Caching:**
- High-traffic production environments
- Applications with many repeated reads of the same data
- When you need to reduce database load
**Note:** Caching requires a Redis instance. You can run Redis locally with Docker:
```bash
docker run -d -p 6379:6379 redis:latest
```
## LLM Provider Configuration ## LLM Provider Configuration
Honcho supports multiple LLM providers for different tasks. API keys are configured in the `[llm]` section, while specific features use their own configuration sections. Honcho supports multiple LLM providers for different tasks. API keys are configured in the `[llm]` section, while specific features use their own configuration sections.
@ -221,6 +261,9 @@ LLM_OPENAI_COMPATIBLE_BASE_URL=https://your-openai-compatible-endpoint.com
```bash ```bash
# Default settings for all LLM calls # Default settings for all LLM calls
LLM_DEFAULT_MAX_TOKENS=2500 LLM_DEFAULT_MAX_TOKENS=2500
# Embedding provider (used when EMBED_MESSAGES=true)
LLM_EMBEDDING_PROVIDER=openai # Options: openai, gemini
``` ```
### Feature-Specific Model Configuration ### Feature-Specific Model Configuration
@ -228,67 +271,147 @@ LLM_DEFAULT_MAX_TOKENS=2500
Different features can use different providers and models: Different features can use different providers and models:
**Dialectic API:** **Dialectic API:**
The Dialectic API provides theory-of-mind informed responses by integrating long-term facts with current context.
```bash ```bash
# Main dialectic model (default: Anthropic) # Main dialectic model (default: Anthropic)
DIALECTIC_PROVIDER=anthropic DIALECTIC_PROVIDER=anthropic
DIALECTIC_MODEL=claude-sonnet-4-20250514 DIALECTIC_MODEL=claude-sonnet-4-20250514
DIALECTIC_MAX_OUTPUT_TOKENS=2500 DIALECTIC_MAX_OUTPUT_TOKENS=2500
DIALECTIC_THINKING_BUDGET_TOKENS=1024 DIALECTIC_THINKING_BUDGET_TOKENS=1024 # Only used with Anthropic provider
DIALECTIC_CONTEXT_WINDOW_SIZE=100000 # Maximum context window tokens
# Query generation for dialectic (default: Groq) # Query generation for dialectic searches
DIALECTIC_PERFORM_QUERY_GENERATION=false # Enable query generation for semantic search
DIALECTIC_QUERY_GENERATION_PROVIDER=groq DIALECTIC_QUERY_GENERATION_PROVIDER=groq
DIALECTIC_QUERY_GENERATION_MODEL=llama-3.1-8b-instant DIALECTIC_QUERY_GENERATION_MODEL=llama-3.1-8b-instant
# Semantic search settings # Semantic search settings
DIALECTIC_SEMANTIC_SEARCH_TOP_K=10 DIALECTIC_SEMANTIC_SEARCH_TOP_K=10 # Number of results to retrieve
DIALECTIC_SEMANTIC_SEARCH_MAX_DISTANCE=0.85 DIALECTIC_SEMANTIC_SEARCH_MAX_DISTANCE=0.85 # Maximum distance for relevance
``` ```
**Deriver:** **Deriver (Theory of Mind):**
The Deriver is a background processing system that extracts facts from messages and builds theory-of-mind representations of peers.
```bash ```bash
# Deriver model (default: Google) # LLM settings for deriver
DERIVER_PROVIDER=google DERIVER_PROVIDER=google
DERIVER_MODEL=gemini-2.0-flash-lite DERIVER_MODEL=gemini-2.5-flash-lite
DERIVER_MAX_OUTPUT_TOKENS=10000
DERIVER_THINKING_BUDGET_TOKENS=1024 # Only used with Anthropic provider
DERIVER_MAX_INPUT_TOKENS=23000 # Maximum input tokens for deriver
# Worker settings # Worker settings
DERIVER_WORKERS=1 DERIVER_WORKERS=1 # Number of background worker processes
DERIVER_STALE_SESSION_TIMEOUT_MINUTES=5 DERIVER_POLLING_SLEEP_INTERVAL_SECONDS=1.0 # Time between queue checks
DERIVER_POLLING_SLEEP_INTERVAL_SECONDS=1.0 DERIVER_STALE_SESSION_TIMEOUT_MINUTES=5 # Timeout for stale sessions
# Peer card settings # Queue management
DERIVER_PEER_CARD_PROVIDER=openai DERIVER_QUEUE_ERROR_RETENTION_SECONDS=2592000 # Keep errored items for 30 days
DERIVER_PEER_CARD_MODEL=gpt-5-nano-2025-08-07
DERIVER_PEER_CARD_MAX_OUTPUT_TOKENS=2000
# Maximum number of observations to store in working representation # Working representation settings
# This is applied to both explicit and deductive observations DERIVER_WORKING_REPRESENTATION_MAX_OBSERVATIONS=50 # Max observations stored
DERIVER_WORKING_REPRESENTATION_MAX_OBSERVATIONS=100 DERIVER_REPRESENTATION_BATCH_MAX_TOKENS=4096 # Max tokens per batch
```
**Peer Card:**
Peer cards are short, structured summaries of peer identity and characteristics.
```bash
# Enable/disable peer card generation
PEER_CARD_ENABLED=true
# LLM settings for peer card generation
PEER_CARD_PROVIDER=openai
PEER_CARD_MODEL=gpt-5-nano-2025-08-07
PEER_CARD_MAX_OUTPUT_TOKENS=4000 # Includes thinking tokens for GPT-5 models
``` ```
**Summary Generation:** **Summary Generation:**
```bash
# Summary model (default: Google)
SUMMARY_PROVIDER=google
SUMMARY_MODEL=gemini-1.5-flash-latest
SUMMARY_MAX_TOKENS_SHORT=1000
SUMMARY_MAX_TOKENS_LONG=2000
SUMMARY_THINKING_BUDGET_TOKENS=512
# Summary frequency Session summaries provide compressed context for long conversations. Honcho creates two types: short summaries (frequent) and long summaries (comprehensive).
SUMMARY_MESSAGES_PER_SHORT_SUMMARY=20
SUMMARY_MESSAGES_PER_LONG_SUMMARY=60 ```bash
# Enable/disable summarization
SUMMARY_ENABLED=true
# LLM settings for summary generation
SUMMARY_PROVIDER=openai
SUMMARY_MODEL=gpt-4o-mini-2024-07-18
SUMMARY_MAX_TOKENS_SHORT=1000 # Max tokens for short summaries
SUMMARY_MAX_TOKENS_LONG=4000 # Max tokens for long summaries
SUMMARY_THINKING_BUDGET_TOKENS=512 # Only used with Anthropic provider
# Summary frequency thresholds
SUMMARY_MESSAGES_PER_SHORT_SUMMARY=20 # Create short summary every N messages
SUMMARY_MESSAGES_PER_LONG_SUMMARY=60 # Create long summary every N messages
``` ```
### Default Provider Usage ### Default Provider Usage
By default, Honcho uses: By default, Honcho uses:
- **Anthropic** for dialectic API responses - **Anthropic** (Claude) for dialectic API responses
- **Groq** for query generation - **Groq** for query generation (fast, cost-effective)
- **Google** for deriving theory of mind and summarization - **Google** (Gemini) for theory of mind derivation
- **OpenAI** (GPT) for peer cards and summarization
- **OpenAI** for embeddings (if `EMBED_MESSAGES=true`) - **OpenAI** for embeddings (if `EMBED_MESSAGES=true`)
You only need to set the API keys for the providers you plan to use. You only need to set the API keys for the providers you plan to use. All providers are configurable per feature.
## Additional Features Configuration
### Dream Processing
Dream processing consolidates and refines peer representations during idle periods, similar to how human memory consolidation works during sleep.
**Dream Settings:**
```bash
# Enable/disable dream processing
DREAM_ENABLED=true
# Trigger thresholds
DREAM_DOCUMENT_THRESHOLD=50 # Minimum documents to trigger a dream
DREAM_IDLE_TIMEOUT_MINUTES=60 # Minutes of inactivity before dream can start
DREAM_MIN_HOURS_BETWEEN_DREAMS=8 # Minimum hours between dreams for a peer
# Dream types to enable
DREAM_ENABLED_TYPES=["consolidate"] # Currently supported: consolidate
# LLM settings for dream processing
DREAM_PROVIDER=openai
DREAM_MODEL=gpt-4o-mini-2024-07-18
DREAM_MAX_OUTPUT_TOKENS=2000
```
### Webhook Configuration
Webhooks allow you to receive real-time notifications when events occur in Honcho (e.g., new messages, session updates).
**Webhook Settings:**
```bash
# Webhook secret for signing payloads (optional but recommended)
WEBHOOK_SECRET=your-webhook-signing-secret
# Limit on webhooks per workspace
WEBHOOK_MAX_WORKSPACE_LIMIT=10
```
### Metrics Collection
Enable metrics collection for monitoring Honcho performance and usage.
**Metrics Settings:**
```bash
# Enable/disable metrics collection
METRICS_ENABLED=false
# Namespace for metrics (used in metric names)
METRICS_NAMESPACE=honcho
```
## Monitoring Configuration ## Monitoring Configuration
@ -296,13 +419,17 @@ You only need to set the API keys for the providers you plan to use.
**Sentry Settings:** **Sentry Settings:**
```bash ```bash
# Enable/disable Sentry # Enable/disable Sentry error tracking
SENTRY_ENABLED=false SENTRY_ENABLED=false
# Sentry configuration # Sentry configuration
SENTRY_DSN=https://your-sentry-dsn@sentry.io/project-id SENTRY_DSN=https://your-sentry-dsn@sentry.io/project-id
SENTRY_TRACES_SAMPLE_RATE=0.1 SENTRY_RELEASE=2.4.0 # Optional: track which version errors come from
SENTRY_PROFILES_SAMPLE_RATE=0.1 SENTRY_ENVIRONMENT=production # Environment name (development, staging, production)
# Sampling rates (0.0 to 1.0)
SENTRY_TRACES_SAMPLE_RATE=0.1 # 10% of transactions tracked
SENTRY_PROFILES_SAMPLE_RATE=0.1 # 10% of transactions profiled
``` ```
## Environment-Specific Examples ## Environment-Specific Examples
@ -313,7 +440,7 @@ SENTRY_PROFILES_SAMPLE_RATE=0.1
```toml ```toml
[app] [app]
LOG_LEVEL = "DEBUG" LOG_LEVEL = "DEBUG"
SESSION_PEERS_LIMIT = 10 SESSION_OBSERVERS_LIMIT = 10
EMBED_MESSAGES = false EMBED_MESSAGES = false
[db] [db]
@ -323,21 +450,40 @@ POOL_SIZE = 5
[auth] [auth]
USE_AUTH = false USE_AUTH = false
[cache]
ENABLED = false
[dialectic] [dialectic]
PROVIDER = "anthropic" PROVIDER = "anthropic"
MODEL = "claude-sonnet-4-20250514" MODEL = "claude-sonnet-4-20250514"
QUERY_GENERATION_PROVIDER = "groq" PERFORM_QUERY_GENERATION = false
QUERY_GENERATION_MODEL = "llama-3.1-8b-instant"
MAX_OUTPUT_TOKENS = 2500 MAX_OUTPUT_TOKENS = 2500
[summary]
PROVIDER = "google"
MODEL = "gemini-1.5-flash-latest"
MAX_TOKENS_SHORT = 1000
MAX_TOKENS_LONG = 2000
[deriver] [deriver]
WORKERS = 1 WORKERS = 1
PROVIDER = "google"
MODEL = "gemini-2.5-flash-lite"
[peer_card]
ENABLED = true
PROVIDER = "openai"
MODEL = "gpt-5-nano-2025-08-07"
[summary]
ENABLED = true
PROVIDER = "openai"
MODEL = "gpt-4o-mini-2024-07-18"
MAX_TOKENS_SHORT = 1000
MAX_TOKENS_LONG = 4000
[dream]
ENABLED = true
[webhook]
MAX_WORKSPACE_LIMIT = 10
[metrics]
ENABLED = false
[sentry] [sentry]
ENABLED = false ENABLED = false
@ -349,7 +495,12 @@ ENABLED = false
LOG_LEVEL=DEBUG LOG_LEVEL=DEBUG
DB_CONNECTION_URI=postgresql+psycopg://postgres:postgres@localhost:5432/honcho_dev DB_CONNECTION_URI=postgresql+psycopg://postgres:postgres@localhost:5432/honcho_dev
AUTH_USE_AUTH=false AUTH_USE_AUTH=false
ANTHROPIC_API_KEY=your-dev-anthropic-key CACHE_ENABLED=false
# LLM Provider API Keys
LLM_ANTHROPIC_API_KEY=your-dev-anthropic-key
LLM_OPENAI_API_KEY=your-dev-openai-key
LLM_GEMINI_API_KEY=your-dev-gemini-key
``` ```
### Production Configuration ### Production Configuration
@ -358,7 +509,7 @@ ANTHROPIC_API_KEY=your-dev-anthropic-key
```toml ```toml
[app] [app]
LOG_LEVEL = "WARNING" LOG_LEVEL = "WARNING"
SESSION_PEERS_LIMIT = 10 SESSION_OBSERVERS_LIMIT = 10
EMBED_MESSAGES = true EMBED_MESSAGES = true
[db] [db]
@ -369,27 +520,50 @@ MAX_OVERFLOW = 40
[auth] [auth]
USE_AUTH = true USE_AUTH = true
[cache]
ENABLED = true
URL = "redis://redis:6379/0"
DEFAULT_TTL_SECONDS = 300
[dialectic] [dialectic]
PROVIDER = "anthropic" PROVIDER = "anthropic"
MODEL = "claude-sonnet-4-20250514" MODEL = "claude-sonnet-4-20250514"
QUERY_GENERATION_PROVIDER = "groq" PERFORM_QUERY_GENERATION = false
QUERY_GENERATION_MODEL = "llama-3.1-8b-instant"
MAX_OUTPUT_TOKENS = 2500 MAX_OUTPUT_TOKENS = 2500
[summary]
PROVIDER = "google"
MODEL = "gemini-1.5-flash-latest"
MAX_TOKENS_SHORT = 1000
MAX_TOKENS_LONG = 2000
[deriver] [deriver]
WORKERS = 4 WORKERS = 4
PROVIDER = "google" PROVIDER = "google"
MODEL = "gemini-2.0-flash-lite" MODEL = "gemini-2.5-flash-lite"
[peer_card]
ENABLED = true
PROVIDER = "openai"
MODEL = "gpt-5-nano-2025-08-07"
[summary]
ENABLED = true
PROVIDER = "openai"
MODEL = "gpt-4o-mini-2024-07-18"
MAX_TOKENS_SHORT = 1000
MAX_TOKENS_LONG = 4000
[dream]
ENABLED = true
PROVIDER = "openai"
MODEL = "gpt-4o-mini-2024-07-18"
[webhook]
MAX_WORKSPACE_LIMIT = 10
[metrics]
ENABLED = true
[sentry] [sentry]
ENABLED = true ENABLED = true
ENVIRONMENT = "production"
TRACES_SAMPLE_RATE = 0.1 TRACES_SAMPLE_RATE = 0.1
PROFILES_SAMPLE_RATE = 0.1
``` ```
**Environment variables for production:** **Environment variables for production:**
@ -397,11 +571,27 @@ TRACES_SAMPLE_RATE = 0.1
# .env.production # .env.production
LOG_LEVEL=WARNING LOG_LEVEL=WARNING
DB_CONNECTION_URI=postgresql+psycopg://honcho_user:secure_password@prod-db:5432/honcho_prod DB_CONNECTION_URI=postgresql+psycopg://honcho_user:secure_password@prod-db:5432/honcho_prod
# Authentication
AUTH_USE_AUTH=true AUTH_USE_AUTH=true
AUTH_JWT_SECRET=your-super-secret-jwt-key AUTH_JWT_SECRET=your-super-secret-jwt-key
ANTHROPIC_API_KEY=your-prod-anthropic-key
GEMINI_API_KEY=your-prod-gemini-key # Cache
CACHE_ENABLED=true
CACHE_URL=redis://redis:6379/0
# LLM Provider API Keys
LLM_ANTHROPIC_API_KEY=your-prod-anthropic-key
LLM_OPENAI_API_KEY=your-prod-openai-key
LLM_GEMINI_API_KEY=your-prod-gemini-key
LLM_GROQ_API_KEY=your-prod-groq-key
# Webhooks
WEBHOOK_SECRET=your-webhook-signing-secret
# Monitoring
SENTRY_DSN=https://your-sentry-dsn@sentry.io/project-id SENTRY_DSN=https://your-sentry-dsn@sentry.io/project-id
SENTRY_ENVIRONMENT=production
``` ```
## Migration Management ## Migration Management

View File

@ -5,13 +5,31 @@ icon: 'handshake'
Thank you for your interest in contributing to Honcho! This guide outlines the process for contributing to the project and our development conventions. Thank you for your interest in contributing to Honcho! This guide outlines the process for contributing to the project and our development conventions.
## Before you write code
**Every pull request needs an issue, and that issue needs the `maintainer-approved` label.**
A pull request that is not linked to an approved issue gets labelled `needs-approved-issue`, with a comment explaining why. You then have 72 hours to link one before it is closed automatically. Reopening costs nothing once the link is in place. This is automated. We do this because an unreviewable backlog helps nobody: a PR against an unapproved issue is work you did that we may not be able to merge, no matter how good it is.
So, in order:
1. **Find approved work.** Browse [issues labelled `maintainer-approved`](https://github.com/plastic-labs/honcho/issues?q=is%3Aissue+is%3Aopen+label%3Amaintainer-approved). That label is the queue of things we have agreed should be built. Anything in it is fair game — comment on the issue to claim it.
2. **Or open an issue and get it approved.** Use the [issue templates](https://github.com/plastic-labs/honcho/issues/new/choose). Maintainers triage and apply the label.
3. **If you feel strongly about an issue, come to [Discord](https://discord.gg/honcho).** This is the fastest path by a wide margin. Maintainers are more active there than in the issue tracker, and a five-minute conversation about what you want to build usually resolves whether it fits before either side spends real time on it.
4. **Then open the PR** and link the issue — either `Fixes #123` in the description, or **Development → link an issue** in the sidebar. Both work.
Small exceptions we will not be pedantic about: fixing a typo, a broken link, or an obviously wrong code sample. Open the PR, explain it in one line, and we will sort out the issue linkage.
## Getting Started ## Getting Started
Before you start contributing, please: Before you start contributing, please:
1. **Set up your development environment** - Follow the [Local Development guide](https://github.com/plastic-labs/honcho/blob/main/CONTRIBUTING.md#local-development) in the Honcho repository to get Honcho running locally. 1. **Set up your development environment** - Follow the [Local Development guide](https://github.com/plastic-labs/honcho/blob/main/CONTRIBUTING.md#local-development) in the Honcho repository to get Honcho running locally.
2. **Join our community** - Feel free to join us in our [Discord](http://discord.gg/plasticlabs) to discuss your changes, get help, or ask questions. 2. **Join our community** - Feel free to join us in our [Discord](https://discord.gg/honcho) to discuss your changes, get help, or ask questions.
3. **Review existing issues** - Check the [issues tab](https://github.com/plastic-labs/honcho/issues) to see what's already being worked on or to find something to contribute to. 3. **Review existing issues** - Check the [issues tab](https://github.com/plastic-labs/honcho/issues) to see what's already being worked on or to find something to contribute to.
@ -94,7 +112,7 @@ git commit -m "docs(readme): update installation instructions"
3. Fill out the pull request template with: 3. Fill out the pull request template with:
- A clear description of what changes you've made - A clear description of what changes you've made
- The motivation for the changes - The motivation for the changes
- Any relevant issue numbers (use "Closes #123" to auto-close issues) - A link to the approved issue — `Fixes #123` in the description, or **Development → link an issue** in the sidebar. This is required; see [Before you write code](#before-you-write-code).
- Screenshots or examples if applicable - Screenshots or examples if applicable
## Coding Standards ## Coding Standards
@ -128,7 +146,7 @@ git commit -m "docs(readme): update installation instructions"
## Review Process ## Review Process
1. **Automated checks** - Your PR will run through automated checks including tests and linting 1. **Automated checks** - Your PR will run through automated checks including tests, linting, and the issue gate
2. **Project maintainer review** - A project maintainer will review your code for: 2. **Project maintainer review** - A project maintainer will review your code for:
- Code quality and adherence to standards - Code quality and adherence to standards
- Functionality and correctness - Functionality and correctness
@ -153,17 +171,21 @@ We welcome various types of contributions:
When reporting bugs or requesting features: When reporting bugs or requesting features:
1. Check if the issue already exists 1. Check if the issue already exists
2. Use the appropriate issue template 2. Use the appropriate [issue template](https://github.com/plastic-labs/honcho/issues/new/choose) (bug, memory/recall quality, feature, integration, or documentation)
3. Provide clear reproduction steps for bugs 3. Provide clear reproduction steps for bugs
4. Include relevant environment information 4. Include relevant environment information (managed vs self-hosted, server version, SDK)
5. Be specific about expected vs actual behavior 5. Be specific about expected vs actual behavior
6. Redact secrets, JWTs, and production user content
## Questions and Support ## Questions and Support
- **General questions** - Join our [Discord](http://discord.gg/plasticlabs) - **General questions** - Join our [Discord](https://discord.gg/honcho)
- **Bug reports** - Use GitHub issues - **Bug reports** - GitHub issues → Bug report template
- **Feature requests** - Use GitHub issues with the feature request template - **Memory / recall quality** - GitHub issues → Memory / recall quality template
- **Security issues** - Please email us privately rather than opening a public issue - **Feature requests** - GitHub issues → Feature request template
- **Integrations / plugins / app-store listings** - GitHub issues → Integration request template
- **Documentation issues** - GitHub issues → Documentation issue template
- **Security issues** - Report **privately** only — see [`SECURITY.md`](https://github.com/plastic-labs/honcho/blob/main/SECURITY.md) (GitHub Private Vulnerability Reporting or email). Do not open a public issue.
## License ## License

View File

@ -59,7 +59,8 @@ OPENAI_API_KEY=your-openai-api-key
ANTHROPIC_API_KEY=your-anthropic-api-key ANTHROPIC_API_KEY=your-anthropic-api-key
# Database will be created automatically by Docker # Database will be created automatically by Docker
DB_CONNECTION_URI=postgresql+psycopg://postgres:postgres@database:5432/honcho DB_CONNECTION_URI=postgresql+psycopg://postgres:postgres@database:5432/postgres
# Disable auth for local development # Disable auth for local development
AUTH_USE_AUTH=false AUTH_USE_AUTH=false
@ -134,24 +135,21 @@ Download from [postgresql.org](https://www.postgresql.org/download/windows/)
```bash ```bash
docker run --name honcho-db \ docker run --name honcho-db \
-e POSTGRES_DB=honcho \
-e POSTGRES_USER=postgres \ -e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres \ -e POSTGRES_PASSWORD=postgres \
-p 5432:5432 \ -p 5432:5432 \
-d pgvector/pgvector:pg15 -d pgvector/pgvector:pg15
``` ```
### 3. Create Database and Enable Extensions ### 3. Enable Extensions
Connect to PostgreSQL and set up the database: Connect to PostgreSQL and enable pgvector:
```bash ```bash
# Connect to PostgreSQL # Connect to PostgreSQL
psql -U postgres psql -U postgres
# Create database and enable extensions # Enable extensions on the default database
CREATE DATABASE honcho;
\c honcho
CREATE EXTENSION IF NOT EXISTS vector; CREATE EXTENSION IF NOT EXISTS vector;
CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE EXTENSION IF NOT EXISTS pg_trgm;
\q \q
@ -169,7 +167,7 @@ Edit `.env` with your configuration:
```bash ```bash
# Database connection # Database connection
DB_CONNECTION_URI=postgresql+psycopg://postgres:postgres@localhost:5432/honcho DB_CONNECTION_URI=postgresql+psycopg://postgres:postgres@localhost:5432/postgres
# Optional API keys (required for LLM features) # Optional API keys (required for LLM features)
OPENAI_API_KEY=your-openai-api-key OPENAI_API_KEY=your-openai-api-key
@ -279,7 +277,7 @@ const client = new Honcho({
- **Explore the API**: Check out the [API Reference](/v2/api-reference/introduction) - **Explore the API**: Check out the [API Reference](/v2/api-reference/introduction)
- **Try the SDKs**: See our [guides](/v2/guides) for examples - **Try the SDKs**: See our [guides](/v2/guides) for examples
- **Configure Honcho**: Visit the [Configuration Guide](./configuration) for detailed settings - **Configure Honcho**: Visit the [Configuration Guide](./configuration) for detailed settings
- **Join the community**: [Discord](https://discord.gg/plasticlabs) - **Join the community**: [Discord](https://discord.gg/honcho)
## Troubleshooting ## Troubleshooting
@ -310,7 +308,7 @@ const client = new Honcho({
### Getting Help ### Getting Help
- **GitHub Issues**: [Report bugs](https://github.com/plastic-labs/honcho/issues) - **GitHub Issues**: [Report bugs](https://github.com/plastic-labs/honcho/issues)
- **Discord**: [Join our community](https://discord.gg/plasticlabs) - **Discord**: [Join our community](https://discord.gg/honcho)
- **Documentation**: Check the [Configuration Guide](./configuration) for detailed settings - **Documentation**: Check the [Configuration Guide](./configuration) for detailed settings
## Production Considerations ## Production Considerations

View File

@ -7,17 +7,15 @@ sidebarTitle: "Architecture"
<Note> The goal of this page is to build an intuition for the primitives in Honcho and how they fit together </Note> <Note> The goal of this page is to build an intuition for the primitives in Honcho and how they fit together </Note>
Honcho has 3 main components that work together to manage agent identity and context. Honcho has 2 main components that work together to manage agent identity and context.
- **The Storage API**: The Memory layer for storing interaction history for your agents - **The Memory Layer**: The Memory layer for storing interaction history for your agents
- **The Deriver**: The background processing layer that builds representations of users and agents - **The Reasoning Layer**: The background processing layer that builds representations of users and agents
- **The Dialectic API**: The natural language API for chatting with representations
Below we'll deep dive into these different areas, discussing the data Below we'll deep dive into these different areas, discussing the data
primitives, the flow of data through the system, artifacts Honcho produces, and primitives, the flow of data through the system, artifacts Honcho produces, and
how to use them. how to use them.
## Data Model ## Data Model
Honcho has a hierarchical data model centered around the entities below. Honcho has a hierarchical data model centered around the entities below.
@ -37,9 +35,9 @@ Honcho has a hierarchical data model centered around the entities below.
style SM fill:#e8f5e9,stroke:#2e7d32,color:#000 style SM fill:#e8f5e9,stroke:#2e7d32,color:#000
``` ```
There are `Workspaces` at the top that contain `Peers` and `Sessions`. A `Peer` - A `Workspaces` has `Peers` & `Sessions`
can be part of many `Sessions` and a `Session` can have many `Peers`. `Sessions` - A `Peer` can be in multiple `Sessions` and can send `Messages` in a `Session`.
hold messages that are sent by `Peers`. - A `Session` can have many `Peers` and stores `Messages` sent by its `Peers`.
### <Icon icon="building" /> Workspaces ### <Icon icon="building" /> Workspaces
@ -126,19 +124,38 @@ with a single peer and structure the data as messages.
- File uploads (PDFs, text files, JSON documents) - File uploads (PDFs, text files, JSON documents)
## Deriver ## Reasoning Layer
At the core of developing representations of Peers, we have the Deriver. The The raw data you store in Honcho is useful, but it's not in a format that's most
Deriver refers to a set of processes in Honcho that enqueue new messages sent useful for an LLM to consume. There may be too many tokens that need to be
by peers and reasons over them to extract facts, insights, and context. compacted, key facts about what happened may be hard to piece together because
they involve messages from across different sessions, etc.
To solve this problem, Honcho has a reasoning layer that continually processes
incoming data to form the most informationally dense and useful representations of `Peers`
that we can then expose to agents. Honcho does the following tasks in
the reasoning engine.
- **Fact Derivation**
- **Generate Summaries**
- **Generate Peer Cards**
- **Dreaming**
Honcho will reason about each `Message` it
ingests to generate new facts and insights that are spelled out and easy to
consume in an LLM prompt.
We refer to this module of Honcho as the `Deriver`, because it's constantly
deriving new insights from messages. The sum total of all these generated
insights are what we refer to as a `Representation`, all the data related to who
and what a `Peer` is.
Depending on the configuration of a `Peer` or `Session`, the deriver will behave Depending on the configuration of a `Peer` or `Session`, the deriver will behave
differently and update different representations. differently and update different representations.
Facts derived here are used in the Dialectic chat endpoint to generate Facts derived here are used in the Dialectic chat endpoint, get_context
context-aware responses that can correctly reference both concrete facts endpoint,
extracted from messages and social insights deduced from facts, tone, and
opinion.
<Info> <Info>
Deriver tasks are processed in parallel, but tasks affecting the same peer representation will always be processed serially in order of message creation, so as to properly understand their cumulative effect. Deriver tasks are processed in parallel, but tasks affecting the same peer representation will always be processed serially in order of message creation, so as to properly understand their cumulative effect.
@ -149,7 +166,7 @@ There are two types of tasks that the deriver currently does:
- **Representation Tasks**: Generate/update peer representations - **Representation Tasks**: Generate/update peer representations
- **Summary Tasks**: Generate conversation summaries - **Summary Tasks**: Generate conversation summaries
### Peer Representations ### Local & Global Representations
Peer representations are more of an abstract concept, as they are made up of Peer representations are more of an abstract concept, as they are made up of
various pieces of data stored throughout Honcho. There are however various pieces of data stored throughout Honcho. There are however

View File

@ -1,14 +1,144 @@
--- ---
title: 'Configuration' title: 'Configure Reasoning'
description: 'Customizing how Honcho handles peers and sessions' description: 'Customizing how Honcho handles peers, sessions, and messages'
icon: 'wrench' icon: 'wrench'
--- ---
Entities in Honcho can sometimes be configured to change the behavior of the deriver, which is responsible for generating and storing facts, summaries, and user representations. Honcho's reasoning engine (the "deriver") can be configured at multiple levels to control how it processes messages, generates facts, creates summaries, and builds peer representations.
These configurations can be set at the peer, session, and session-peer level (AKA the state of a peer within a specific session). Configuration follows a hierarchy: **message > session > workspace > global defaults**. Settings at lower levels override those at higher levels, giving you fine-grained control over behavior.
### Peer Configuration ## Configuration Hierarchy
Honcho uses a hierarchical configuration system where more specific settings override more general ones:
1. **Global Defaults**: Built-in system defaults
2. **Workspace Configuration**: Settings that apply to all sessions in a workspace
3. **Session Configuration**: Settings that apply to all messages in a session
4. **Message Configuration**: Settings that apply to a specific message
<Info>
All configuration fields are optional. If not specified, the value is inherited from the next level up in the hierarchy.
</Info>
## Configuration Options
### Deriver Configuration
Controls the core reasoning engine that extracts facts and insights from messages.
| Field | Type | Description |
|-------|------|-------------|
| `enabled` | `bool` | Whether to enable deriver functionality. When disabled, no facts or representations are generated. |
<CodeGroup>
```python Python
from honcho import Honcho
honcho = Honcho()
# Disable deriver at session level
session = honcho.session("private-session", config={
"deriver": {"enabled": False}
})
```
```typescript TypeScript
import { Honcho } from "@honcho-ai/sdk";
const honcho = new Honcho({});
// Disable deriver at session level
const session = await honcho.session("private-session", {
config: {
deriver: { enabled: false }
}
});
```
</CodeGroup>
### Peer Card Configuration
Controls how peer cards (concise summaries of what's known about a peer) are generated and used.
| Field | Type | Description |
|-------|------|-------------|
| `use` | `bool` | Whether to use peer cards during the deriver process. |
| `create` | `bool` | Whether to generate peer cards based on message content. |
<CodeGroup>
```python Python
# Disable peer card generation but still use existing cards
session = honcho.session("my-session", config={
"peer_card": {"create": False, "use": True}
})
```
```typescript TypeScript
// Disable peer card generation but still use existing cards
const session = await honcho.session("my-session", {
config: {
peer_card: { create: false, use: true }
}
});
```
</CodeGroup>
### Summary Configuration
Controls automatic conversation summarization. Available at workspace and session levels only.
| Field | Type | Description |
|-------|------|-------------|
| `enabled` | `bool` | Whether to enable summary functionality. |
| `messages_per_short_summary` | `int` | Number of messages between short summaries. Must be ≥ 10. |
| `messages_per_long_summary` | `int` | Number of messages between long summaries. Must be ≥ 20 and greater than `messages_per_short_summary`. |
<CodeGroup>
```python Python
# Customize summary frequency
session = honcho.session("verbose-session", config={
"summary": {
"enabled": True,
"messages_per_short_summary": 15,
"messages_per_long_summary": 45
}
})
```
```typescript TypeScript
// Customize summary frequency
const session = await honcho.session("verbose-session", {
config: {
summary: {
enabled: true,
messages_per_short_summary: 15,
messages_per_long_summary: 45
}
}
});
```
</CodeGroup>
### Dream Configuration
Controls the "dreaming" process that consolidates and refines representations. Available at workspace and session levels only.
| Field | Type | Description |
|-------|------|-------------|
| `enabled` | `bool` | Whether to enable dream functionality. Automatically disabled if deriver is disabled. |
<CodeGroup>
```python Python
# Disable dreams for a workspace
# (done via API when creating/updating workspace)
```
```typescript TypeScript
// Disable dreams for a workspace
// (done via API when creating/updating workspace)
```
</CodeGroup>
---
## Peer Configuration
By default, all peers are "observed" by Honcho. This means that Honcho will derive facts from messages sent by the peer and generate a representation of them. In most cases, this is why you use Honcho! However, sometimes an application requires a peer that should not be observed: for example, an assistant or game NPC that your program will never need to ask questions about. By default, all peers are "observed" by Honcho. This means that Honcho will derive facts from messages sent by the peer and generate a representation of them. In most cases, this is why you use Honcho! However, sometimes an application requires a peer that should not be observed: for example, an assistant or game NPC that your program will never need to ask questions about.
@ -27,7 +157,7 @@ honcho = Honcho()
peer = honcho.peer("my-peer", config={"observe_me": False}) peer = honcho.peer("my-peer", config={"observe_me": False})
# Change peer's configuration # Change peer's configuration
peer.set_peer_config({"observe_me": True}) peer.set_config({"observe_me": True})
# Note: creating the same peer again will also replace the configuration # Note: creating the same peer again will also replace the configuration
peer = honcho.peer("my-peer", config={"observe_me": False}) peer = honcho.peer("my-peer", config={"observe_me": False})
@ -43,7 +173,7 @@ import { Honcho } from "@honcho-ai/sdk";
const peer = await honcho.peer("my-peer", { config: { observe_me: false } }); const peer = await honcho.peer("my-peer", { config: { observe_me: false } });
// Change peer's configuration // Change peer's configuration
await peer.setPeerConfig({ observe_me: true }); await peer.setConfig({ observe_me: true });
// Note: creating the same peer again will also replace the configuration // Note: creating the same peer again will also replace the configuration
await honcho.peer("my-peer", { config: { observe_me: false } }); await honcho.peer("my-peer", { config: { observe_me: false } });
@ -51,9 +181,9 @@ import { Honcho } from "@honcho-ai/sdk";
``` ```
</CodeGroup> </CodeGroup>
### Session Configuration ## Session Configuration
By default, all sessions have the deriver enabled, much like peers. You may create a session that escapes the deriver's watchful eye by setting the `deriver_disabled` flag to `true`. You can update the flag by calling `get_or_create` on the session with a new configuration. Sessions support the full configuration schema. You can disable the deriver entirely for a session, customize summary behavior, or adjust peer card settings.
<CodeGroup> <CodeGroup>
```python Python ```python Python
@ -62,8 +192,18 @@ from honcho import Honcho
# Initialize client # Initialize client
honcho = Honcho() honcho = Honcho()
# Create session with configuration # Create session with deriver disabled
session = honcho.session("my-session", config={"deriver_disabled": True}) session = honcho.session("my-session", config={
"deriver": {"enabled": False}
})
# Create session with custom summary settings
session = honcho.session("detailed-session", config={
"summary": {
"messages_per_short_summary": 10,
"messages_per_long_summary": 30
}
})
``` ```
```typescript TypeScript ```typescript TypeScript
import { Honcho } from "@honcho-ai/sdk"; import { Honcho } from "@honcho-ai/sdk";
@ -72,15 +212,73 @@ import { Honcho } from "@honcho-ai/sdk";
// Initialize client // Initialize client
const honcho = new Honcho({}); const honcho = new Honcho({});
// Create session with configuration // Create session with deriver disabled
const session = await honcho.session("my-session", { config: { deriver_disabled: true } }); const session = await honcho.session("my-session", {
config: { deriver: { enabled: false } }
});
// Create session with custom summary settings
const detailedSession = await honcho.session("detailed-session", {
config: {
summary: {
messages_per_short_summary: 10,
messages_per_long_summary: 30
}
}
});
})(); })();
``` ```
</CodeGroup> </CodeGroup>
### Session-Peer Configuration ## Message Configuration
Configuration at the session-peer level is the most common use case for configuration flags. You will often want to arrange a session such that certain peers observe others in order to form "local representations" of them. There are two flags that can be set at the session-peer level: Individual messages can override session and workspace configuration for fine-grained control. This is useful for excluding specific messages from processing or adjusting behavior on a per-message basis.
<CodeGroup>
```python Python
from honcho import Honcho
honcho = Honcho()
session = honcho.session("my-session")
user = honcho.peer("user")
# Create a message that skips deriver processing
session.add_messages([
user.message("This message won't be analyzed", config={
"deriver": {"enabled": False}
})
])
# Create a message with custom peer card settings
session.add_messages([
user.message("Use existing card but don't update it", config={
"peer_card": {"use": True, "create": False}
})
])
```
```typescript TypeScript
import { Honcho } from "@honcho-ai/sdk";
(async () => {
const honcho = new Honcho({});
const session = await honcho.session("my-session");
const user = await honcho.peer("user");
// Create a message that skips deriver processing
await session.addMessages([
user.message("This message won't be analyzed", {
configuration: { deriver: { enabled: false } }
})
]);
})();
```
</CodeGroup>
## Session-Peer Configuration
Configuration at the session-peer level controls how peers observe each other within a specific session. This is the most common use case for enabling "local representations" — where one peer forms a model of another peer based only on what they observe in that session.
There are two flags that can be set at the session-peer level:
- `observe_me`: Whether this peer should *be observed* by others in the session. By default, this is `true`. This overrides the peer-level `observe_me` flag. - `observe_me`: Whether this peer should *be observed* by others in the session. By default, this is `true`. This overrides the peer-level `observe_me` flag.
@ -96,7 +294,7 @@ You can dynamically change the configuration of a session-peer by calling `set_p
<CodeGroup> <CodeGroup>
```python Python ```python Python
from honcho import Honcho from honcho import Honcho, SessionPeerConfig
# Initialize client # Initialize client
honcho = Honcho() honcho = Honcho()
@ -113,11 +311,11 @@ session.add_peers([alice, bob])
# Add another peer to the session with a custom configuration # Add another peer to the session with a custom configuration
charlie = honcho.peer("charlie") charlie = honcho.peer("charlie")
session.add_peers([charlie, {"observe_me": False, "observe_others": True}]) session.add_peers([(charlie, SessionPeerConfig(observe_me=False, observe_others=True))])
# Set session-peer configuration # Set session-peer configuration
session.set_peer_config(alice, {"observe_others": True}) session.set_peer_config(alice, SessionPeerConfig(observe_others=True))
session.set_peer_config(bob, {"observe_me": False}) session.set_peer_config(bob, SessionPeerConfig(observe_me=False))
# Get session-peer configuration # Get session-peer configuration
charlie_config = session.get_peer_config(charlie) charlie_config = session.get_peer_config(charlie)
@ -142,7 +340,7 @@ import { Honcho } from "@honcho-ai/sdk";
// Add another peer to the session with a custom configuration // Add another peer to the session with a custom configuration
const charlie = await honcho.peer("charlie"); const charlie = await honcho.peer("charlie");
await session.addPeers([charlie, { observe_me: false, observe_others: true }]); await session.addPeers([[charlie, { observe_me: false, observe_others: true }]]);
// Set session-peer configuration // Set session-peer configuration
await session.setPeerConfig(alice, { observe_others: true }); await session.setPeerConfig(alice, { observe_others: true });
@ -154,3 +352,59 @@ import { Honcho } from "@honcho-ai/sdk";
})(); })();
``` ```
</CodeGroup> </CodeGroup>
### Observation and Peer Join Order
Reasoning tasks are scheduled at the time a message is created, based on which peers are in the session **at that moment**. Honcho does not retroactively schedule reasoning for peers that join later.
This means:
- If Peer C joins a session **after** messages from Peer A and Peer B have already been sent, Peer C will **not** receive reasoning tasks for those earlier messages—even if Peer C has `observe_others` enabled.
- Peer C will only begin observing new messages sent after they join the session.
- Similarly, if a peer leaves a session, they stop being included as an observer for any messages sent after their departure.
<Warning>
There is no retroactive reasoning. If your application needs an observer peer to reason about prior conversation history, add the peer to the session **before** messages are sent. Alternatively use the .chat() endpoint to include the conversation history in the agent's context, regardless of if they were reasoned against or not
</Warning>
## Full Configuration Schema Reference
### Workspace & Session Configuration
```json
{
"deriver": {
"enabled": true
},
"peer_card": {
"use": true,
"create": true
},
"summary": {
"enabled": true,
"messages_per_short_summary": 20,
"messages_per_long_summary": 60
},
"dream": {
"enabled": true
}
}
```
### Message Configuration
```json
{
"deriver": {
"enabled": true
},
"peer_card": {
"use": true,
"create": true
}
}
```
<Note>
Message configuration only supports `deriver` and `peer_card` settings. Summary and dream configurations are session/workspace-level only.
</Note>

View File

@ -1,44 +0,0 @@
---
title: 'Features'
description: 'Key features and capabilities of Honcho'
icon: 'star'
---
This page is a quick overview of the features within Honcho. In-depth
guides are available for each feature in the [Spellbooks - Design Patterns](../../guides/overview#design-patterns) section.
### Local vs Global Representation
Peers in Honcho are abstract entities that can represent humans, agents, or NPCs. Honcho has a two-layer approach to forming representations of Peers.
- **Global Representation**: Representation owned by a Peer that is constructed from everything the Peer has sent within Honcho.
- **Local Representation**: The representation that a Peer forms of other Peers, based on the messages those other Peers have sent (as observed by the Peer forming the representation).
- At the Session level, you can configure which Peers are able to observe messages from other Peers in that Session. This determines which Peers form representations of others within the Session.
### Queue Status
To help developers understand when a Peer's representation is fully up to date, Honcho exposes the ability to poll the status of Peer-centric queues that construct representations.
- If no Session is specified, the queue status reflects pending work for the Peer's global representation.
- If a Session is specified, the queue status reflects pending work for the Peer's working representation in that Session.
### Search
Honcho implements a powerful search endpoint that allows you to search for messages across a workspace, session, or peer with complex [filters](/v2/guides/using-filters).
The search process combines full-text and semantic search using reciprocal rank fusion. By default, all messages ingested into Honcho have embeddings generated and stored in the database, enabling semantic search -- if this feature is disabled, the search process will only use full-text search.
Results are returned in the form of a list of Message objects, and you may choose how many results to return. The default is 10 results, with a maximum of 100.
In the SDK, search is available on `Workspace`, `Session`, and `Peer` objects, and an optional `filters` parameter may be used to apply a narrower search scope such as a time range or developer-defined metadata attached to messages.
Note that results are not ordered by recency, only relevance. Results can be sorted by timestamp or a filter on the `created_at` field can limit results to recent messages.
[Look here for examples of how to use search in the SDK](/v2/guides/search).
### Scoped API Keys
Builders can create scoped API keys to control access to different resources within Honcho.
- **Workspace-Level Keys**: Access to everything scoped to a Workspace.
- **Peer-Level Keys**: Access to everything scoped to a Peer.
- **Session-Level Keys**: Access to everything scoped to a Session.
### Get Context
Honcho provides a powerful context retrieval feature that delivers formatted conversation context from sessions, making it easy to integrate with LLMs like OpenAI, Anthropic, and others.
- By default, the context includes a blend of summary and messages which covers the entire history of the session.
- Summaries are generated automatically at intervals, and recent messages are included based on your specified token budget for the context.
- You can set any token limit, and if you prefer, you can disable summaries so that the context consists entirely of the most recent messages up to your chosen limit.

View File

@ -84,4 +84,4 @@ for await (const line of responseStream.iter_text()) {
``` ```
</CodeGroup> </CodeGroup>
We've designed the Dialectic endpoint to be infinitely flexible. We wrote an incomplete list of ideas on how to use it on our blog [here](https://blog.plasticlabs.ai/blog/Introducing-Honcho's-Dialectic-API#how-it-works). We've designed the Dialectic endpoint to be infinitely flexible. We wrote an incomplete list of ideas on how to use it on our blog [here](https://blog.plasticlabs.ai/archive/ARCHIVED;-Introducing-Honcho's-Dialectic-API#how-it-works).

View File

@ -1,5 +1,5 @@
--- ---
title: 'Working with Session Context' title: 'Get Context'
description: 'Learn how to use get_context() to retrieve and format conversation context for LLM integration' description: 'Learn how to use get_context() to retrieve and format conversation context for LLM integration'
icon: 'messages' icon: 'messages'
--- ---
@ -93,6 +93,127 @@ context = session.get_context(summary=False, tokens=2000)
``` ```
</CodeGroup> </CodeGroup>
### Peer Representation in Context
You can include a peer's representation and peer card in the context by specifying `peer_target`. This is useful for providing the LLM with knowledge about a specific peer.
<CodeGroup>
```python Python
# Get context with peer representation included
context = session.get_context(
tokens=2000,
peer_target="user-123" # Include representation of user-123
)
# Access the representation and peer card
print(context.peer_representation) # String representation
print(context.peer_card) # List of peer card items
# Get representation from a specific peer's perspective
context = session.get_context(
tokens=2000,
peer_target="user-123",
peer_perspective="assistant" # From assistant's viewpoint
)
```
```typescript TypeScript
(async () => {
// Get context with peer representation included
const context = await session.getContext({
tokens: 2000,
peerTarget: "user-123" // Include representation of user-123
});
// Access the representation and peer card
console.log(context.peerRepresentation); // String representation
console.log(context.peerCard); // Array of peer card items
// Get representation from a specific peer's perspective
const perspectiveContext = await session.getContext({
tokens: 2000,
peerTarget: "user-123",
peerPerspective: "assistant" // From assistant's viewpoint
});
})();
```
</CodeGroup>
### Semantic Search
Use `search_query` to fetch semantically relevant observations based on a query string:
<CodeGroup>
```python Python
# Get context with semantic search based on query
context = session.get_context(
tokens=2000,
peer_target="user-123",
search_query="What are my account preferences?",
search_top_k=10, # Number of relevant observations
search_max_distance=0.8, # Max semantic distance (0.0-1.0)
include_most_derived=True, # Include most recent observations
max_observations=25 # Cap total observations
)
```
```typescript TypeScript
(async () => {
// Get context with semantic search based on query
const context = await session.getContext({
tokens: 2000,
peerTarget: "user-123",
searchQuery: "What are my account preferences?",
searchTopK: 10, // Number of relevant observations
searchMaxDistance: 0.8, // Max semantic distance (0.0-1.0)
includeMostDerived: true, // Include most recent observations
maxObservations: 25 // Cap total observations
});
})();
```
</CodeGroup>
### Session-Scoped Representations
Use `limit_to_session` to only include observations from the current session:
<CodeGroup>
```python Python
# Get context limited to this session's observations only
context = session.get_context(
tokens=2000,
peer_target="user-123",
limit_to_session=True # Only observations from this session
)
```
```typescript TypeScript
(async () => {
// Get context limited to this session's observations only
const context = await session.getContext({
tokens: 2000,
peerTarget: "user-123",
limitToSession: true // Only observations from this session
});
})();
```
</CodeGroup>
### All Parameters Reference
| Parameter | Type | Description |
|-----------|------|-------------|
| `summary` | `bool` | Include summary in context (default: true) |
| `tokens` | `int` | Maximum tokens to include |
| `peer_target` | `str` | Peer ID to include representation for |
| `peer_perspective` | `str` | Peer ID for perspective (requires peer_target) |
| `search_query` | `str` | Query for semantic search (requires peer_target) |
| `limit_to_session` | `bool` | Limit to session observations only |
| `search_top_k` | `int` | Semantic search results to include (1-100) |
| `search_max_distance` | `float` | Max semantic distance (0.0-1.0) |
| `include_most_derived` | `bool` | Include most recently derived observations |
| `max_observations` | `int` | Maximum observations to include (1-100) |
## Converting to LLM Formats ## Converting to LLM Formats
The `SessionContext` object provides methods to convert the context into formats compatible with popular LLM APIs. When converting to OpenAI format, you must specify the assistant peer to format the context in such a way that the LLM can understand it. The `SessionContext` object provides methods to convert the context into formats compatible with popular LLM APIs. When converting to OpenAI format, you must specify the assistant peer to format the context in such a way that the LLM can understand it.

View File

@ -0,0 +1,68 @@
---
title: Local vs Global Representations
description: Model directional relationships between Peers in Honcho
icon: location-pin
---
One of the unique affordances of Honcho is that it allows developers to model
directional relationships between Peers. What I mean by this is you can model
how one `Peer` thinks about another `Peer`.
There are many use cases where you don't want every agent or human to know
everything about another user such as games or multi-agent workflows. To
illustrate this, the following examples shows 2 conversations.
Conversation #1 (With Bob and Alice)
```
Alice: I had a great breakfast today.
Bob: What did you eat?
Alice: I had pancakes and eggs and bacon
```
Conversation #2 (With Alice and Charlie)
```
Alice: I actually didn't eat any breakfast today.
Charlie: Oh that's too bad.
Alice: But I lied to Bob and told him I did, so back me up if you see them.
```
Alice told Bob a lie in this conversation. If we stored both of these
conversations in Honcho with Alice, Bob, and Charlie as `Peers` and let them
use Honcho to get insights on each other then Bob would immediately know this
deception. For example:
<CodeGroup>
```python Python
# Bob could run
alice.chat("What did Alice eat today?")
# Response: Alice did not eat anything today
```
</CodeGroup>
This is a problem. Bob shouldn't be able to know everything about Alice in this
situation. So to support these situations we support what we call **Local
Representations**.
By default insights generated for a `Peer` are scoped globally. This means every
message sent by that `Peer` in any conversation updates the same representation
of that `Peer`. However, we can enable **Local Representations** so Bob can
form a representation Alice based only on what they observe Alice do.
This feature is illustrated in the graphic below:
<img src="/images/local-vs-global-reps.png" alt="Peer Representations" />
We can enable local representation for a `Peer` by setting `observe_others=True`.
This is shown in the [Configure
Reasoning](/v2/documentation/core-concepts/configuration) page.
Now if we used Bob's local representation of Alice then Bob would only get
insights on what they've seen Alice say to them.
```python
bob.chat(target="alice", query="What did Alice eat today?")
# Response: Alice ate pancakes, eggs, and bacon
```
<Note>
Local Representations are turned off by default
</Note>

View File

@ -0,0 +1,132 @@
---
title: Queue Status
description: Learn how to check the status of the Deriver
icon: lines-leaning
---
Whenever `Messages` are stored in Honcho, a background process called the
[Deriver](/docs/v2/documentation/core-concepts/architecture#reasoning-layer) is
triggered to reason about the conversation and generate insights.
The Deriver is an asynchronous process and, depending on load may not immediately
generated insights for the latest message you've sent. To help with this, Honcho
provides several utilities to check the status of the Deriver.
<CodeGroup>
```python Python
from honcho import Honcho
honcho = Honcho()
status = honcho.get_deriver_status()
honcho.poll_deriver_status()
```
```typescript typescript
import { Honcho } from '@honcho-ai/sdk';
const honcho = new Honcho({});
const status = await honcho.getDeriverStatus();
await honcho.pollDeriverStatus();
```
</CodeGroup>
Output types
<CodeGroup>
```python Python
class DeriverStatus(BaseModel):
completed_work_units: int
"""Completed work units"""
in_progress_work_units: int
"""Work units currently being processed"""
pending_work_units: int
"""Work units waiting to be processed"""
total_work_units: int
"""Total work units"""
sessions: Optional[Dict[str, Sessions]] = None
"""Per-session status when not filtered by session"""
```
```typescript TypeScript
Promise<{
totalWorkUnits: number
completedWorkUnits: number
inProgressWorkUnits: number
pendingWorkUnits: number
sessions?: Record<string, DeriverStatus.Sessions>
}>
```
</CodeGroup>
Whenever a `Message` is sent it will generate several tasks. These could
be tasks such as generating insights, cleaning up a representation, summarizing
a conversation etc. These tasks are defined based on who is sending the
message, what `Session` the message is in, and potentially who is observing the
message. We call the combination of these parameters a `work_unit`
This has a few different implications.
- tasks within the same work_unit are processed sequentially, but multiple
work_units will be processed in parallel
- If local representations are turned in a Session then a `Message` will
generate an additional work unit for every `Peer` that has `observe_others=True`
The `get_deriver_status` and `poll_deriver_status` methods can take additional
parameters to scope the status to a specific work unit
<CodeGroup>
```python Python
def get_deriver_status(
self,
observer_id: str | None = None,
sender_id: str | None = None,
session_id: str | None = None,
) -> DeriverStatus:
```
```typescript TypeScript
export const DeriverStatusOptionsSchema = z.object({
observerId: z.string().optional(),
senderId: z.string().optional(),
sessionId: z.string().optional(),
timeoutMs: z
.number()
.positive('Timeout must be a positive number')
.optional(),
})
```
</CodeGroup>
Additionally, there are deriver status and polling deriver status methods
available on the `Session` objects in each of the SDKs.
Below are the function signatures for the session level deriver status method
<CodeGroup>
```python python
@validate_call
def get_deriver_status(
self,
observer_id: str | None = None,
sender_id: str | None = None,
) -> DeriverStatus:
```
```typescript TypeScript
async getDeriverStatus(
options?: Omit<DeriverStatusOptions, 'sessionId'>
): Promise<{
totalWorkUnits: number
completedWorkUnits: number
inProgressWorkUnits: number
pendingWorkUnits: number
sessions?: Record<string, DeriverStatus.Sessions>
}>
```
</CodeGroup>

View File

@ -0,0 +1,61 @@
---
title: Storing Data
description: "Store Data in Honcho to Generate Memories and Insights"
icon: "memory"
---
The most basic building block of Honcho's data model is the `Message` object.
A `Message` is sent by a `Peer` and saved in a `Session`
<CodeGroup>
```python Python
from honcho import Honcho
honcho = Honcho()
peer = honcho.peer("sample-peer")
session = honcho.session("sample-session")
message = peer.message("Hello, world!")
session.add_messages([message])
```
```typescript TypeScript
import { Honcho } from '@honcho-ai/sdk';
const honcho = new Honcho({});
const peer = await honcho.peer('sample-peer');
const session = await honcho.session('sample-session');
const message = peer.message('Hello, world!');
await session.addMessages([message]);
```
</CodeGroup>
Once a `Message` is saved in Honcho, it will kick off a background task that
looks at the new data to generate insights about the `Peer` that sent the `Message`
This is the default behavior of Honcho and can be turned off by [configuring the
Peer or Session](/v2/documentation/core-concepts/configuration)
This pattern of having a Peer, Session, and Messages is highly flexible and
works for many different use cases and agent setups. Some use cases may only
need a single Peer, but many Sessions. Others will only use a single `Session`
for their entire app. These are flexible components that work in any situation.
## Chat Bots
A common use case for Honcho to is to build a chatbot like ChatGPT or Claude.
In this case you can simply
- Make a `Peer` for the User
- Make a `Peer` for the AI
Then you can make a `Session` for each thread of conversation and save
`Messages` from the user and assistant in each turn of conversation

View File

@ -22,7 +22,7 @@ Working representations are automatically generated and cached through Honcho's
## Basic Usage ## Basic Usage
Working representations are accessed through the `working_rep()` method on Session objects: Working representations are accessed through the `working_rep()` method on Session or Peer objects:
<CodeGroup> <CodeGroup>
```python Python ```python Python
@ -50,6 +50,9 @@ response = user.chat("What is this user's main concern right now?", session_id=s
# Retrieve the cached working representation for the user # Retrieve the cached working representation for the user
user_representation = session.working_rep("user-123") user_representation = session.working_rep("user-123")
print("Cached user representation:", user_representation) print("Cached user representation:", user_representation)
# Or access from the peer directly
peer_representation = user.working_rep()
``` ```
```typescript TypeScript ```typescript TypeScript
@ -77,7 +80,76 @@ const response = await user.chat("What is this user's main concern right now?",
// Retrieve the cached working representation for the user // Retrieve the cached working representation for the user
const userRepresentation = await session.workingRep("user-123"); const userRepresentation = await session.workingRep("user-123");
console.log("Cached user representation:", userRepresentation); console.log("Cached user representation:", userRepresentation);
// Returns: { representation: Object }
// Or access from the peer directly
const peerRepresentation = await user.workingRep();
```
</CodeGroup>
## Semantic Search in Representations
Working representations support semantic search to retrieve the most relevant observations for a given query. This is useful when you want to focus the representation on specific topics.
### Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `search_query` | `str` | Semantic search query to filter relevant observations |
| `search_top_k` | `int` | Number of semantic search results to include (1-100) |
| `search_max_distance` | `float` | Maximum semantic distance threshold (0.0-1.0) |
| `include_most_derived` | `bool` | Whether to include the most recently derived observations |
| `max_observations` | `int` | Maximum number of observations to include (1-100) |
<CodeGroup>
```python Python
# Get representation focused on a specific topic
billing_rep = session.working_rep(
"user-123",
search_query="billing and payment issues",
search_top_k=10,
search_max_distance=0.8,
include_most_derived=True,
max_observations=25
)
# Get representation from peer with target
# What user-123 knows about the assistant
local_rep = session.working_rep(
"user-123",
target="ai-assistant",
search_query="support interactions"
)
# Access from peer object with semantic search
user_rep = user.working_rep(
session=session,
search_query="preferences",
search_top_k=5
)
```
```typescript TypeScript
// Get representation focused on a specific topic
const billingRep = await session.workingRep("user-123", {
searchQuery: "billing and payment issues",
searchTopK: 10,
searchMaxDistance: 0.8,
includeMostDerived: true,
maxObservations: 25
});
// Get representation from peer with target
// What user-123 knows about the assistant
const localRep = await session.workingRep("user-123", {
target: "ai-assistant",
searchQuery: "support interactions"
});
// Access from peer object with semantic search
const userRep = await user.workingRep(session, undefined, {
searchQuery: "preferences",
searchTopK: 5
});
``` ```
</CodeGroup> </CodeGroup>

View File

@ -5,108 +5,102 @@ icon: "brain"
sidebarTitle: "Overview" sidebarTitle: "Overview"
--- ---
When building agents developers often run into the same walls: Honcho is an AI-native memory library for building agents with
[state-of-the-art](https://blog.plasticlabs.ai/research/Introducing-Neuromancer-XR)
long-term memory.
> "My agent forgets everything between chats" Agents using Honcho have perfect recall with a wide variety of tools to traverse
their history and get the exact context they need when they need it.
You need memory: session management, message storage, context handling. It's table stakes, but surprisingly complex to get right. It then goes beyond basic memory by reasoning about the stored history
to expand the latent information available to your agent. Agents using Honcho
will understand who they are, who they are interacting with, what happened, and
when it happened — all without you having to think about it.
> "My agent treats everyone exactly the same" Use it to build
You need personalization: user modeling, preference learning, behavioral adaptation. Now you're building a [social cognition](../core-concepts/glossary#social-cognition) engine. - Highly personalized experiences
- Agents with social cognition
- Agents with rich identity that evolve over time
- Multi-agent systems with complex social dynamics
> "I'm writing infrastructure instead of features"
You need Honcho
<img src="/images/agent_hierarchy.png" alt="Honcho's Hiearchy of Agents" />
Honcho delivers production-ready memory infrastructure from day one. Store
conversations, manage sessions, get perfectly formatted context for any LLM.
But here's the magic: while your agents are chatting, Honcho is learning. It
builds Theory of Mind models automatically, transforming raw conversations into
rich psychological understanding.
```python ```python
# Start simple - just add messages # Start simple by just adding messages
session.add_messages([alice.message("I learn best with examples")]) session.add_messages([alice.message("I learn best with examples")])
# Get powerful - query user psychology # Honcho will automatically reason about the message to generate insights about Alice
# Get insights by chatting with the agent
insight = peer.chat("How should I explain this concept?") insight = peer.chat("How should I explain this concept?")
# > "This user learns best through concrete examples..." # > "This user learns best through concrete examples..."
``` ```
Your agents evolve from goldfish to counselor, on the same infrastructure. That's Honcho.
Designed for developers and agents alike: Designed for developers and agents alike:
- **Natural Language Queries**: Chat with Honcho in natural language via the [Dialectic API](../core-concepts/architecture#dialectic-api) and let agents backchannel - **Natural Language Queries**: Chat with Honcho in natural language via the [Dialectic API](../core-concepts/architecture#dialectic-api) to get insights about your users and agents
- **Automatic Context Management**: Smart summarization that respects token limits - **Automatic Context Management**: Smart conversation summaries to have infinite chats
- **Native multi-agent support**: Break out of User/Assistant Paradigms and build complex multi-agent systems - **Native multi-agent support**: Sessions can natively have as many participants as you need
- **Agent-first interfaces**: MCP connections and APIs designed for agents to consume and use as tools - **Agent-first interfaces**: MCP connections and APIs designed for agents to consume and use as tools
- **Provider Agnostic**: Works with any LLM or Agent Framework - **Provider Agnostic**: Works with any LLM or Agent Framework
## How It Works ## How It Works
### Storage <Accordion title="High Level Diagram" defaultOpen="true">
<Frame>
<img src="/images/overview/honcho-overview.svg" alt="High Level Honcho Diagram" />
</Frame>
</Accordion>
Developers use Honcho to store information about their users and application via At a high level Honcho works very simply:
two integrated layers:
<img src="/images/basic_honcho_flowchart.png" alt="Basic Honcho Flowchart" /> 1. Store messages sent by users and agents in Honcho
2. Honcho reasons about the messages to generate insights about each entity in
the system
3. At runtime your agents can leverage insights from Honcho to get the exact
context they need
**Memory Layer**: Captures all user interactions - messages, preferences, and There are several API endpoints to leverage the memory & insights in Honcho.
behavioral patterns - in a peer-centric data model that scales from individual
conversations to complex multi-agent scenarios. This also queues up messages for
the reasoning layer to process.
**Reasoning Layer**: Continuously analyzes stored interactions to build ### Get Context
psychological profiles using [theory of mind](../core-concepts/glossary#theory-of-mind)
inference, extracting patterns about communication style, decision-making
preferences, and mental models.
### Retrieval This is the easiest way to leverage Honcho. simply call get context and get the
most relevant information for your conversation. This endpoint is highly
customizable so you can specify parameters such as:
Once data is stored and generated within Honcho, the API exposes several - A number of tokens you want
different ways to retrieve and use those insights. - An option to include summaries of the conversation
- An option to get a profile of a specific user (Peer Card & Representation)
**[Dialectic API](/v2/guides/dialectic-endpoint)**: This is the ### Search
flagship endpoint that allows developers to send natural language queries to
Honcho to chat with the representation of each user in your system to get
dynamic, in-context actionable insights.
Example Queries This endpoint lets you search across Honcho for relevant messages using a
hybrid search strategy that combines full-text and semantic search.
You can optionally scope the endpoint to a specific workspace, peer, or session.
### Working Representation
This endpoint gives you a snapshot of a user or what we call a
**Representation**. Essentially, a list of explicit and deductive facts about
the user that are relevant to the current conversation.
Plug this into your prompt to get a quick overview of the user.
### Dialectic API
This endpoint lets you chat with Honcho about any entity in your system. Honcho
will leverage what it has remembered and learned about the entity to provide in-context actionable insights.
This is especially helpful when you want your agent to back-channel with Honcho to
change its behavior at runtime.
Example Queries:
- "What's the best way to explain technical concepts to this user?" - "What's the best way to explain technical concepts to this user?"
- "Is this user more task-oriented or relationship-oriented?" - "Is this user more task-oriented or relationship-oriented?"
- "What time of day is this user most engaged?" - "What time of day is this user most engaged?"
- "How does this user prefer to receive feedback?" - "How does this user prefer to receive feedback?"
- "What are this user's core values based on our conversations?" - "What are this user's core values based on our conversations?"
**[Get Context](/v2/guides/get-context)**: This endpoint abstracts context window
constraints and continuously retrieves the most relevant and recent data from a
conversation. Provide a token budget and Honcho will return a combination of
summaries and messages that provide session context. Use this for creating
long-running conversations. We crafted our summaries to provide the most
[coverage of a session possible](../core-concepts/summarizer).
**[Search](/v2/guides/search)**: This endpoint allows you to search across Honcho
for relevant messages either at the workspace, peer, or session level. This
endpoint uses a hybrid search strategy that combines text search and cosine
similarity.
**[Working Representations](/v2/guides/working-rep)**: Get a cached, snapshot
of a user in the context of a session. Instead of waiting for an LLM to
synthesize an in-context response via the Dialectic endpoint, use this to get
recent insights you can plug into your context window.
## Ideal For
**Personalized AI assistants** that need to understand individual psychology, not just remember conversations.
**Customer-facing agents** that must adapt their approach based on user communication preferences and emotional context.
**Multi-agent systems** where AI needs to understand human collaborators' working styles and decision-making patterns.
**NPCs** where you want autonomous agents with a rich and deep personality that isn't the average sycophantic llm
## Getting Started ## Getting Started
@ -115,11 +109,11 @@ Ready to integrate Honcho into your application?
<CardGroup cols={2}> <Card title="Quickstart Guide" icon="rocket" <CardGroup cols={2}> <Card title="Quickstart Guide" icon="rocket"
href="/v2/documentation/introduction/quickstart"> Get up and running with href="/v2/documentation/introduction/quickstart"> Get up and running with
Honcho in minutes </Card> <Card title="Core Concepts" icon="brain" Honcho in minutes </Card> <Card title="Core Concepts" icon="brain"
href="/v2/documentation/core-concepts/glossary"> Understand Honcho's href="/v2/documentation/core-concepts/architecture"> Understand Honcho's
fundamental concepts </Card> </CardGroup> fundamental concepts </Card> </CardGroup>
## Community & Support ## Community & Support
- **GitHub**: [plastic-labs/honcho](https://github.com/plastic-labs/honcho) - **GitHub**: [plastic-labs/honcho](https://github.com/plastic-labs/honcho)
- **Discord**: [Join our community](http://discord.gg/plasticlabs) - **Discord**: [Join our community](http://discord.gg/honcho)
- **Issues**: Report bugs and request features on GitHub - **Issues**: Report bugs and request features on GitHub

View File

@ -62,7 +62,7 @@ The Honcho client is the main entry point for interacting with Honcho's API. By
from honcho import Honcho from honcho import Honcho
# Initialize client (uses demo environment and default workspace) # Initialize client (uses demo environment and default workspace)
client = Honcho() honcho = Honcho()
``` ```
@ -70,7 +70,7 @@ client = Honcho()
import { Honcho } from '@honcho-ai/sdk'; import { Honcho } from '@honcho-ai/sdk';
// Initialize client (uses demo environment and default workspace) // Initialize client (uses demo environment and default workspace)
const client = new Honcho({}); const honcho = new Honcho({});
``` ```
</CodeGroup> </CodeGroup>
@ -83,7 +83,7 @@ import os
from honcho import Honcho from honcho import Honcho
# Production environment with API key # Production environment with API key
client = Honcho( honcho = Honcho(
api_key=os.environ["HONCHO_API_KEY"], api_key=os.environ["HONCHO_API_KEY"],
environment="production", environment="production",
# Create a workspace, otherwise set to "default" # Create a workspace, otherwise set to "default"
@ -95,7 +95,7 @@ client = Honcho(
import { Honcho } from '@honcho-ai/sdk'; import { Honcho } from '@honcho-ai/sdk';
// Production environment with API key // Production environment with API key
const client = new Honcho({ const honcho = new Honcho({
apiKey: process.env.HONCHO_API_KEY!, apiKey: process.env.HONCHO_API_KEY!,
environment: "production", environment: "production",
// Create a workspace, otherwise set to "default" // Create a workspace, otherwise set to "default"
@ -110,13 +110,13 @@ Peers represent individual users, AI agents, or any conversational entity in you
<CodeGroup> <CodeGroup>
```python Python ```python Python
alice = client.peer("alice") alice = honcho.peer("alice")
bob = client.peer("bob") bob = honcho.peer("bob")
``` ```
```typescript TypeScript ```typescript TypeScript
const alice = await client.peer("alice") const alice = await honcho.peer("alice")
const bob = await client.peer("bob") const bob = await honcho.peer("bob")
``` ```
</CodeGroup> </CodeGroup>
@ -126,12 +126,12 @@ Sessions are independent conversations that can include multiple peers:
<CodeGroup> <CodeGroup>
```python Python ```python Python
session = client.session("session_1") session = honcho.session("session_1")
session.add_peers([alice, bob]) session.add_peers([alice, bob])
``` ```
```typescript TypeScript ```typescript TypeScript
const session = await client.session("session_1") const session = await honcho.session("session_1")
await session.addPeers([alice, bob]) await session.addPeers([alice, bob])
``` ```
</CodeGroup> </CodeGroup>
@ -171,7 +171,7 @@ Now ask Honcho what it's learned - this is where the magic happens:
<CodeGroup> <CodeGroup>
```python Python ```python Python
# Ask what Bob is like # Ask what Bob is like
response = alice.chat("Tell me about Bob's interests and habits") response = bob.chat("Tell me about Bob's interests and habits")
print(response) print(response)
# Returns rich context like: # Returns rich context like:
@ -182,36 +182,128 @@ print(response)
``` ```
```typescript TypeScript ```typescript TypeScript
(async () => { bob.chat("Tell me about Bob's interests and habits").then((response) => {
// Ask what Bob is like
const response = await alice.chat("Tell me about Bob's interests and habits");
console.log(response); console.log(response);
// Returns rich context like: // Returns rich context like:
// "Bob is health-conscious and has been working on getting back in shape. // "Bob is health-conscious and has been working on getting back in shape.
// He regularly goes to the gym, particularly in the evenings, and finds // He regularly goes to the gym, particularly in the evenings, and finds
// exercise helps him relax. He's encouraging about fitness and willing // exercise helps him relax. He's encouraging about fitness and willing
// to share advice about workout routines." // to share advice about workout routines."
})(); })
```
</CodeGroup>
## 7. Putting it all together
<CodeGroup>
```python Python
import os
from honcho import Honcho
# Create your client
honcho = Honcho(
api_key=os.environ["HONCHO_API_KEY"],
environment="production",
# Create a workspace, otherwise set to "default"
# workspaceId="your-workspace-id"
)
# Get your Peers
alice = honcho.peer("alice")
bob = honcho.peer("bob")
# Make a Session and add your Peers
session = honcho.session("session_1")
session.add_peers([alice, bob])
# Add messages sent by your Peers
session.add_messages([
alice.message("Hi Bob, how are you?"),
bob.message("I'm good, thank you!"),
alice.message("What are you doing today after work?"),
bob.message("I'm going to the gym! I've been trying to get back in shape."),
alice.message("That's great! I should probably start exercising too."),
bob.message("You should! I find that evening workouts help me relax."),
])
# Get insights about your Peers
response = bob.chat("Tell me about Bob's interests and habits")
print(response)
# Returns rich context like:
# "Bob is health-conscious and has been working on getting back in shape.
# He regularly goes to the gym, particularly in the evenings, and finds
# exercise helps him relax. He's encouraging about fitness and willing
# to share advice about workout routines."
```
```typescript TypeScript
import { Honcho } from '@honcho-ai/sdk';
// Create your client
const honcho = new Honcho({
apiKey: process.env.HONCHO_API_KEY!,
environment: "production",
// Create a workspace, otherwise set to "default"
// workspace: "your-workspace-id"
});
// Get your Peers
const alice = await honcho.peer("alice")
const bob = await honcho.peer("bob")
// Make a Session and add your peers
const session = await honcho.session("session_1")
await session.addPeers([alice, bob])
// Add messages sent by your Peers
await session.addMessages([
alice.message("Hi Bob, how are you?"),
bob.message("I'm good, thank you!"),
alice.message("What are you doing today after work?"),
bob.message("I'm going to the gym! I've been trying to get back in shape."),
alice.message("That's great! I should probably start exercising too."),
bob.message("You should! I find that evening workouts help me relax."),
])
// Get insights about your peers
bob.chat("Tell me about Bob's interests and habits").then((response) => {
console.log(response);
// Returns rich context like:
// "Bob is health-conscious and has been working on getting back in shape.
// He regularly goes to the gym, particularly in the evenings, and finds
// exercise helps him relax. He's encouraging about fitness and willing
// to share advice about workout routines."
})
``` ```
</CodeGroup> </CodeGroup>
## What Just Happened? ## What Just Happened?
Honcho automatically built rich psychological profiles from just a few messages: You just got through building a simple conversation between two people, Alice
and Bob. We:
- **Theory of Mind Processing**: Understanding personality, preferences, and patterns 1. Set up our connection to Honcho.
- **Ambient Learning**: No surveys or explicit training - just natural conversation 2. Setup who the participants of our conversation are, these are called `Peers`.
- **Rich Context**: Far more detailed than simple conversation history 3. Made a `Session` and added our `Peers` to it.
4. Sent messages from our `Peers`
5. Chat with Honcho to get insights about one of the `Peers` in the conversation
The response isn't just retrieving stored text - it's synthesizing insights about Bob's personality, habits, and communication style. As soon as you save a message in Honcho, it will start to reason about it to
pull out insights and develop a profile of the user. This is the default
behavior and can be toggled off via [the configuration](/v2/documentation/core-concepts/configuration).
## Next Steps ## Next Steps
This covers the core concepts: **peers**, **sessions**, **messages**, and **dialectic queries**. <CardGroup cols={3}>
<Card title="Architecture" icon="rocket"
- For production use, [sign up for the managed platform](https://app.honcho.dev) or get an [overview here](../reference/platform). href="/v2/documentation/core-concepts/architecture">
- For detailed API reference, check out our [SDK documentation](../reference/sdk). Learn about the data primitives in Honcho and how they work together
- For more examples, explore our [guides](../guides/overview). </Card>
<Card title="Start Building" icon="brain" href="https://app.honcho.dev">
--- Sign up for Managed Honcho and get started building agents now.
</Card>
<Card title="Guides" icon="book" href="/v2/guides/overview">
Check out spellbooks to see different examples apps built with Honcho
</Card>
</CardGroup>

View File

@ -5,7 +5,17 @@ description: "Universal starter prompt for building with Honcho"
sidebarTitle: 'Vibecoding Setup' sidebarTitle: 'Vibecoding Setup'
--- ---
Copy this prompt into Cursor, Claude, or any AI coding assistant to start building with Honcho. These docs are designed to be easily consumable for LLMs. Each page has a button
the lets you copy the page as Markdown or paste directly into ChatGPT or Claude.
Additionally, we follow the llms.txt standard. There are both an llms.txt and
llms-full.txt available.
- [llms.txt](/llms.txt)
- [llms-full.txt](/llms-full.txt)
Additionally, we provide a starter prompt to paste into a coding assistant to
quickly get started building with Honcho.
## 🚀 Universal Starter Prompt ## 🚀 Universal Starter Prompt
@ -15,10 +25,10 @@ I want to start building with Honcho - a memory and personalization platform for
## Honcho Resources ## Honcho Resources
**Documentation:** **Documentation:**
- Main docs: https://docs.honcho.dev - Main docs: https://honcho.dev/docs
- API Reference: https://docs.honcho.dev/v2/api-reference/introduction - API Reference: https://honcho.dev/docs/v2/api-reference/introduction
- Quickstart: https://docs.honcho.dev/v2/documentation/introduction/quickstart - Quickstart: https://honcho.dev/docs/v2/documentation/introduction/quickstart
- Architecture: https://docs.honcho.dev/v2/documentation/reference/architecture - Architecture: https://honcho.dev/docs/v2/documentation/reference/architecture
**Code & Examples:** **Code & Examples:**
- Core repo: https://github.com/plastic-labs/honcho - Core repo: https://github.com/plastic-labs/honcho

View File

@ -422,4 +422,4 @@ Congratulations! You've built a complete personal AI assistant with Honcho that
- [SDK Reference](/v2/documentation/reference/sdk) - [SDK Reference](/v2/documentation/reference/sdk)
- [API Reference](/v2/api-reference/introduction) - [API Reference](/v2/api-reference/introduction)
- [More Examples](/v2/guides/overview) - [More Examples](/v2/guides/overview)
- [Discord Community](http://discord.gg/plasticlabs) - [Discord Community](http://discord.gg/honcho)

Some files were not shown because too many files have changed in this diff Show More