mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
938 lines
34 KiB
TypeScript
938 lines
34 KiB
TypeScript
/**
|
|
* 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 = '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';
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|