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:
@@ -43,7 +43,7 @@ export default {
|
||||
// "What does it remember?" panel (shown once opted in)
|
||||
[`${P}.panel.title`]: 'What Honcho remembers about you',
|
||||
[`${P}.panel.guide_title`]: 'How to use this page',
|
||||
[`${P}.panel.guide_overview`]: 'Overview shows everything Honcho has derived about you so far: your card (key facts), the facts it concluded, and a summary. Nothing to type.',
|
||||
[`${P}.panel.guide_overview`]: 'Overview shows everything Honcho has derived about you so far: your card (key facts), the individual facts, and the full digest it hands to the assistant. Nothing to type.',
|
||||
[`${P}.panel.guide_search`]: 'Search finds the stored facts most relevant to the words you type. Fast and exact — no AI rewrite, you see the raw facts.',
|
||||
[`${P}.panel.guide_ask`]: 'Ask sends your question to Honcho’s AI, which reads your memory and writes an answer in its own words. Slower, but it can connect the dots.',
|
||||
[`${P}.panel.status_title`]: 'Service status',
|
||||
@@ -54,7 +54,7 @@ export default {
|
||||
[`${P}.panel.overview_title`]: 'Overview',
|
||||
[`${P}.panel.card_title`]: 'Your card',
|
||||
[`${P}.panel.facts_title`]: 'Facts',
|
||||
[`${P}.panel.summary_title`]: 'Summary',
|
||||
[`${P}.panel.representation_title`]: 'Representation (what the assistant receives)',
|
||||
[`${P}.panel.no_memory`]: 'Honcho has no memory about you yet — it builds up as you chat.',
|
||||
[`${P}.panel.query_title`]: 'Search or ask',
|
||||
[`${P}.panel.query_hint`]: 'Words to find facts, or a full question for the AI.',
|
||||
@@ -98,7 +98,7 @@ export default {
|
||||
// Pannello "cosa ricorda di te?" (visibile dopo il consenso)
|
||||
[`${P}.panel.title`]: 'Cosa ricorda Honcho di te',
|
||||
[`${P}.panel.guide_title`]: 'Come usare questa pagina',
|
||||
[`${P}.panel.guide_overview`]: 'La panoramica mostra tutto ciò che Honcho ha ricavato su di te finora: la tua scheda (fatti chiave), i fatti dedotti e un riassunto. Non serve scrivere nulla.',
|
||||
[`${P}.panel.guide_overview`]: 'La panoramica mostra tutto ciò che Honcho ha ricavato su di te finora: la tua scheda (fatti chiave), i singoli fatti e il riassunto completo che consegna all’assistente. Non serve scrivere nulla.',
|
||||
[`${P}.panel.guide_search`]: 'Cerca trova i fatti memorizzati più rilevanti per le parole che scrivi. Veloce ed esatto — niente riscritture dell’AI, vedi i fatti grezzi.',
|
||||
[`${P}.panel.guide_ask`]: 'Chiedi invia la tua domanda all’AI di Honcho, che legge la tua memoria e scrive una risposta con parole sue. Più lento, ma sa collegare i puntini.',
|
||||
[`${P}.panel.status_title`]: 'Stato del servizio',
|
||||
@@ -109,7 +109,7 @@ export default {
|
||||
[`${P}.panel.overview_title`]: 'Panoramica',
|
||||
[`${P}.panel.card_title`]: 'La tua scheda',
|
||||
[`${P}.panel.facts_title`]: 'Fatti',
|
||||
[`${P}.panel.summary_title`]: 'Riassunto',
|
||||
[`${P}.panel.representation_title`]: 'Rappresentazione (quella che riceve l’assistente)',
|
||||
[`${P}.panel.no_memory`]: 'Honcho non ha ancora nessun ricordo di te — si costruisce chiacchierando.',
|
||||
[`${P}.panel.query_title`]: 'Cerca o chiedi',
|
||||
[`${P}.panel.query_hint`]: 'Parole per trovare fatti, oppure una domanda completa per l’AI.',
|
||||
@@ -153,7 +153,7 @@ export default {
|
||||
// Panneau « que retient-il de vous ? » (visible après le consentement)
|
||||
[`${P}.panel.title`]: 'Ce que Honcho retient de vous',
|
||||
[`${P}.panel.guide_title`]: 'Comment utiliser cette page',
|
||||
[`${P}.panel.guide_overview`]: 'L’aperçu montre tout ce que Honcho a déduit de vous jusqu’ici : votre fiche (faits clés), les faits conclus et un résumé. Rien à saisir.',
|
||||
[`${P}.panel.guide_overview`]: 'L’aperçu montre tout ce que Honcho a déduit de vous jusqu’ici : votre fiche (faits clés), les faits individuels et la synthèse complète remise à l’assistant. Rien à saisir.',
|
||||
[`${P}.panel.guide_search`]: 'Rechercher trouve les faits stockés les plus pertinents pour les mots saisis. Rapide et exact — pas de réécriture par l’IA, vous voyez les faits bruts.',
|
||||
[`${P}.panel.guide_ask`]: 'Demander envoie votre question à l’IA de Honcho, qui lit votre mémoire et rédige une réponse avec ses mots. Plus lent, mais elle relie les points.',
|
||||
[`${P}.panel.status_title`]: 'État du service',
|
||||
@@ -164,7 +164,7 @@ export default {
|
||||
[`${P}.panel.overview_title`]: 'Aperçu',
|
||||
[`${P}.panel.card_title`]: 'Votre fiche',
|
||||
[`${P}.panel.facts_title`]: 'Faits',
|
||||
[`${P}.panel.summary_title`]: 'Résumé',
|
||||
[`${P}.panel.representation_title`]: 'Représentation (celle que reçoit l’assistant)',
|
||||
[`${P}.panel.no_memory`]: 'Honcho n’a pas encore de mémoire vous concernant — elle se construit au fil des conversations.',
|
||||
[`${P}.panel.query_title`]: 'Rechercher ou demander',
|
||||
[`${P}.panel.query_hint`]: 'Des mots pour trouver des faits, ou une question complète pour l’IA.',
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
// 2. Once opted in (saved flag, not the draft toggle): the "what does Honcho
|
||||
// remember about me?" debug panel, backed by this plugin's own opt-in-gated
|
||||
// endpoints — `GET ${api}/status` (service health + the caller's own
|
||||
// processing queue), `GET ${api}/overview` (card + facts + summary, no
|
||||
// input), and one text field with two actions: `POST ${api}/search` (raw
|
||||
// ranked facts) and `POST ${api}/ask` (Honcho's server-side LLM answers).
|
||||
// The built-in mini-guide explains the difference, because "words → facts"
|
||||
// vs "question → AI answer" is not obvious.
|
||||
// processing queue), `GET ${api}/overview` (card + facts + representation,
|
||||
// no input), and one text field with two actions: `POST ${api}/search`
|
||||
// (raw ranked facts) and `POST ${api}/ask` (Honcho's server-side LLM
|
||||
// answers). The built-in mini-guide explains the difference, because
|
||||
// "words → facts" vs "question → AI answer" is not obvious.
|
||||
//
|
||||
// Errors from these endpoints arrive already localized *and specific* (the
|
||||
// backend forwards the real Honcho transport/HTTP detail) — they are surfaced
|
||||
@@ -37,12 +37,12 @@ export default class HonchoMemoryPage extends HonchoBase {
|
||||
// Debug panel (only used once the *saved* opt-in flag is on).
|
||||
_svc: { state: true }, // null | { ok, latency_ms?, queue? } | { ok:false, error }
|
||||
_svcBusy: { state: true },
|
||||
_ov: { state: true }, // null | { card, conclusions, summary }
|
||||
_ov: { state: true }, // null | { card, representation, conclusions }
|
||||
_ovBusy: { state: true },
|
||||
_ovErr: { state: true }, // string | null
|
||||
_q: { state: true }, // query input value
|
||||
_qBusy: { state: true }, // null | 'search' | 'ask'
|
||||
_qRes: { state: true }, // null | { kind:'search', conclusions, empty } | { kind:'ask', answer }
|
||||
_qRes: { state: true }, // null | { kind:'search', conclusions } | { kind:'ask', answer }
|
||||
_qErr: { state: true }, // string | null
|
||||
};
|
||||
}
|
||||
@@ -145,7 +145,7 @@ export default class HonchoMemoryPage extends HonchoBase {
|
||||
body: JSON.stringify({ query: q }),
|
||||
});
|
||||
this._qRes = kind === 'search'
|
||||
? { kind, conclusions: r?.conclusions ?? [], empty: !!r?.empty }
|
||||
? { kind, conclusions: r?.conclusions ?? [] }
|
||||
: { kind, answer: r?.answer ?? '' };
|
||||
} catch (e) {
|
||||
this._qErr = e.message;
|
||||
@@ -276,19 +276,10 @@ export default class HonchoMemoryPage extends HonchoBase {
|
||||
<div class="mt-2">${body}</div>`;
|
||||
}
|
||||
|
||||
// Normalize the peer card into renderable pieces: an array (or an object with
|
||||
// an array under a known key) becomes items; anything else is shown as JSON.
|
||||
// The backend already unwraps Honcho's `{"peer_card": …}` envelope: the card
|
||||
// arrives as a bare array of fact strings, or null when none was curated.
|
||||
_cardItems(card) {
|
||||
if (card == null) return null;
|
||||
if (Array.isArray(card)) return card.length ? card : null;
|
||||
if (typeof card === 'object') {
|
||||
for (const k of ['card', 'facts', 'items']) {
|
||||
if (Array.isArray(card[k]) && card[k].length) return card[k];
|
||||
}
|
||||
return { raw: JSON.stringify(card, null, 2) };
|
||||
}
|
||||
if (typeof card === 'string') return card.trim() ? [card] : null;
|
||||
return null;
|
||||
return Array.isArray(card) && card.length ? card : null;
|
||||
}
|
||||
|
||||
_renderOverview() {
|
||||
@@ -305,24 +296,22 @@ export default class HonchoMemoryPage extends HonchoBase {
|
||||
} else if (this._ov) {
|
||||
const conclusions = this._ov.conclusions ?? [];
|
||||
const card = this._cardItems(this._ov.card);
|
||||
const summary = (this._ov.summary ?? '').trim();
|
||||
if (!card && !conclusions.length && !summary) {
|
||||
const representation = (this._ov.representation ?? '').trim();
|
||||
if (!card && !conclusions.length && !representation) {
|
||||
body = html`<div class="text-body-secondary" style="font-size:.8rem">${t(`${P}.panel.no_memory`)}</div>`;
|
||||
} else {
|
||||
body = html`
|
||||
${card ? html`
|
||||
<div style="font-size:.75rem; font-weight:600" class="text-body-secondary">${t(`${P}.panel.card_title`)}</div>
|
||||
${Array.isArray(card)
|
||||
? html`<ul class="mb-2" style="font-size:.82rem">${card.map((c, i) => html`<li key=${i}>${typeof c === 'string' ? c : JSON.stringify(c)}</li>`)}</ul>`
|
||||
: html`<pre class="mb-2" style="font-size:.72rem; white-space:pre-wrap">${card.raw}</pre>`}
|
||||
<ul class="mb-2" style="font-size:.82rem">${card.map((c, i) => html`<li key=${i}>${c}</li>`)}</ul>
|
||||
` : nothing}
|
||||
${conclusions.length ? html`
|
||||
<div style="font-size:.75rem; font-weight:600" class="text-body-secondary">${t(`${P}.panel.facts_title`)}</div>
|
||||
<ul class="mb-2" style="font-size:.82rem">${conclusions.map(this._factLi)}</ul>
|
||||
` : nothing}
|
||||
${summary ? html`
|
||||
<div style="font-size:.75rem; font-weight:600" class="text-body-secondary">${t(`${P}.panel.summary_title`)}</div>
|
||||
<div style="font-size:.82rem; white-space:pre-wrap">${summary}</div>
|
||||
${representation ? html`
|
||||
<div style="font-size:.75rem; font-weight:600" class="text-body-secondary">${t(`${P}.panel.representation_title`)}</div>
|
||||
<div style="font-size:.82rem; white-space:pre-wrap">${representation}</div>
|
||||
` : nothing}`;
|
||||
}
|
||||
} else {
|
||||
@@ -347,11 +336,9 @@ export default class HonchoMemoryPage extends HonchoBase {
|
||||
if (this._qErr) {
|
||||
result = html`<div class="alert alert-danger py-2" style="font-size:.8rem">${this._qErr}</div>`;
|
||||
} else if (this._qRes?.kind === 'search') {
|
||||
result = this._qRes.empty
|
||||
? html`<div class="alert alert-info py-2" style="font-size:.8rem">${t(`${P}.panel.no_memory`)}</div>`
|
||||
: this._qRes.conclusions.length
|
||||
? html`<ul style="font-size:.82rem">${this._qRes.conclusions.map(this._factLi)}</ul>`
|
||||
: html`<div class="text-body-secondary" style="font-size:.8rem">${t(`${P}.panel.search_empty`)}</div>`;
|
||||
result = this._qRes.conclusions.length
|
||||
? html`<ul style="font-size:.82rem">${this._qRes.conclusions.map(this._factLi)}</ul>`
|
||||
: html`<div class="text-body-secondary" style="font-size:.8rem">${t(`${P}.panel.search_empty`)}</div>`;
|
||||
} else if (this._qRes?.kind === 'ask') {
|
||||
result = html`
|
||||
<div style="font-size:.75rem; font-weight:600" class="text-body-secondary">${t(`${P}.panel.answer_title`)}</div>
|
||||
|
||||
Reference in New Issue
Block a user