/
GreyCat
/
openhuman
Обзор
Документация
Войти
/
GreyCat
/
openhuman
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
app/src/utils/tauriCommands/memoryTree.ts
1 002 строки
36 KB
Steven Enamakel
refactor: rename tabs, flatten composer, polish UI (#3611)
12 июн 2026, 11:05
Не верифицирован
12 июн 2026, 11:05
5bf7dae
Код
Авторство
О чём код?
/** * memory_tree subsystem commands. * * Thin wrappers over the `openhuman.memory_tree_*` JSON-RPC surface that * powers the Memory tab and the Settings → AI backend chooser. Method * shapes mirror the Rust handlers in `src/openhuman/memory/tree/read_rpc.rs` * and `schemas.rs`. * * Responses come back wrapped by `RpcOutcome::single_log` as * `{ result: <T>, logs: string[] }` (single-log envelope). Each helper * unwraps `result` so callers see the bare value the Rust handler * returned, falling back gracefully if a future handler stops emitting * logs and the bare value flows through. * * Logging convention: `[memory-tree-rpc]` prefix for grep-friendly tracing * per the project debug-logging rule. */ import { callCoreRpc } from '../../services/coreRpcClient'; // ── Public types — match the memory_tree RPC contract ──────────────────── /** * Source kind values the Rust core uses for canonical chunk metadata. * The list is closed for the surfaces the Memory tab cares about, but * the wire type is `string` so any future kind round-trips through the * UI without a recompile. */ export type SourceKind = 'email' | 'chat' | 'screen' | 'voice' | 'doc'; /** Chunk lifecycle phase as emitted by the admission gate. */ export type LifecycleStatus = 'admitted' | 'buffered' | 'pending_extraction' | 'dropped'; /** * Canonical entity-kind strings emitted by the entity index. Kept * permissive (`string`) on the Rust side; the TS union is the curated * subset the UI knows how to render. */ export type EntityKind = | 'person' | 'organization' | 'location' | 'event' | 'product' | 'datetime' | 'technology' | 'artifact' | 'quantity' | 'misc'; /** * A single chunk in the memory tree — one user-visible message-sized unit * (an email, a chat turn, a doc page, a transcribed voice clip). * * Wire shape mirrors Rust's [`ChunkRow`](src/openhuman/memory/tree/read_rpc.rs) * — body is replaced with a `≤500-char preview` plus a flag indicating * whether the row has an embedding. */ export interface Chunk { id: string; source_kind: SourceKind; source_id: string; source_ref?: string; owner: string; timestamp_ms: number; token_count: number; lifecycle_status: LifecycleStatus; content_path?: string; /** Up to 500 chars; used as the result-list subject preview. */ content_preview?: string; has_embedding: boolean; /** Hierarchical: ["person/Steve-Enamakel", "organization/TinyHumans"]. */ tags: string[]; } export interface ChunkFilter { source_kinds?: string[]; source_ids?: string[]; entity_ids?: string[]; since_ms?: number; until_ms?: number; query?: string; limit?: number; offset?: number; } export interface ListChunksResponse { chunks: Chunk[]; total: number; } /** * Distinct ingest source as returned by `memory_tree_list_sources`. * * `lifecycle_status` is **optional** — the Rust handler does not emit it * (it's a UI-derived aggregate), but the navigator pane wants a per-source * dot color. Consumers compute it from chunk-level state and pass it in, * or omit it and the UI falls back to a neutral dot. */ export interface Source { source_id: string; /** Un-slugged readable; user-email stripped when `user_email_hint` matched. */ display_name: string; source_kind: string; chunk_count: number; most_recent_ms: number; lifecycle_status?: LifecycleStatus; } export interface EntityRef { /** Canonical id (e.g. `person:Steven Enamakel`, `email:alice@example.com`). */ entity_id: string; kind: string; surface: string; count: number; } export interface ScoreSignal { name: string; weight: number; value: number; } export interface ScoreBreakdown { signals: ScoreSignal[]; total: number; threshold: number; kept: boolean; llm_consulted: boolean; } export interface RecallResponse { chunks: Chunk[]; scores: number[]; } /** * Response shape for `memory_tree_delete_chunk`. The Rust handler also * surfaces the number of dependent rows removed so UIs can render a * detailed "purged X / Y / Z" toast. */ export interface DeleteChunkResponse { deleted: boolean; score_rows_removed: number; entity_index_rows_removed: number; } /** Backend selector value. */ export type LlmBackend = 'cloud' | 'local'; export interface LlmResponse { current: LlmBackend; } /** * Wire shape for `openhuman.memory_tree_set_llm`. * * `backend` is required and always overwrites `memory_tree.llm_backend`. * * The three model fields are optional; absent means "leave the * corresponding `memory_tree.*_model` config key untouched", present * means "overwrite it". This lets the UI flip the backend without * touching models, or persist a per-role model selection without having * to re-supply every other model id. Field names are snake_case to match * the Rust `SetLlmRequest` struct verbatim — the wrapper does not * translate. */ export interface SetLlmRequest { backend: LlmBackend; cloud_model?: string; extract_model?: string; summariser_model?: string; } // ── Envelope unwrap helper ──────────────────────────────────────────────── /** * Internal envelope shape produced by `RpcOutcome::single_log` on the * Rust side. Every read_rpc handler emits at least one log line, so the * shape will be `{ result, logs }` in practice — but we keep the * fallback path for defensive parsing. */ interface ResultEnvelope<T> { result?: T; logs?: string[]; } function unwrapResult<T>(resp: T | ResultEnvelope<T>): T { if (resp && typeof resp === 'object' && 'result' in resp) { return (resp as ResultEnvelope<T>).result as T; } return resp as T; } // ── memory_tree_list_chunks ────────────────────────────────────────────── /** * Paginated chunk listing with optional filters. Backed by * `openhuman.memory_tree_list_chunks`. */ export async function memoryTreeListChunks(filter: ChunkFilter): Promise<ListChunksResponse> { console.debug('[memory-tree-rpc] memoryTreeListChunks: entry filter=%o', filter); const resp = await callCoreRpc<ListChunksResponse | ResultEnvelope<ListChunksResponse>>({ method: 'openhuman.memory_tree_list_chunks', params: filter, }); const out = unwrapResult(resp); console.debug( '[memory-tree-rpc] memoryTreeListChunks: exit n=%d total=%d', out.chunks?.length ?? 0, out.total ?? 0 ); return out; } // ── memory_tree_list_sources ───────────────────────────────────────────── /** * Distinct (source_kind, source_id) pairs with chunk counts and most-recent * timestamps. `user_email_hint` (when supplied) tells the Rust handler to * strip that address from email-thread display names. */ export async function memoryTreeListSources(userEmailHint?: string): Promise<Source[]> { console.debug( '[memory-tree-rpc] memoryTreeListSources: entry hint=%s', userEmailHint ?? '<none>' ); const params = userEmailHint ? { user_email_hint: userEmailHint } : {}; const resp = await callCoreRpc<Source[] | ResultEnvelope<Source[]>>({ method: 'openhuman.memory_tree_list_sources', params, }); const out = unwrapResult(resp); console.debug('[memory-tree-rpc] memoryTreeListSources: exit n=%d', out?.length ?? 0); return out ?? []; } // ── memory_tree_search ─────────────────────────────────────────────────── /** * Keyword `LIKE`-search over chunk bodies. Cheap, deterministic; useful * as a fallback when semantic recall is unavailable. */ export async function memoryTreeSearch(query: string, k: number): Promise<Chunk[]> { console.debug('[memory-tree-rpc] memoryTreeSearch: entry query_len=%d k=%d', query.length, k); const resp = await callCoreRpc<Chunk[] | ResultEnvelope<Chunk[]>>({ method: 'openhuman.memory_tree_search', params: { query, k }, }); const out = unwrapResult(resp); console.debug('[memory-tree-rpc] memoryTreeSearch: exit n=%d', out?.length ?? 0); return out ?? []; } // ── memory_tree_recall ─────────────────────────────────────────────────── /** * Semantic recall via the Phase 4 cosine rerank path. Returns leaf chunks * and a parallel `scores` array. */ export async function memoryTreeRecall(query: string, k: number): Promise<RecallResponse> { console.debug('[memory-tree-rpc] memoryTreeRecall: entry query_len=%d k=%d', query.length, k); const resp = await callCoreRpc<RecallResponse | ResultEnvelope<RecallResponse>>({ method: 'openhuman.memory_tree_recall', params: { query, k }, }); const out = unwrapResult(resp); console.debug('[memory-tree-rpc] memoryTreeRecall: exit n=%d', out?.chunks?.length ?? 0); return out ?? { chunks: [], scores: [] }; } // ── memory_tree_entity_index_for ───────────────────────────────────────── /** * All canonical entities indexed against a single chunk (or summary node) id. */ export async function memoryTreeEntityIndexFor(chunkId: string): Promise<EntityRef[]> { console.debug('[memory-tree-rpc] memoryTreeEntityIndexFor: entry chunk_id=%s', chunkId); const resp = await callCoreRpc<EntityRef[] | ResultEnvelope<EntityRef[]>>({ method: 'openhuman.memory_tree_entity_index_for', params: { chunk_id: chunkId }, }); const out = unwrapResult(resp); console.debug('[memory-tree-rpc] memoryTreeEntityIndexFor: exit n=%d', out?.length ?? 0); return out ?? []; } // ── memory_tree_chunks_for_entity ──────────────────────────────────────── /** * Inverse of `memoryTreeEntityIndexFor` — return chunk IDs that reference * the given entity. Used by the Memory tab's People/Topics lenses to * filter the chunk list to those mentioning a selected entity. */ export async function memoryTreeChunksForEntity(entityId: string): Promise<string[]> { console.debug('[memory-tree-rpc] memoryTreeChunksForEntity: entry entity_id=%s', entityId); const resp = await callCoreRpc<string[] | ResultEnvelope<string[]>>({ method: 'openhuman.memory_tree_chunks_for_entity', params: { entity_id: entityId }, }); const out = unwrapResult(resp); console.debug('[memory-tree-rpc] memoryTreeChunksForEntity: exit n=%d', out?.length ?? 0); return out ?? []; } // ── memory_tree_top_entities ───────────────────────────────────────────── /** * Most-frequent canonical entities across the workspace, optionally narrowed * by `kind`. The Rust handler treats `limit` as required; we default to 50 * to match the navigator's lens cardinality. */ export async function memoryTreeTopEntities(kind?: string, limit = 50): Promise<EntityRef[]> { console.debug( '[memory-tree-rpc] memoryTreeTopEntities: entry kind=%s limit=%d', kind ?? '<all>', limit ); const params: Record<string, unknown> = { limit }; if (kind) params.kind = kind; const resp = await callCoreRpc<EntityRef[] | ResultEnvelope<EntityRef[]>>({ method: 'openhuman.memory_tree_top_entities', params, }); const out = unwrapResult(resp); console.debug('[memory-tree-rpc] memoryTreeTopEntities: exit n=%d', out?.length ?? 0); return out ?? []; } // ── memory_tree_chunk_score ────────────────────────────────────────────── /** * Score breakdown stored in `mem_tree_score` for one chunk. Returns * `null` when the chunk has no score row (e.g. it was admitted before * scoring was enabled, or it is a synthesized fixture in tests). */ export async function memoryTreeChunkScore(chunkId: string): Promise<ScoreBreakdown | null> { console.debug('[memory-tree-rpc] memoryTreeChunkScore: entry chunk_id=%s', chunkId); const resp = await callCoreRpc<ScoreBreakdown | null | ResultEnvelope<ScoreBreakdown | null>>({ method: 'openhuman.memory_tree_chunk_score', params: { chunk_id: chunkId }, }); const out = unwrapResult(resp); console.debug('[memory-tree-rpc] memoryTreeChunkScore: exit kept=%o', out?.kept); return out ?? null; } // ── memory_tree_delete_chunk ───────────────────────────────────────────── /** * Purge one chunk plus its score row, entity-index rows, and on-disk .md * file. Idempotent — missing chunk returns `deleted=false`. */ export async function memoryTreeDeleteChunk(chunkId: string): Promise<DeleteChunkResponse> { console.debug('[memory-tree-rpc] memoryTreeDeleteChunk: entry chunk_id=%s', chunkId); const resp = await callCoreRpc<DeleteChunkResponse | ResultEnvelope<DeleteChunkResponse>>({ method: 'openhuman.memory_tree_delete_chunk', params: { chunk_id: chunkId }, }); const out = unwrapResult(resp); console.debug( '[memory-tree-rpc] memoryTreeDeleteChunk: exit deleted=%o score_rows=%d entity_rows=%d', out?.deleted, out?.score_rows_removed, out?.entity_index_rows_removed ); return out; } // ── memory_tree_get_llm / memory_tree_set_llm ──────────────────────────── /** * Read the currently configured LLM backend (`cloud` or `local`). */ export async function memoryTreeGetLlm(): Promise<LlmResponse> { console.debug('[memory-tree-rpc] memoryTreeGetLlm: entry'); const resp = await callCoreRpc<LlmResponse | ResultEnvelope<LlmResponse>>({ method: 'openhuman.memory_tree_get_llm', }); const out = unwrapResult(resp); console.debug('[memory-tree-rpc] memoryTreeGetLlm: exit current=%s', out?.current); return out; } /** * Update the LLM backend selector — and, optionally, per-role model * choices (`cloud_model`, `extract_model`, `summariser_model`) — and * persist the result to `config.toml` in a single atomic write. Survives * sidecar restart. * * Returns the effective backend after the call (the core may downgrade * `local` → `cloud` if the host can't satisfy the local minimums; today * the handler accepts the value verbatim). * * Accepts either a bare backend string (legacy callers) or the full * {@link SetLlmRequest} object, so call-sites that only flip the mode * stay terse while sites that want to persist model picks pass the * extended shape. */ export async function memoryTreeSetLlm( reqOrBackend: LlmBackend | SetLlmRequest ): Promise<LlmResponse> { const params: SetLlmRequest = typeof reqOrBackend === 'string' ? { backend: reqOrBackend } : reqOrBackend; console.debug( '[memory-tree-rpc] memoryTreeSetLlm: entry backend=%s cloud_model=%s extract_model=%s summariser_model=%s', params.backend, params.cloud_model ?? '<none>', params.extract_model ?? '<none>', params.summariser_model ?? '<none>' ); const resp = await callCoreRpc<LlmResponse | ResultEnvelope<LlmResponse>>({ method: 'openhuman.memory_tree_set_llm', params, }); const out = unwrapResult(resp); console.debug('[memory-tree-rpc] memoryTreeSetLlm: exit current=%s', out?.current); return out; } // ── memory_tree_graph_export ──────────────────────────────────────────── /** * Discriminator for graph nodes. `"summary"` is a sealed summary tree * node (Tree mode); `"chunk"` is a raw memory chunk and `"contact"` * is a person entity (Contacts mode). */ export type GraphNodeKind = 'root' | 'source' | 'summary' | 'chunk' | 'contact'; /** * One node in the graph export. Optional fields are populated only * when relevant to the node's `kind`; the UI branches on `kind` and * ignores the rest. */ export interface GraphNode { kind: GraphNodeKind; id: string; /** Display-friendly label (scope, preview snippet, or surface form). */ label: string; // Summary-only ── tree_id?: string; tree_kind?: 'source' | 'topic' | 'global'; tree_scope?: string; level?: number; parent_id?: string | null; child_count?: number; /** Filesystem-safe basename (no `.md`); used to build Obsidian deep links. */ file_basename?: string; // Summary or chunk ── time_range_start_ms?: number; time_range_end_ms?: number; // Contact-only ── /** `"person" | "organization" | …`. */ entity_kind?: string; } /** One explicit edge — used in Contacts mode to link chunks to contacts. */ export interface GraphEdge { from: string; to: string; } export type GraphMode = 'tree' | 'contacts'; export interface GraphExportResponse { nodes: GraphNode[]; /** * Explicit edges. Empty in `tree` mode (each summary node's * `parent_id` carries the edge); chunk→contact mention edges in * `contacts` mode. */ edges: GraphEdge[]; /** Absolute filesystem path to `<workspace>/memory_tree/content/`. */ content_root_abs: string; } /** Response shape for `memory_tree_wipe_all`. */ export interface WipeAllResponse { rows_deleted: number; dirs_removed: string[]; /** * Composio sync-state KV rows deleted. Clearing these (per-connection * cursors + synced-id dedup sets) is what lets the next sync re-fetch * every upstream item instead of skipping ones it's already seen. */ sync_state_cleared: number; } /** * Destructive reset: truncate every `mem_tree_*` table, remove the * on-disk chunk-store directories under the workspace content root, * **and** clear the `composio-sync-state` KV namespace so the next * sync re-fetches every upstream item from scratch (no * synced-id-dedup carry-over). Backed by * `openhuman.memory_tree_wipe_all`. * * Callers can rely on `sync_state_cleared` in the response — a * positive count means the next sync will be a full re-fetch; `0` * means there were no live cursors to drop (e.g. fresh workspace). */ export async function memoryTreeWipeAll(): Promise<WipeAllResponse> { console.debug('[memory-tree-rpc] memoryTreeWipeAll: entry'); const resp = await callCoreRpc<WipeAllResponse | ResultEnvelope<WipeAllResponse>>({ method: 'openhuman.memory_tree_wipe_all', }); const out = unwrapResult(resp); console.debug( '[memory-tree-rpc] memoryTreeWipeAll: exit rows=%d dirs=%o', out.rows_deleted, out.dirs_removed ); return out; } /** Response shape for `memory_tree_reset_tree`. */ export interface ResetTreeResponse { /** Tree-state SQLite rows deleted (summaries + trees + buffers + jobs). */ tree_rows_deleted: number; /** Chunks reset to lifecycle_status = 'pending_extraction'. */ chunks_requeued: number; /** `extract_chunk` jobs enqueued (one per chunk). */ jobs_enqueued: number; } /** * Wipe summary-tree state but keep chunks, raw archive, and sync * state — then re-enqueue every chunk through extraction so the * tree rebuilds without a fresh upstream sync. Backed by * `openhuman.memory_tree_reset_tree`. * * Use after changing the summariser backend (e.g. flipping inert * → real local LLM) to re-summarise existing data on the new * model. */ export async function memoryTreeResetTree(): Promise<ResetTreeResponse> { console.debug('[memory-tree-rpc] memoryTreeResetTree: entry'); const resp = await callCoreRpc<ResetTreeResponse | ResultEnvelope<ResetTreeResponse>>({ method: 'openhuman.memory_tree_reset_tree', }); const out = unwrapResult(resp); console.debug( '[memory-tree-rpc] memoryTreeResetTree: exit tree_rows=%d chunks=%d jobs=%d', out.tree_rows_deleted, out.chunks_requeued, out.jobs_enqueued ); return out; } /** Response shape for `memory_tree_flush_now`. */ export interface FlushNowResponse { enqueued: boolean; stale_buffers: number; } /** * Manually trigger the summary-tree build. Enqueues a `flush_stale` job * with `max_age_secs=0` so every L0 buffer force-seals immediately; the * seal worker runs each through the configured cloud or local * summariser. Backed by `openhuman.memory_tree_flush_now`. * * Safe to spam — same UTC-day dedupe key as the scheduled flush, so * duplicate clicks return `enqueued=false` rather than queuing twice. */ interface FlushSourceResponse { tree_scope: string; seals_fired: number; } export async function memoryTreeFlushSource(sourceScope: string): Promise<FlushSourceResponse> { console.debug('[memory-tree-rpc] memoryTreeFlushSource: entry scope=%s', sourceScope); const resp = await callCoreRpc<FlushSourceResponse | ResultEnvelope<FlushSourceResponse>>({ method: 'openhuman.memory_tree_flush_source', params: { source_scope: sourceScope }, }); const out = unwrapResult(resp); console.debug( '[memory-tree-rpc] memoryTreeFlushSource: exit scope=%s seals=%d', out.tree_scope, out.seals_fired ); return out; } export async function memoryTreeFlushNow(): Promise<FlushNowResponse> { console.debug('[memory-tree-rpc] memoryTreeFlushNow: entry'); const resp = await callCoreRpc<FlushNowResponse | ResultEnvelope<FlushNowResponse>>({ method: 'openhuman.memory_tree_flush_now', }); const out = unwrapResult(resp); console.debug( '[memory-tree-rpc] memoryTreeFlushNow: exit enqueued=%s stale_buffers=%d', out.enqueued, out.stale_buffers ); return out; } /** * Return either the summary tree (parent→child links between sealed * summaries) or the document↔contact graph (chunks linked to person * entities they mention). Backed by `openhuman.memory_tree_graph_export`. */ export async function memoryTreeGraphExport( mode: GraphMode = 'tree' ): Promise<GraphExportResponse> { console.debug('[memory-tree-rpc] memoryTreeGraphExport: entry mode=%s', mode); const resp = await callCoreRpc<GraphExportResponse | ResultEnvelope<GraphExportResponse>>({ method: 'openhuman.memory_tree_graph_export', params: { mode }, }); const out = unwrapResult(resp); console.debug( // Don't log the absolute content root — it embeds the user's // home directory + username and shows up in console logs / bug // reports. The path is still returned to the caller. '[memory-tree-rpc] memoryTreeGraphExport: exit mode=%s n=%d edges=%d', mode, out.nodes?.length ?? 0, out.edges?.length ?? 0 ); return out; } /** Response shape for `memory_tree_obsidian_vault_status`. */ export interface ObsidianVaultStatus { /** * True when the content root (or an ancestor) is already a registered * Obsidian vault, so `obsidian://open?path=` will actually resolve. */ registered: boolean; /** * True when an `obsidian.json` was found and parsed (Obsidian is set up). * Lets the UI offer "Open folder as vault" vs. "Install Obsidian". */ config_found: boolean; /** Absolute filesystem path to `<workspace>/memory_tree/content/`. */ content_root_abs: string; } /** * Best-effort check of whether the memory-tree content root is a registered * Obsidian vault. Called before firing the `obsidian://open?path=` deep link, * which only resolves vaults already in Obsidian's `obsidian.json` registry — * it cannot register a new vault on its own. * * `obsidianConfigDir` optionally overrides where the core looks for * `obsidian.json` (non-standard installs: Flatpak / Snap / portable). Backed * by `openhuman.memory_tree_obsidian_vault_status`. */ export async function memoryTreeObsidianVaultStatus( obsidianConfigDir?: string ): Promise<ObsidianVaultStatus> { console.debug( '[memory-tree-rpc] memoryTreeObsidianVaultStatus: entry override=%s', obsidianConfigDir ? 'set' : 'none' ); const resp = await callCoreRpc<ObsidianVaultStatus | ResultEnvelope<ObsidianVaultStatus>>({ method: 'openhuman.memory_tree_obsidian_vault_status', // Only send the override when present so the core uses its default probe. params: obsidianConfigDir ? { obsidian_config_dir: obsidianConfigDir } : {}, }); const out = unwrapResult(resp); console.debug( '[memory-tree-rpc] memoryTreeObsidianVaultStatus: exit registered=%s config_found=%s', out.registered, out.config_found ); return out; } /** Response shape for `memory_tree_vault_health_check`. */ export interface VaultHealthCheck { /** Absolute filesystem path to `<workspace>/memory_tree/content/`. */ content_root_abs: string; /** True when the vault directory exists on disk. */ exists: boolean; /** True when the vault directory is readable. */ readable: boolean; /** True when a temp-file create+delete probe succeeds in the vault. */ writable: boolean; /** True when Obsidian has this folder (or an ancestor) registered as a vault. */ obsidian_registered: boolean; /** True when pipeline status is not paused and not in error. */ pipeline_healthy: boolean; /** Epoch ms of newest chunk timestamp; zero when no chunks exist yet. */ last_sync_ms: number; } /** * Consolidated onboarding/settings health snapshot for the workspace memory * vault (`<workspace>/memory_tree/content/`). * * Backed by `openhuman.memory_tree_vault_health_check`. */ export async function memoryTreeVaultHealthCheck( obsidianConfigDir?: string ): Promise<VaultHealthCheck> { console.debug( '[memory-tree-rpc] memoryTreeVaultHealthCheck: entry override=%s', obsidianConfigDir ? 'set' : 'none' ); const resp = await callCoreRpc<VaultHealthCheck | ResultEnvelope<VaultHealthCheck>>({ method: 'openhuman.memory_tree_vault_health_check', params: obsidianConfigDir ? { obsidian_config_dir: obsidianConfigDir } : {}, }); const out = unwrapResult(resp); console.debug( '[memory-tree-rpc] memoryTreeVaultHealthCheck: exit exists=%s readable=%s writable=%s obsidian_registered=%s pipeline_healthy=%s', out.exists, out.readable, out.writable, out.obsidian_registered, out.pipeline_healthy ); return out; } /** * #1574 §4b: per-model embedding re-embed backfill status. The AI settings * panel polls this after an embedder change to warn that semantic recall * is reduced until the new embedding space is fully re-embedded, and to * dismiss the warning once the chain drains. Backed by * `openhuman.memory_tree_memory_backfill_status`. */ export interface BackfillStatus { /** True while a re-embed backfill still has work pending. */ in_progress: boolean; /** Count of `reembed_backfill` jobs in ready/running state. */ pending_jobs: number; } export async function memoryTreeBackfillStatus(): Promise<BackfillStatus> { console.debug('[memory-tree-rpc] memoryTreeBackfillStatus: entry'); const resp = await callCoreRpc<BackfillStatus | ResultEnvelope<BackfillStatus>>({ method: 'openhuman.memory_tree_memory_backfill_status', }); const out = unwrapResult(resp); console.debug( '[memory-tree-rpc] memoryTreeBackfillStatus: exit in_progress=%s pending=%d', out.in_progress, out.pending_jobs ); return out; } // ── memory_tree_pipeline_status (#1856 Part 1) ─────────────────────────── /** * Coarse status string emitted by `memory_tree_pipeline_status`. Mapped * verbatim to a colored pill in the status panel — `paused` is the only * state the toggle directly influences. */ export type MemoryTreePipelineStatusKind = | 'running' | 'paused' | 'syncing' | 'error' | 'idle' | 'degraded'; /** * Stable typed failure codes the Rust `health::FailureCode` emits (#002). The * UI maps each to a localized remediation string; `remediation_key` carries * the i18n key directly so the panel renders the core's guidance verbatim. */ export type MemoryTreeFailureCode = | 'budget_exhausted' | 'auth_missing' | 'auth_invalid' | 'embeddings_unconfigured' | 'embedding_dim_mismatch' | 'local_model_unavailable' | 'extraction_timeout' | 'summarizer_unavailable' | 'transient'; /** * Typed pipeline failure (#002 FR-004). Mirrors Rust `health::PipelineFailure`. * `remediation_key` is an i18n key (e.g. `memory.health.remediation.*`); the UI * resolves it via `useT()`. `detail` is a short non-localized diagnostic * string (never a secret) for logs/tooltips. */ export interface MemoryTreePipelineFailure { code: MemoryTreeFailureCode; class: 'transient' | 'unrecoverable'; remediation_key: string; detail?: string; } /** * "The pipeline ran but output quality is reduced" (#002 FR-002/FR-005). * Mirrors Rust `health::DegradedState`. `semantic_recall` true when embeddings * were skipped (no usable provider → recall falls back to recency); * `structure` true when extraction yielded nothing across the board. */ export interface MemoryTreeDegradedState { semantic_recall: boolean; structure: boolean; cause?: MemoryTreePipelineFailure | null; } /** * Per-state job counters returned in {@link MemoryTreePipelineStatus}. Mirrors * the Rust `PipelineJobCounts` struct exactly — snake_case carried through. */ export interface MemoryTreePipelineJobCounts { /** Jobs queued and waiting for a worker. */ ready: number; /** Jobs currently being processed by a worker. */ running: number; /** Jobs that exhausted retries and remain in the table for diagnosis. */ failed: number; } /** * Aggregated Memory Tree health snapshot returned by * `openhuman.memory_tree_pipeline_status`. The UI status panel polls this * (every ~1.5s while syncing, ~4s otherwise) and renders the four tiles * directly from the payload — no client-side derivation required. */ export interface MemoryTreePipelineStatus { /** UI status pill — one of `running` / `paused` / `syncing` / `error` / `idle`. */ status: MemoryTreePipelineStatusKind; /** * Optional human-readable reason. Present when `status` is `paused` * (carries the gate mode) or `error` (carries the failed-job count); * `null` otherwise. */ reason: string | null; /** Epoch ms of the most-recent chunk timestamp. Zero when the store is empty. */ last_sync_ms: number; /** Total `mem_tree_chunks` rows across all sources. */ total_chunks: number; /** Recursive on-disk size of the `wiki/` sub-tree under the memory_tree content root, in bytes. */ wiki_size_bytes: number; /** Snapshot of `mem_tree_jobs` by status. */ pipeline_jobs: MemoryTreePipelineJobCounts; /** Convenience flag: at least one job is currently `running`. */ is_syncing: boolean; /** Convenience flag: scheduler-gate mode is `off`. */ is_paused: boolean; /** * #002 (FR-002/FR-005): degradation snapshot. Optional for back-compat with * older cores that don't emit it (the Rust field is `#[serde(default)]`); * absent ⇒ treat as not degraded. */ degraded?: MemoryTreeDegradedState; /** * #002 (FR-004): the single first blocking/most-significant cause, rendered * verbatim by the panel (resolving `remediation_key`). `null`/absent when * the pipeline is healthy. */ first_blocking_cause?: MemoryTreePipelineFailure | null; /** * #002 (FR-010 / US5): fraction of chunks with ≥1 indexed entity, in * `[0.0, 1.0]`. Near 0 with `total_chunks > 0` ⇒ extraction is producing no * structure ("empty-but-built wiki"). Optional for back-compat. */ extraction_coverage?: number | null; } /** * Fetch the Memory Tree pipeline status snapshot. Cheap and idempotent — * the handler runs three SQL counters + one recursive dir walk. Safe to * poll at ~1.5s intervals while the panel is mounted. * * Backed by `openhuman.memory_tree_pipeline_status` (#1856 Part 1). */ export async function memoryTreePipelineStatus(): Promise<MemoryTreePipelineStatus> { console.debug('[memory-tree-rpc] memoryTreePipelineStatus: entry'); const resp = await callCoreRpc< MemoryTreePipelineStatus | ResultEnvelope<MemoryTreePipelineStatus> >({ method: 'openhuman.memory_tree_pipeline_status', params: {} }); const out = unwrapResult(resp); console.debug( '[memory-tree-rpc] memoryTreePipelineStatus: exit status=%s total=%d syncing=%s paused=%s', out.status, out.total_chunks, out.is_syncing, out.is_paused ); return out; } // ── memory_tree_set_enabled (#1856 Part 1) ─────────────────────────────── /** * Wire shape returned by `openhuman.memory_tree_set_enabled`. `changed=false` * means the persisted mode already matched the request (idempotent toggle). */ export interface MemoryTreeSetEnabledResponse { /** Echo of the requested enabled state (post-write). */ enabled: boolean; /** True when the persisted mode flipped; false when the call was a no-op. */ changed: boolean; /** New scheduler-gate mode as wire string (`auto` / `off`). */ mode: string; } /** * Toggle Memory Tree auto-sync. `enabled=true` flips the scheduler-gate to * `auto`; `enabled=false` flips it to `off`, which pauses every LLM-bound * background worker cooperatively at their next `wait_for_capacity()` * await on the Rust side. * * Backed by `openhuman.memory_tree_set_enabled` (#1856 Part 1). The 20-min * Composio fetch loop is *not* paused by this toggle yet — that lands in * #1856 Part 2. */ export async function memoryTreeSetEnabled( enabled: boolean ): Promise<MemoryTreeSetEnabledResponse> { console.debug('[memory-tree-rpc] memoryTreeSetEnabled: entry enabled=%s', enabled); const resp = await callCoreRpc< MemoryTreeSetEnabledResponse | ResultEnvelope<MemoryTreeSetEnabledResponse> >({ method: 'openhuman.memory_tree_set_enabled', params: { enabled } }); const out = unwrapResult(resp); console.debug( '[memory-tree-rpc] memoryTreeSetEnabled: exit enabled=%s changed=%s mode=%s', out.enabled, out.changed, out.mode ); return out; } // ── Sync Audit Log ───────────────────────────────────────────────── export interface SyncAuditEntry { timestamp: string; source_id: string; source_kind: string; scope: string; items_fetched: number; batches: number; input_tokens: number; output_tokens: number; estimated_cost_usd: number; duration_ms: number; success: boolean; error?: string; } export async function memorySyncAuditLog(): Promise<SyncAuditEntry[]> { const resp = await callCoreRpc< { entries: SyncAuditEntry[] } | ResultEnvelope<{ entries: SyncAuditEntry[] }> >({ method: 'openhuman.memory_sources_sync_audit_log', params: {} }); return unwrapResult(resp).entries ?? []; } // ── memory_sync_status_list (#2763 — per-integration health strip) ─────── /** * Freshness label emitted by `openhuman.memory_sync_status_list`. Snake-case * mirrors the Rust `FreshnessLabel` serde rename. Derived from * `now - last_chunk_at_ms` at RPC time, not stored. */ export type MemorySyncFreshness = 'active' | 'recent' | 'idle'; /** * One row per provider that has produced chunks. Mirrors the Rust * `MemorySyncStatus` struct exactly — snake_case carried through so the * wire payload deserialises without a remap layer. */ export interface MemorySyncStatusRow { /** Provider key — `slack`, `gmail`, `notion`, `discord`, `telegram`, etc. */ provider: string; /** Total chunks in `mem_tree_chunks` for this provider. */ chunks_synced: number; /** Chunks fetched but not yet extracted/embedded. Lifetime metric. */ chunks_pending: number; /** Total chunks in the current sync wave. Zero when no wave is active. */ batch_total: number; /** Of `batch_total`, how many have been processed. */ batch_processed: number; /** Epoch ms of the most-recent chunk for this provider; null if none yet. */ last_chunk_at_ms: number | null; /** Coarse activity label — derived at RPC time. */ freshness: MemorySyncFreshness; } /** * Fetch the per-provider sync-status list. Single SQL query against * `mem_tree_chunks` (GROUP BY source_kind); safe to poll alongside * `memoryTreePipelineStatus` on the same 1.5s / 4s adaptive cadence. * * Backed by `openhuman.memory_sync_status_list` (#1136). Surfaced by the * per-integration health strip in `MemoryTreeStatusPanel` (#2763). */ export async function memorySyncStatusList(): Promise<MemorySyncStatusRow[]> { console.debug('[memory-tree-rpc] memorySyncStatusList: entry'); const resp = await callCoreRpc< { statuses: MemorySyncStatusRow[] } | ResultEnvelope<{ statuses: MemorySyncStatusRow[] }> >({ method: 'openhuman.memory_sync_status_list', params: {} }); const out = unwrapResult(resp); const rows = out.statuses ?? []; console.debug('[memory-tree-rpc] memorySyncStatusList: exit rows=%d', rows.length); return rows; }