fix(honcho): read Honcho 3.0.x response schema — memory reads were silently empty
Nightly Build / build (push) Successful in 3m48s
Nightly Build / build (push) Successful in 3m48s
Against a self-hosted Honcho 3.0.11 every read path came back empty while the server was healthy and full of derived facts: the plugin parsed a `conclusions`/`summary` shape the API no longer emits. - honcho-client: typed models for the real schema — `PeerContext` (`representation` markdown + `peer_card`), `SessionContext` (`summary` as an object, `peer_representation`), wrapped `PeerCard` (a bare array on PUT is a 422, which also broke `honcho_profile` writes). - plugin: the turn-time injection and `honcho_context` read the representation; `honcho_search` and the page's /search now use `conclusions/query` (observer/observed scoping inside `filters`) — a real ranked semantic search with fact ids, which `peer_context?search_query` never provided; /overview returns card + representation + conclusions. - compose: pin the Honcho image by digest (3.0.11) — ghcr publishes no v3 semver tags, and an untracked `:latest` pull is what drifted the schema. - tests: fixture tests from payloads captured on the live server, plus an env-gated live smoke test (HONCHO_E2E_URL/_WS/_PEER, `cargo test -p honcho-client -- --ignored`) — run it before any future Honcho bump.
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
//! - `GET /status` — user-facing service health (reachability + the
|
||||
//! caller's own processing queue).
|
||||
//! - `GET /overview` — the caller's full memory snapshot (peer card +
|
||||
//! conclusions + summary). Cheap GETs, no LLM.
|
||||
//! representation digest + derived conclusions with ids). Cheap, no LLM.
|
||||
//! - `POST /search` — semantic search over the caller's derived facts.
|
||||
//! - `POST /ask` — Dialectic: Honcho's server-side LLM answers a
|
||||
//! natural-language question from the caller's memory.
|
||||
@@ -63,7 +63,10 @@ use core_api::user_channel::UserChannelApi;
|
||||
use core_api::user_plugin_config::PluginUserConfigApi;
|
||||
use honcho_client::HonchoClient;
|
||||
use honcho_client::error::HonchoError;
|
||||
use honcho_client::models::{DialecticOptions, PageParams, PeerRepresentationGet, WorkspaceGet};
|
||||
use honcho_client::models::{
|
||||
ConclusionGet, ConclusionQuery, DialecticOptions, PageParams, PeerRepresentationGet,
|
||||
WorkspaceGet,
|
||||
};
|
||||
|
||||
// Namespaced i18n keys for the router's user-facing strings (backend tables in
|
||||
// `../i18n/*.json`), resolved to the caller's language via `web.i18n`.
|
||||
@@ -334,8 +337,14 @@ async fn user_status(
|
||||
// ── GET /overview ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// The caller's full memory snapshot — the direct answer to "what does Honcho
|
||||
/// know about me?": peer card (curated key facts) + conclusions (derived facts,
|
||||
/// with their ids) + summary. Two cheap GETs, no LLM synthesis.
|
||||
/// know about me?": the peer card (curated key facts), the pre-rendered
|
||||
/// markdown `representation` (what the assistant actually receives), and the
|
||||
/// individual conclusions with their ids (for targeted deletion). Cheap, no
|
||||
/// LLM synthesis.
|
||||
///
|
||||
/// Schema note (Honcho 3.0.x): `peer_context` carries `representation` +
|
||||
/// `peer_card`, NOT a `conclusions` array — the ids come from a separate
|
||||
/// `conclusions/list` scoped to the caller's peer.
|
||||
async fn user_overview(
|
||||
State(cell): State<WebCell>,
|
||||
Extension(caller): Extension<Caller>,
|
||||
@@ -349,10 +358,12 @@ async fn user_overview(
|
||||
Err(r) => return r,
|
||||
};
|
||||
|
||||
// A 404 on either call is "no peer/card yet", not a failure — the peer is
|
||||
// A 404 on the context call is "no peer yet", not a failure — the peer is
|
||||
// created lazily by the write path on the user's first forwarded turn.
|
||||
// The card response wraps the list: `{"peer_card": [...] | null}` — unwrap
|
||||
// it so the page receives the bare value.
|
||||
let card = match web.client.get_peer_card(&web.workspace_id, &peer, None).await {
|
||||
Ok(v) => v,
|
||||
Ok(c) => json!(c.peer_card),
|
||||
Err(e @ HonchoError::Http { status: 404, .. }) => {
|
||||
debug!("honcho /overview: no card for peer '{peer}' yet: {e}");
|
||||
Value::Null
|
||||
@@ -360,7 +371,7 @@ async fn user_overview(
|
||||
Err(e) => return honcho_error(&web, &caller, &e).await,
|
||||
};
|
||||
|
||||
let ctx = match web.client.peer_context(
|
||||
let representation = match web.client.peer_context(
|
||||
&web.workspace_id,
|
||||
&peer,
|
||||
&PeerRepresentationGet {
|
||||
@@ -368,27 +379,39 @@ async fn user_overview(
|
||||
..Default::default()
|
||||
},
|
||||
).await {
|
||||
Ok(v) => v,
|
||||
Ok(ctx) => ctx.representation,
|
||||
Err(e @ HonchoError::Http { status: 404, .. }) => {
|
||||
debug!("honcho /overview: no context for peer '{peer}' yet: {e}");
|
||||
Value::Null
|
||||
None
|
||||
}
|
||||
Err(e) => return honcho_error(&web, &caller, &e).await,
|
||||
};
|
||||
|
||||
let conclusions = match web.client.list_conclusions(
|
||||
&web.workspace_id,
|
||||
&PageParams { size: Some(OVERVIEW_MAX_CONCLUSIONS as u64), ..Default::default() },
|
||||
&ConclusionGet {
|
||||
filters: Some(json!({ "observer_id": peer, "observed_id": peer })),
|
||||
},
|
||||
).await {
|
||||
Ok(page) => page.items,
|
||||
Err(e) => return honcho_error(&web, &caller, &e).await,
|
||||
};
|
||||
|
||||
Json(json!({
|
||||
"card": card,
|
||||
"conclusions": ctx.get("conclusions").cloned().unwrap_or(Value::Array(vec![])),
|
||||
"summary": ctx.get("summary").and_then(Value::as_str),
|
||||
"card": card,
|
||||
"representation": representation,
|
||||
"conclusions": conclusions,
|
||||
})).into_response()
|
||||
}
|
||||
|
||||
// ── POST /search ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// Semantic search over the caller's derived facts: `peer_context` with a
|
||||
/// `search_query`, ranked raw excerpts with their ids — no LLM synthesis. The
|
||||
/// same proven path as the `honcho_search` tool (the direct
|
||||
/// `conclusions/query` endpoint needs observer/observed filters and is not it).
|
||||
/// Semantic search over the caller's derived facts: `conclusions/query`,
|
||||
/// ranked raw excerpts with their ids — no LLM synthesis. The same path as the
|
||||
/// `honcho_search` tool; the observer/observed scoping lives in `filters`
|
||||
/// (and `peer_context` would not do: it returns the whole representation with
|
||||
/// no ids once it fits the budget).
|
||||
async fn user_search(
|
||||
State(cell): State<WebCell>,
|
||||
Extension(caller): Extension<Caller>,
|
||||
@@ -407,24 +430,16 @@ async fn user_search(
|
||||
Err(r) => return r,
|
||||
};
|
||||
|
||||
match web.client.peer_context(
|
||||
match web.client.query_conclusions(
|
||||
&web.workspace_id,
|
||||
&peer,
|
||||
&PeerRepresentationGet {
|
||||
search_query: Some(query),
|
||||
search_top_k: Some(SEARCH_TOP_K),
|
||||
..Default::default()
|
||||
&ConclusionQuery {
|
||||
query,
|
||||
top_k: Some(SEARCH_TOP_K),
|
||||
distance: None,
|
||||
filters: Some(json!({ "observer_id": peer, "observed_id": peer })),
|
||||
},
|
||||
).await {
|
||||
Ok(ctx) => Json(json!({
|
||||
"conclusions": ctx.get("conclusions").cloned().unwrap_or(Value::Array(vec![])),
|
||||
})).into_response(),
|
||||
// No peer in Honcho yet ⇒ nothing was ever derived: distinguish it from
|
||||
// "nothing matches" so the debug page can say which of the two it is.
|
||||
Err(e @ HonchoError::Http { status: 404, .. }) => {
|
||||
debug!("honcho /search: no context for peer '{peer}' yet: {e}");
|
||||
Json(json!({ "conclusions": [], "empty": true })).into_response()
|
||||
}
|
||||
Ok(conclusions) => Json(json!({ "conclusions": conclusions })).into_response(),
|
||||
Err(e) => honcho_error(&web, &caller, &e).await,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user