From 6db993ecd653fa279f2c4a5b3bb7a471210e2dde Mon Sep 17 00:00:00 2001
From: Steven Enamakel
Date: Tue, 14 Jul 2026 13:41:22 +0000
Subject: [PATCH 01/28] feat(memory): ingest coding-agent sessions
---
Cargo.lock | 23 +-
Cargo.toml | 2 +-
.../intelligence/CodingSessionsCard.tsx | 140 ++++++++++
.../__tests__/CodingSessionsCard.test.tsx | 82 ++++++
app/src/lib/i18n/ar.ts | 14 +
app/src/lib/i18n/bn.ts | 14 +
app/src/lib/i18n/de.ts | 14 +
app/src/lib/i18n/en.ts | 14 +
app/src/lib/i18n/es.ts | 14 +
app/src/lib/i18n/fr.ts | 14 +
app/src/lib/i18n/hi.ts | 14 +
app/src/lib/i18n/id.ts | 14 +
app/src/lib/i18n/it.ts | 15 ++
app/src/lib/i18n/ko.ts | 14 +
app/src/lib/i18n/pl.ts | 14 +
app/src/lib/i18n/pt.ts | 14 +
app/src/lib/i18n/ru.ts | 14 +
app/src/lib/i18n/zh-CN.ts | 14 +
app/src/pages/Brain.tsx | 2 +
app/src/services/memorySourcesService.test.ts | 44 ++++
app/src/services/memorySourcesService.ts | 48 ++++
.../e2e/specs/coding-session-memory.spec.ts | 18 ++
.../specs/coding-session-memory.spec.ts | 17 ++
docs/TEST-COVERAGE-MATRIX.md | 83 +++---
src/openhuman/about_app/catalog_data.rs | 16 ++
src/openhuman/about_app/catalog_tests.rs | 17 ++
.../agent/harness/archivist/recap.rs | 3 +
src/openhuman/memory/tree_source/file.rs | 1 +
.../memory_search/tools/hybrid_search.rs | 7 +-
.../memory_search/tools/vector_search.rs | 1 +
src/openhuman/memory_sources/rpc.rs | 53 ++++
src/openhuman/memory_sources/schemas.rs | 65 +++++
src/openhuman/memory_store/retrieval/mod.rs | 1 +
.../memory_store/tools/raw_chunks.rs | 1 +
src/openhuman/memory_store/traits.rs | 1 +
.../memory_store/trees/store_tests.rs | 1 +
src/openhuman/memory_tree/tree/registry.rs | 2 +
src/openhuman/memory_tree/tree/rpc.rs | 1 +
src/openhuman/tinycortex/ingest.rs | 31 ++-
src/openhuman/tinycortex/mod.rs | 5 +
src/openhuman/tinycortex/parity.rs | 2 +-
src/openhuman/tinycortex/persona.rs | 243 ++++++++++++++++++
tests/coding_sessions_feature.rs | 49 ++++
tests/json_rpc_e2e.rs | 45 ++++
vendor/tinycortex | 2 +-
45 files changed, 1141 insertions(+), 62 deletions(-)
create mode 100644 app/src/components/intelligence/CodingSessionsCard.tsx
create mode 100644 app/src/components/intelligence/__tests__/CodingSessionsCard.test.tsx
create mode 100644 app/test/e2e/specs/coding-session-memory.spec.ts
create mode 100644 app/test/playwright/specs/coding-session-memory.spec.ts
create mode 100644 src/openhuman/tinycortex/persona.rs
create mode 100644 tests/coding_sessions_feature.rs
diff --git a/Cargo.lock b/Cargo.lock
index 170a3a02b7..5df1e9361e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4438,7 +4438,7 @@ dependencies = [
"tar",
"tempfile",
"thiserror 2.0.18",
- "tinyagents",
+ "tinyagents 1.9.0",
"tinychannels",
"tinycortex",
"tinyflows",
@@ -6823,6 +6823,23 @@ dependencies = [
"tracing",
]
+[[package]]
+name = "tinyagents"
+version = "2.0.0"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "chrono",
+ "futures",
+ "reqwest 0.12.28",
+ "serde",
+ "serde_json",
+ "sha2 0.11.0",
+ "thiserror 2.0.18",
+ "tokio",
+ "tracing",
+]
+
[[package]]
name = "tinychannels"
version = "0.1.0"
@@ -6888,7 +6905,7 @@ dependencies = [
"serde_json",
"sha2 0.10.9",
"thiserror 2.0.18",
- "tinyagents",
+ "tinyagents 2.0.0",
"tokio",
"toml 0.8.23",
"tracing",
@@ -6908,7 +6925,7 @@ dependencies = [
"serde",
"serde_json",
"thiserror 2.0.18",
- "tinyagents",
+ "tinyagents 1.9.0",
"tracing",
]
diff --git a/Cargo.toml b/Cargo.toml
index 4168dbe92d..dcd6a0e031 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -88,7 +88,7 @@ tinyagents = { version = "1.7", features = ["sqlite", "repl"] }
# security gating, and the global singleton stay host-side. rusqlite/git2 are
# aligned to the host pins (=0.40 / 0.21) so one bundled SQLite + one libgit2
# link. Keep the version pin in lockstep with the submodule tag.
-tinycortex = { version = "0.1", features = ["git-diff", "sync"] }
+tinycortex = { version = "0.1", features = ["git-diff", "persona", "sync"] }
tinychannels = { version = "0.1", features = ["relay-websocket"] }
# TokenJuice code compressor — AST-aware signature extraction. Optional (C build)
# behind the default `tokenjuice-treesitter` feature; disabling it falls back to
diff --git a/app/src/components/intelligence/CodingSessionsCard.tsx b/app/src/components/intelligence/CodingSessionsCard.tsx
new file mode 100644
index 0000000000..559236ae93
--- /dev/null
+++ b/app/src/components/intelligence/CodingSessionsCard.tsx
@@ -0,0 +1,140 @@
+import { useCallback, useEffect, useMemo, useState } from 'react';
+
+import { useT } from '../../lib/i18n/I18nContext';
+import {
+ type CodingSessionSourceStatus,
+ getCodingSessionStatus,
+ ingestCodingSessions,
+} from '../../services/memorySourcesService';
+import type { ToastNotification } from '../../types/intelligence';
+import Button from '../ui/Button';
+
+interface CodingSessionsCardProps {
+ onToast?: (toast: Omit) => void;
+}
+
+const SOURCE_LABEL_KEYS: Record = {
+ claude_code: 'memorySources.codingSessions.claude',
+ codex: 'memorySources.codingSessions.codex',
+};
+
+export function CodingSessionsCard({ onToast }: CodingSessionsCardProps) {
+ const { t } = useT();
+ const [sources, setSources] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [ingesting, setIngesting] = useState(false);
+ const [error, setError] = useState(null);
+
+ const load = useCallback(async () => {
+ console.debug('[coding-sessions] status: entry');
+ setError(null);
+ try {
+ const next = await getCodingSessionStatus();
+ setSources(next);
+ console.debug('[coding-sessions] status: exit sources=%d', next.length);
+ } catch (cause) {
+ console.error('[coding-sessions] status failed', cause);
+ setError(cause instanceof Error ? cause.message : String(cause));
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ void load();
+ }, [load]);
+
+ const totals = useMemo(
+ () => ({
+ files: sources.reduce((sum, source) => sum + source.session_files, 0),
+ evidence: sources.reduce((sum, source) => sum + source.evidence_units, 0),
+ }),
+ [sources]
+ );
+
+ const ingest = useCallback(async () => {
+ console.debug('[coding-sessions] ingest: entry');
+ setIngesting(true);
+ setError(null);
+ try {
+ const result = await ingestCodingSessions(false);
+ console.debug(
+ '[coding-sessions] ingest: exit processed=%d failed=%d',
+ result.sessions_processed,
+ result.sessions_failed
+ );
+ onToast?.({
+ type: result.sessions_failed > 0 ? 'warning' : 'success',
+ title: t('memorySources.codingSessions.complete'),
+ message: t('memorySources.codingSessions.completeMessage')
+ .replace('{processed}', String(result.sessions_processed))
+ .replace('{observations}', String(result.observations)),
+ });
+ await load();
+ } catch (cause) {
+ console.error('[coding-sessions] ingest failed', cause);
+ const message = cause instanceof Error ? cause.message : String(cause);
+ setError(message);
+ onToast?.({ type: 'error', title: t('memorySources.codingSessions.failed'), message });
+ } finally {
+ setIngesting(false);
+ }
+ }, [load, onToast, t]);
+
+ return (
+
+
+
+
+ {t('memorySources.codingSessions.title')}
+
+
+ {t('memorySources.codingSessions.description')}
+
+
+
+
+
+
+ {sources.map(source => (
+
+
+ {t(SOURCE_LABEL_KEYS[source.kind])}
+
+
+ {source.available
+ ? t('memorySources.codingSessions.counts')
+ .replace('{files}', String(source.session_files))
+ .replace('{evidence}', String(source.evidence_units))
+ : t('memorySources.codingSessions.notFound')}
+
+
+ ))}
+
+
+ {loading && (
+
+ {t('memorySources.codingSessions.scanning')}
+
+ )}
+ {error && (
+
+ {error}
+
+ )}
+
+ );
+}
diff --git a/app/src/components/intelligence/__tests__/CodingSessionsCard.test.tsx b/app/src/components/intelligence/__tests__/CodingSessionsCard.test.tsx
new file mode 100644
index 0000000000..c9633e8392
--- /dev/null
+++ b/app/src/components/intelligence/__tests__/CodingSessionsCard.test.tsx
@@ -0,0 +1,82 @@
+import { fireEvent, screen, waitFor } from '@testing-library/react';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+import * as service from '../../../services/memorySourcesService';
+import { renderWithProviders } from '../../../test/test-utils';
+import { CodingSessionsCard } from '../CodingSessionsCard';
+
+vi.mock('../../../services/memorySourcesService', async () => {
+ const actual = await vi.importActual(
+ '../../../services/memorySourcesService'
+ );
+ return { ...actual, getCodingSessionStatus: vi.fn(), ingestCodingSessions: vi.fn() };
+});
+
+const mockedStatus = vi.mocked(service.getCodingSessionStatus);
+const mockedIngest = vi.mocked(service.ingestCodingSessions);
+
+describe('CodingSessionsCard', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockedStatus.mockResolvedValue([
+ {
+ kind: 'claude_code',
+ available: true,
+ session_files: 2,
+ evidence_units: 4,
+ invalid_files: 0,
+ },
+ { kind: 'codex', available: true, session_files: 3, evidence_units: 7, invalid_files: 0 },
+ ]);
+ });
+
+ it('shows discovered local session counts', async () => {
+ renderWithProviders();
+
+ expect(await screen.findByTestId('coding-session-source-claude_code')).toHaveTextContent(
+ '2 sessions · 4 human turns'
+ );
+ expect(screen.getByTestId('coding-session-source-codex')).toHaveTextContent(
+ '3 sessions · 7 human turns'
+ );
+ expect(screen.getByTestId('coding-sessions-ingest')).toBeEnabled();
+ });
+
+ it('ingests incrementally and reports the distilled observations', async () => {
+ mockedIngest.mockResolvedValue({
+ mode: 'incremental',
+ files_seen: 5,
+ sessions_processed: 4,
+ sessions_skipped: 1,
+ sessions_failed: 0,
+ evidence_units: 11,
+ observations: 6,
+ budget_hit: false,
+ pack_path: '/workspace/persona/PERSONA.md',
+ });
+ const onToast = vi.fn();
+ renderWithProviders();
+
+ fireEvent.click(await screen.findByTestId('coding-sessions-ingest'));
+
+ await waitFor(() => expect(mockedIngest).toHaveBeenCalledWith(false));
+ await waitFor(() =>
+ expect(onToast).toHaveBeenCalledWith(
+ expect.objectContaining({
+ type: 'success',
+ message: '4 sessions produced 6 persona observations.',
+ })
+ )
+ );
+ });
+
+ it('keeps ingestion disabled when no human-authored evidence exists', async () => {
+ mockedStatus.mockResolvedValue([
+ { kind: 'codex', available: false, session_files: 0, evidence_units: 0, invalid_files: 0 },
+ ]);
+ renderWithProviders();
+
+ expect(await screen.findByText('No local history found')).toBeInTheDocument();
+ expect(screen.getByTestId('coding-sessions-ingest')).toBeDisabled();
+ });
+});
diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts
index 676ad9a4ee..e48f7c20a1 100644
--- a/app/src/lib/i18n/ar.ts
+++ b/app/src/lib/i18n/ar.ts
@@ -7067,6 +7067,20 @@ const messages: TranslationMap = {
'flows.delete.confirm': 'حذف',
'flows.delete.deleting': 'جارٍ الحذف…',
'flows.canvas.renameLabel': 'إعادة تسمية سير العمل',
+ 'memorySources.codingSessions.title': 'جلسات وكلاء البرمجة',
+ 'memorySources.codingSessions.description':
+ 'حوّل قرارات وتصحيحات Codex وClaude Code إلى ذاكرة شخصية خاصة.',
+ 'memorySources.codingSessions.ingest': 'استيعاب الجلسات الجديدة',
+ 'memorySources.codingSessions.ingesting': 'جارٍ الاستيعاب…',
+ 'memorySources.codingSessions.claude': 'كلود كود',
+ 'memorySources.codingSessions.codex': 'Codex',
+ 'memorySources.codingSessions.counts': '{files} جلسات · {evidence} مداخلات بشرية',
+ 'memorySources.codingSessions.notFound': 'لم يُعثر على سجل محلي',
+ 'memorySources.codingSessions.scanning': 'جارٍ فحص سجل الجلسات المحلي…',
+ 'memorySources.codingSessions.complete': 'تم استيعاب جلسات البرمجة',
+ 'memorySources.codingSessions.completeMessage':
+ 'أنتجت {processed} جلسات {observations} ملاحظات شخصية.',
+ 'memorySources.codingSessions.failed': 'فشل استيعاب جلسات البرمجة',
};
export default messages;
diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts
index 05add8b3b4..e48944b69d 100644
--- a/app/src/lib/i18n/bn.ts
+++ b/app/src/lib/i18n/bn.ts
@@ -7231,6 +7231,20 @@ const messages: TranslationMap = {
'flows.delete.confirm': 'মুছুন',
'flows.delete.deleting': 'মুছে ফেলা হচ্ছে…',
'flows.canvas.renameLabel': 'ওয়ার্কফ্লো পুনঃনামকরণ করুন',
+ 'memorySources.codingSessions.title': 'কোডিং-এজেন্ট সেশন',
+ 'memorySources.codingSessions.description':
+ 'Codex ও Claude Code-এর সিদ্ধান্ত এবং সংশোধনকে ব্যক্তিগত পারসোনা মেমরিতে রূপ দিন।',
+ 'memorySources.codingSessions.ingest': 'নতুন সেশন গ্রহণ করুন',
+ 'memorySources.codingSessions.ingesting': 'গ্রহণ করা হচ্ছে…',
+ 'memorySources.codingSessions.claude': 'ক্লড কোড',
+ 'memorySources.codingSessions.codex': 'Codex',
+ 'memorySources.codingSessions.counts': '{files}টি সেশন · {evidence}টি মানব বার্তা',
+ 'memorySources.codingSessions.notFound': 'কোনো স্থানীয় ইতিহাস পাওয়া যায়নি',
+ 'memorySources.codingSessions.scanning': 'স্থানীয় সেশন ইতিহাস স্ক্যান করা হচ্ছে…',
+ 'memorySources.codingSessions.complete': 'কোডিং সেশন গ্রহণ সম্পন্ন',
+ 'memorySources.codingSessions.completeMessage':
+ '{processed}টি সেশন থেকে {observations}টি পারসোনা পর্যবেক্ষণ তৈরি হয়েছে।',
+ 'memorySources.codingSessions.failed': 'কোডিং সেশন গ্রহণ ব্যর্থ হয়েছে',
};
export default messages;
diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts
index 4930d7f242..61400bab1c 100644
--- a/app/src/lib/i18n/de.ts
+++ b/app/src/lib/i18n/de.ts
@@ -7446,6 +7446,20 @@ const messages: TranslationMap = {
'flows.delete.confirm': 'Löschen',
'flows.delete.deleting': 'Wird gelöscht…',
'flows.canvas.renameLabel': 'Workflow umbenennen',
+ 'memorySources.codingSessions.title': 'Coding-Agent-Sitzungen',
+ 'memorySources.codingSessions.description':
+ 'Verwandle Entscheidungen und Korrekturen aus Codex und Claude Code in private Persona-Erinnerungen.',
+ 'memorySources.codingSessions.ingest': 'Neue Sitzungen einlesen',
+ 'memorySources.codingSessions.ingesting': 'Wird eingelesen…',
+ 'memorySources.codingSessions.claude': 'Claude-Code-Verlauf',
+ 'memorySources.codingSessions.codex': 'Codex',
+ 'memorySources.codingSessions.counts': '{files} Sitzungen · {evidence} menschliche Beiträge',
+ 'memorySources.codingSessions.notFound': 'Kein lokaler Verlauf gefunden',
+ 'memorySources.codingSessions.scanning': 'Lokaler Sitzungsverlauf wird durchsucht…',
+ 'memorySources.codingSessions.complete': 'Coding-Sitzungen eingelesen',
+ 'memorySources.codingSessions.completeMessage':
+ '{processed} Sitzungen ergaben {observations} Persona-Beobachtungen.',
+ 'memorySources.codingSessions.failed': 'Einlesen der Coding-Sitzungen fehlgeschlagen',
};
export default messages;
diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts
index 028c53b042..4e1c675429 100644
--- a/app/src/lib/i18n/en.ts
+++ b/app/src/lib/i18n/en.ts
@@ -7545,6 +7545,20 @@ const en: TranslationMap = {
'Your AI provider has no API key set. Add one in provider settings to continue.',
'userErrors.scope.chat': 'Chat',
'userErrors.scope.cron': 'Scheduled job',
+ 'memorySources.codingSessions.title': 'Coding-agent sessions',
+ 'memorySources.codingSessions.description':
+ 'Turn your Codex and Claude Code decisions and corrections into private persona memory.',
+ 'memorySources.codingSessions.ingest': 'Ingest new sessions',
+ 'memorySources.codingSessions.ingesting': 'Ingesting…',
+ 'memorySources.codingSessions.claude': 'Claude Code',
+ 'memorySources.codingSessions.codex': 'Codex',
+ 'memorySources.codingSessions.counts': '{files} sessions · {evidence} human turns',
+ 'memorySources.codingSessions.notFound': 'No local history found',
+ 'memorySources.codingSessions.scanning': 'Scanning local session history…',
+ 'memorySources.codingSessions.complete': 'Coding sessions ingested',
+ 'memorySources.codingSessions.completeMessage':
+ '{processed} sessions produced {observations} persona observations.',
+ 'memorySources.codingSessions.failed': 'Coding-session ingestion failed',
};
export default en;
diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts
index 9cbd6020be..2c76388b3d 100644
--- a/app/src/lib/i18n/es.ts
+++ b/app/src/lib/i18n/es.ts
@@ -7380,6 +7380,20 @@ const messages: TranslationMap = {
'flows.delete.confirm': 'Eliminar',
'flows.delete.deleting': 'Eliminando…',
'flows.canvas.renameLabel': 'Cambiar el nombre del flujo de trabajo',
+ 'memorySources.codingSessions.title': 'Sesiones de agentes de programación',
+ 'memorySources.codingSessions.description':
+ 'Convierte tus decisiones y correcciones de Codex y Claude Code en memoria privada de personalidad.',
+ 'memorySources.codingSessions.ingest': 'Ingerir sesiones nuevas',
+ 'memorySources.codingSessions.ingesting': 'Ingiriendo…',
+ 'memorySources.codingSessions.claude': 'Historial de Claude Code',
+ 'memorySources.codingSessions.codex': 'Codex',
+ 'memorySources.codingSessions.counts': '{files} sesiones · {evidence} intervenciones humanas',
+ 'memorySources.codingSessions.notFound': 'No se encontró historial local',
+ 'memorySources.codingSessions.scanning': 'Buscando historial local de sesiones…',
+ 'memorySources.codingSessions.complete': 'Sesiones de programación ingeridas',
+ 'memorySources.codingSessions.completeMessage':
+ '{processed} sesiones produjeron {observations} observaciones de personalidad.',
+ 'memorySources.codingSessions.failed': 'Falló la ingesta de sesiones de programación',
};
export default messages;
diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts
index 21ff77839a..694f634af6 100644
--- a/app/src/lib/i18n/fr.ts
+++ b/app/src/lib/i18n/fr.ts
@@ -7414,6 +7414,20 @@ const messages: TranslationMap = {
'flows.delete.confirm': 'Supprimer',
'flows.delete.deleting': 'Suppression…',
'flows.canvas.renameLabel': 'Renommer le workflow',
+ 'memorySources.codingSessions.title': 'Sessions d’agents de programmation',
+ 'memorySources.codingSessions.description':
+ 'Transformez vos décisions et corrections Codex et Claude Code en mémoire de persona privée.',
+ 'memorySources.codingSessions.ingest': 'Ingérer les nouvelles sessions',
+ 'memorySources.codingSessions.ingesting': 'Ingestion…',
+ 'memorySources.codingSessions.claude': 'Historique Claude Code',
+ 'memorySources.codingSessions.codex': 'Codex',
+ 'memorySources.codingSessions.counts': '{files} sessions · {evidence} interventions humaines',
+ 'memorySources.codingSessions.notFound': 'Aucun historique local trouvé',
+ 'memorySources.codingSessions.scanning': 'Analyse de l’historique local…',
+ 'memorySources.codingSessions.complete': 'Sessions de programmation ingérées',
+ 'memorySources.codingSessions.completeMessage':
+ '{processed} sessions ont produit {observations} observations de persona.',
+ 'memorySources.codingSessions.failed': 'Échec de l’ingestion des sessions de programmation',
};
export default messages;
diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts
index 84e87de0bf..6a3ba29a35 100644
--- a/app/src/lib/i18n/hi.ts
+++ b/app/src/lib/i18n/hi.ts
@@ -7229,6 +7229,20 @@ const messages: TranslationMap = {
'flows.delete.confirm': 'हटाएं',
'flows.delete.deleting': 'हटाया जा रहा है…',
'flows.canvas.renameLabel': 'वर्कफ़्लो का नाम बदलें',
+ 'memorySources.codingSessions.title': 'कोडिंग-एजेंट सत्र',
+ 'memorySources.codingSessions.description':
+ 'Codex और Claude Code के निर्णयों व सुधारों को निजी व्यक्तित्व स्मृति में बदलें।',
+ 'memorySources.codingSessions.ingest': 'नए सत्र शामिल करें',
+ 'memorySources.codingSessions.ingesting': 'शामिल किया जा रहा है…',
+ 'memorySources.codingSessions.claude': 'क्लॉड कोड',
+ 'memorySources.codingSessions.codex': 'Codex',
+ 'memorySources.codingSessions.counts': '{files} सत्र · {evidence} मानवीय संदेश',
+ 'memorySources.codingSessions.notFound': 'कोई स्थानीय इतिहास नहीं मिला',
+ 'memorySources.codingSessions.scanning': 'स्थानीय सत्र इतिहास स्कैन हो रहा है…',
+ 'memorySources.codingSessions.complete': 'कोडिंग सत्र शामिल हो गए',
+ 'memorySources.codingSessions.completeMessage':
+ '{processed} सत्रों से {observations} व्यक्तित्व अवलोकन बने।',
+ 'memorySources.codingSessions.failed': 'कोडिंग सत्र शामिल करना विफल रहा',
};
export default messages;
diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts
index ce8afb3658..15bfbfd056 100644
--- a/app/src/lib/i18n/id.ts
+++ b/app/src/lib/i18n/id.ts
@@ -7263,6 +7263,20 @@ const messages: TranslationMap = {
'flows.delete.confirm': 'Hapus',
'flows.delete.deleting': 'Menghapus…',
'flows.canvas.renameLabel': 'Ganti nama alur kerja',
+ 'memorySources.codingSessions.title': 'Sesi agen pemrograman',
+ 'memorySources.codingSessions.description':
+ 'Ubah keputusan dan koreksi Codex serta Claude Code menjadi memori persona pribadi.',
+ 'memorySources.codingSessions.ingest': 'Serap sesi baru',
+ 'memorySources.codingSessions.ingesting': 'Menyerap…',
+ 'memorySources.codingSessions.claude': 'Riwayat Claude Code',
+ 'memorySources.codingSessions.codex': 'Codex',
+ 'memorySources.codingSessions.counts': '{files} sesi · {evidence} masukan manusia',
+ 'memorySources.codingSessions.notFound': 'Riwayat lokal tidak ditemukan',
+ 'memorySources.codingSessions.scanning': 'Memindai riwayat sesi lokal…',
+ 'memorySources.codingSessions.complete': 'Sesi pemrograman telah diserap',
+ 'memorySources.codingSessions.completeMessage':
+ '{processed} sesi menghasilkan {observations} pengamatan persona.',
+ 'memorySources.codingSessions.failed': 'Gagal menyerap sesi pemrograman',
};
export default messages;
diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts
index 6a3ed01795..dd3355ca73 100644
--- a/app/src/lib/i18n/it.ts
+++ b/app/src/lib/i18n/it.ts
@@ -7370,6 +7370,21 @@ const messages: TranslationMap = {
'flows.delete.confirm': 'Elimina',
'flows.delete.deleting': 'Eliminazione…',
'flows.canvas.renameLabel': 'Rinomina flusso di lavoro',
+ 'memorySources.codingSessions.title': 'Sessioni degli agenti di programmazione',
+ 'memorySources.codingSessions.description':
+ 'Trasforma decisioni e correzioni di Codex e Claude Code in memoria privata della persona.',
+ 'memorySources.codingSessions.ingest': 'Acquisisci nuove sessioni',
+ 'memorySources.codingSessions.ingesting': 'Acquisizione…',
+ 'memorySources.codingSessions.claude': 'Cronologia Claude Code',
+ 'memorySources.codingSessions.codex': 'Codex',
+ 'memorySources.codingSessions.counts': '{files} sessioni · {evidence} interventi umani',
+ 'memorySources.codingSessions.notFound': 'Nessuna cronologia locale trovata',
+ 'memorySources.codingSessions.scanning': 'Scansione della cronologia locale…',
+ 'memorySources.codingSessions.complete': 'Sessioni di programmazione acquisite',
+ 'memorySources.codingSessions.completeMessage':
+ '{processed} sessioni hanno prodotto {observations} osservazioni della persona.',
+ 'memorySources.codingSessions.failed':
+ 'Acquisizione delle sessioni di programmazione non riuscita',
};
export default messages;
diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts
index d873347bd8..2782d1bdff 100644
--- a/app/src/lib/i18n/ko.ts
+++ b/app/src/lib/i18n/ko.ts
@@ -7149,6 +7149,20 @@ const messages: TranslationMap = {
'flows.delete.confirm': '삭제',
'flows.delete.deleting': '삭제 중…',
'flows.canvas.renameLabel': '워크플로 이름 바꾸기',
+ 'memorySources.codingSessions.title': '코딩 에이전트 세션',
+ 'memorySources.codingSessions.description':
+ 'Codex와 Claude Code의 결정 및 수정 사항을 비공개 페르소나 메모리로 변환합니다.',
+ 'memorySources.codingSessions.ingest': '새 세션 수집',
+ 'memorySources.codingSessions.ingesting': '수집 중…',
+ 'memorySources.codingSessions.claude': '클로드 코드',
+ 'memorySources.codingSessions.codex': 'Codex',
+ 'memorySources.codingSessions.counts': '세션 {files}개 · 사용자 입력 {evidence}개',
+ 'memorySources.codingSessions.notFound': '로컬 기록을 찾지 못했습니다',
+ 'memorySources.codingSessions.scanning': '로컬 세션 기록을 검색하는 중…',
+ 'memorySources.codingSessions.complete': '코딩 세션 수집 완료',
+ 'memorySources.codingSessions.completeMessage':
+ '세션 {processed}개에서 페르소나 관찰 {observations}개를 만들었습니다.',
+ 'memorySources.codingSessions.failed': '코딩 세션 수집 실패',
};
export default messages;
diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts
index 553e8e83c4..11f82caa21 100644
--- a/app/src/lib/i18n/pl.ts
+++ b/app/src/lib/i18n/pl.ts
@@ -7339,6 +7339,20 @@ const messages: TranslationMap = {
'flows.delete.confirm': 'Usuń',
'flows.delete.deleting': 'Usuwanie…',
'flows.canvas.renameLabel': 'Zmień nazwę przepływu pracy',
+ 'memorySources.codingSessions.title': 'Sesje agentów programistycznych',
+ 'memorySources.codingSessions.description':
+ 'Zamień decyzje i poprawki z Codex oraz Claude Code w prywatną pamięć persony.',
+ 'memorySources.codingSessions.ingest': 'Wczytaj nowe sesje',
+ 'memorySources.codingSessions.ingesting': 'Wczytywanie…',
+ 'memorySources.codingSessions.claude': 'Historia Claude Code',
+ 'memorySources.codingSessions.codex': 'Codex',
+ 'memorySources.codingSessions.counts': '{files} sesji · {evidence} wypowiedzi użytkownika',
+ 'memorySources.codingSessions.notFound': 'Nie znaleziono lokalnej historii',
+ 'memorySources.codingSessions.scanning': 'Skanowanie lokalnej historii sesji…',
+ 'memorySources.codingSessions.complete': 'Sesje programistyczne wczytane',
+ 'memorySources.codingSessions.completeMessage':
+ '{processed} sesji utworzyło {observations} obserwacji persony.',
+ 'memorySources.codingSessions.failed': 'Nie udało się wczytać sesji programistycznych',
};
export default messages;
diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts
index 52b86b7f72..3c383b2c15 100644
--- a/app/src/lib/i18n/pt.ts
+++ b/app/src/lib/i18n/pt.ts
@@ -7353,6 +7353,20 @@ const messages: TranslationMap = {
'flows.delete.confirm': 'Excluir',
'flows.delete.deleting': 'Excluindo…',
'flows.canvas.renameLabel': 'Renomear fluxo de trabalho',
+ 'memorySources.codingSessions.title': 'Sessões de agentes de programação',
+ 'memorySources.codingSessions.description':
+ 'Transforme decisões e correções do Codex e Claude Code em memória privada de persona.',
+ 'memorySources.codingSessions.ingest': 'Ingerir novas sessões',
+ 'memorySources.codingSessions.ingesting': 'Ingerindo…',
+ 'memorySources.codingSessions.claude': 'Histórico do Claude Code',
+ 'memorySources.codingSessions.codex': 'Codex',
+ 'memorySources.codingSessions.counts': '{files} sessões · {evidence} mensagens humanas',
+ 'memorySources.codingSessions.notFound': 'Nenhum histórico local encontrado',
+ 'memorySources.codingSessions.scanning': 'Verificando o histórico local…',
+ 'memorySources.codingSessions.complete': 'Sessões de programação ingeridas',
+ 'memorySources.codingSessions.completeMessage':
+ '{processed} sessões produziram {observations} observações de persona.',
+ 'memorySources.codingSessions.failed': 'Falha ao ingerir sessões de programação',
};
export default messages;
diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts
index b9b9b939a7..4575ee64bf 100644
--- a/app/src/lib/i18n/ru.ts
+++ b/app/src/lib/i18n/ru.ts
@@ -7309,6 +7309,20 @@ const messages: TranslationMap = {
'flows.delete.confirm': 'Удалить',
'flows.delete.deleting': 'Удаление…',
'flows.canvas.renameLabel': 'Переименовать рабочий процесс',
+ 'memorySources.codingSessions.title': 'Сеансы агентов программирования',
+ 'memorySources.codingSessions.description':
+ 'Превратите решения и исправления из Codex и Claude Code в приватную память персоны.',
+ 'memorySources.codingSessions.ingest': 'Загрузить новые сеансы',
+ 'memorySources.codingSessions.ingesting': 'Загрузка…',
+ 'memorySources.codingSessions.claude': 'Клод Код',
+ 'memorySources.codingSessions.codex': 'Codex',
+ 'memorySources.codingSessions.counts': '{files} сеансов · {evidence} сообщений пользователя',
+ 'memorySources.codingSessions.notFound': 'Локальная история не найдена',
+ 'memorySources.codingSessions.scanning': 'Сканирование локальной истории…',
+ 'memorySources.codingSessions.complete': 'Сеансы программирования загружены',
+ 'memorySources.codingSessions.completeMessage':
+ '{processed} сеансов дали {observations} наблюдений персоны.',
+ 'memorySources.codingSessions.failed': 'Не удалось загрузить сеансы программирования',
};
export default messages;
diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts
index 2e4e33b866..68989bc742 100644
--- a/app/src/lib/i18n/zh-CN.ts
+++ b/app/src/lib/i18n/zh-CN.ts
@@ -6840,6 +6840,20 @@ const messages: TranslationMap = {
'flows.delete.confirm': '删除',
'flows.delete.deleting': '正在删除…',
'flows.canvas.renameLabel': '重命名工作流',
+ 'memorySources.codingSessions.title': '编程智能体会话',
+ 'memorySources.codingSessions.description':
+ '将 Codex 和 Claude Code 中的决策与纠正转化为私有人格记忆。',
+ 'memorySources.codingSessions.ingest': '摄取新会话',
+ 'memorySources.codingSessions.ingesting': '正在摄取…',
+ 'memorySources.codingSessions.claude': '克劳德代码',
+ 'memorySources.codingSessions.codex': 'Codex',
+ 'memorySources.codingSessions.counts': '{files} 个会话 · {evidence} 条用户输入',
+ 'memorySources.codingSessions.notFound': '未找到本地历史记录',
+ 'memorySources.codingSessions.scanning': '正在扫描本地会话历史…',
+ 'memorySources.codingSessions.complete': '编程会话已摄取',
+ 'memorySources.codingSessions.completeMessage':
+ '{processed} 个会话生成了 {observations} 条人格观察。',
+ 'memorySources.codingSessions.failed': '编程会话摄取失败',
};
export default messages;
diff --git a/app/src/pages/Brain.tsx b/app/src/pages/Brain.tsx
index e803a1b849..291493ad5b 100644
--- a/app/src/pages/Brain.tsx
+++ b/app/src/pages/Brain.tsx
@@ -8,6 +8,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
+import { CodingSessionsCard } from '../components/intelligence/CodingSessionsCard';
import GoalsPanel from '../components/intelligence/GoalsPanel';
import IntelligenceSubconsciousTab from '../components/intelligence/IntelligenceSubconsciousTab';
import { MemoryControls } from '../components/intelligence/MemoryControls';
@@ -293,6 +294,7 @@ export default function Brain() {
{activeTab === 'sources' && (
+
)}
diff --git a/app/src/services/memorySourcesService.test.ts b/app/src/services/memorySourcesService.test.ts
index ff45196442..7e5f93bead 100644
--- a/app/src/services/memorySourcesService.test.ts
+++ b/app/src/services/memorySourcesService.test.ts
@@ -4,6 +4,8 @@ import { callCoreRpc } from './coreRpcClient';
import {
addMemorySource,
applyAllIn,
+ getCodingSessionStatus,
+ ingestCodingSessions,
listMemorySources,
type MemorySourceEntry,
removeMemorySource,
@@ -190,4 +192,46 @@ describe('memorySourcesService', () => {
expect(entry.max_tokens_per_sync).toBe(100_000);
expect(entry.max_cost_per_sync_usd).toBe(1.5);
});
+
+ it('discovers Codex and Claude Code session sources', async () => {
+ mockedCall.mockResolvedValue({
+ result: {
+ sources: [
+ { kind: 'codex', available: true, session_files: 2, evidence_units: 5, invalid_files: 0 },
+ ],
+ },
+ logs: [],
+ } as never);
+
+ const sources = await getCodingSessionStatus();
+
+ expect(mockedCall).toHaveBeenCalledWith({
+ method: 'openhuman.memory_sources_coding_session_status',
+ });
+ expect(sources[0]).toMatchObject({ kind: 'codex', evidence_units: 5 });
+ });
+
+ it('requests bounded incremental coding-session ingestion', async () => {
+ mockedCall.mockResolvedValue({
+ result: {
+ mode: 'incremental',
+ files_seen: 2,
+ sessions_processed: 2,
+ sessions_skipped: 0,
+ sessions_failed: 0,
+ evidence_units: 5,
+ observations: 3,
+ budget_hit: false,
+ },
+ logs: [],
+ } as never);
+
+ const result = await ingestCodingSessions(false, 25);
+
+ expect(mockedCall).toHaveBeenCalledWith({
+ method: 'openhuman.memory_sources_ingest_coding_sessions',
+ params: { backfill: false, max_sessions: 25 },
+ });
+ expect(result.sessions_processed).toBe(2);
+ });
});
diff --git a/app/src/services/memorySourcesService.ts b/app/src/services/memorySourcesService.ts
index bcd646a5b7..9bd41b1131 100644
--- a/app/src/services/memorySourcesService.ts
+++ b/app/src/services/memorySourcesService.ts
@@ -201,6 +201,54 @@ export async function applyAllIn(): Promise {
return { sources: data.sources ?? [], sync_triggered: data.sync_triggered ?? 0 };
}
+export interface CodingSessionSourceStatus {
+ kind: 'claude_code' | 'codex';
+ available: boolean;
+ session_files: number;
+ evidence_units: number;
+ invalid_files: number;
+}
+
+export interface CodingSessionIngestResult {
+ mode: 'backfill' | 'incremental';
+ files_seen: number;
+ sessions_processed: number;
+ sessions_skipped: number;
+ sessions_failed: number;
+ evidence_units: number;
+ observations: number;
+ budget_hit: boolean;
+ pack_path?: string | null;
+}
+
+export async function getCodingSessionStatus(): Promise {
+ log('coding_session_status: entry');
+ const resp = await callCoreRpc<{ sources: CodingSessionSourceStatus[] }>({
+ method: 'openhuman.memory_sources_coding_session_status',
+ });
+ const data = unwrap<{ sources: CodingSessionSourceStatus[] }>(resp);
+ log('coding_session_status: exit sources=%d', data.sources?.length ?? 0);
+ return data.sources ?? [];
+}
+
+export async function ingestCodingSessions(
+ backfill = false,
+ maxSessions = 100
+): Promise {
+ log('ingest_coding_sessions: entry backfill=%s max_sessions=%d', backfill, maxSessions);
+ const resp = await callCoreRpc({
+ method: 'openhuman.memory_sources_ingest_coding_sessions',
+ params: { backfill, max_sessions: maxSessions },
+ });
+ const data = unwrap(resp);
+ log(
+ 'ingest_coding_sessions: exit processed=%d failed=%d',
+ data.sessions_processed,
+ data.sessions_failed
+ );
+ return data;
+}
+
/// i18n keys for each source kind's user-visible label. Resolve via
/// `t(SOURCE_KIND_LABEL_KEYS[kind])` in components — keeping the keys
/// as a constant lets the dialog kind-picker render the same labels
diff --git a/app/test/e2e/specs/coding-session-memory.spec.ts b/app/test/e2e/specs/coding-session-memory.spec.ts
new file mode 100644
index 0000000000..365e04b653
--- /dev/null
+++ b/app/test/e2e/specs/coding-session-memory.spec.ts
@@ -0,0 +1,18 @@
+import { waitForApp } from '../helpers/app-helpers';
+import { navigateViaHash } from '../helpers/shared-flows';
+
+describe('Coding-agent session memory', () => {
+ before(async () => {
+ await waitForApp();
+ await navigateViaHash('/brain?tab=sources');
+ });
+
+ it('surfaces Codex and Claude Code as private local memory sources', async () => {
+ const card = await $('[data-testid="coding-sessions-card"]');
+ await card.waitForDisplayed({ timeout: 20_000 });
+ expect(await card.getText()).toContain('Coding-agent sessions');
+ await expect($('[data-testid="coding-session-source-claude_code"]')).toBeDisplayed();
+ await expect($('[data-testid="coding-session-source-codex"]')).toBeDisplayed();
+ await expect($('[data-testid="coding-sessions-ingest"]')).toBeDisplayed();
+ });
+});
diff --git a/app/test/playwright/specs/coding-session-memory.spec.ts b/app/test/playwright/specs/coding-session-memory.spec.ts
new file mode 100644
index 0000000000..88bf35d56f
--- /dev/null
+++ b/app/test/playwright/specs/coding-session-memory.spec.ts
@@ -0,0 +1,17 @@
+import { expect, test } from '@playwright/test';
+
+import { bootAuthenticatedPage, waitForAppReady } from '../helpers/core-rpc';
+
+test.describe('Coding-agent session memory', () => {
+ test('shows Codex and Claude Code discovery on the Brain sources page', async ({ page }) => {
+ await bootAuthenticatedPage(page, 'pw-coding-session-memory', '/brain?tab=sources');
+ await waitForAppReady(page);
+
+ const card = page.getByTestId('coding-sessions-card');
+ await expect(card).toBeVisible({ timeout: 20_000 });
+ await expect(card).toContainText('Coding-agent sessions');
+ await expect(page.getByTestId('coding-session-source-claude_code')).toBeVisible();
+ await expect(page.getByTestId('coding-session-source-codex')).toBeVisible();
+ await expect(page.getByTestId('coding-sessions-ingest')).toBeVisible();
+ });
+});
diff --git a/docs/TEST-COVERAGE-MATRIX.md b/docs/TEST-COVERAGE-MATRIX.md
index 422bbf066f..10f5b09a39 100644
--- a/docs/TEST-COVERAGE-MATRIX.md
+++ b/docs/TEST-COVERAGE-MATRIX.md
@@ -187,7 +187,7 @@ Canonical mapping of every product feature to its test source(s). Drives gap-fil
| 4.2.6 | Background-activity panel (chat-header Background tasks button) | VU+WD | `app/src/pages/conversations/hooks/useBackgroundActivity.test.ts`, `app/src/pages/conversations/components/__tests__/BackgroundActivityRows.test.tsx`, `app/test/e2e/specs/chat-background-activity-panel.spec.ts` | ✅ | View-only panel surfacing this chat's async sub-agents + global cron jobs, subconscious/heartbeat status, and memory syncing; freshness-only "Syncing now" labeling; E2E opens the panel and asserts its sections render and close |
| 4.2.7 | Plan-mode review (Approve / Reject / Send-feedback before execute) | RU+RI+VU | `src/openhuman/plan_review/gate.rs`, `src/openhuman/plan_review/tool.rs`, `src/openhuman/plan_review/schemas.rs`, `tests/json_rpc_e2e.rs`, `app/src/pages/conversations/components/PlanReviewCard.test.tsx`, `app/src/pages/__tests__/Conversations.render.test.tsx` | ✅ | Interactive turns call `request_plan_review`, which parks the LIVE turn on the in-memory `PlanReviewGate` (oneshot) until the user decides; `plan_review_request` socket event drives `PlanReviewCard`, which resolves via `openhuman.plan_review_decide` (approve resumes-and-executes / reject resumes-and-stops / revise resumes-with-feedback). RU covers gate park/resolve/timeout + tool auto-approve + parking; RI covers the decide RPC; VU covers the card + provider wiring. WD E2E (agent-driven park flow) tracked as follow-up |
-| 4.2.8 | Composer attachments (image / video / document; drag-drop + paste) | VU | `app/src/lib/attachments.test.ts`, `app/src/components/chat/__tests__/ChatComposer.test.tsx`, `app/src/pages/__tests__/Conversations.attachments.test.tsx` | 🟡 | Attach affordance gated on the resolved vision tier (images/video need vision; documents flow on any model); video is sampled into still frames client-side and forwarded through the existing `[IMAGE:]` vision path; drag-drop + clipboard-paste reuse the picker ingest. VU covers MIME/kind/limits/marker building + drag-drop + paste; real video decode and the frames→vision round-trip are manual-smoke only (jsdom has no video codec). WD E2E is a follow-up |
+| 4.2.8 | Composer attachments (image / video / document; drag-drop + paste) | VU | `app/src/lib/attachments.test.ts`, `app/src/components/chat/__tests__/ChatComposer.test.tsx`, `app/src/pages/__tests__/Conversations.attachments.test.tsx` | 🟡 | Attach affordance gated on the resolved vision tier (images/video need vision; documents flow on any model); video is sampled into still frames client-side and forwarded through the existing `[IMAGE:]` vision path; drag-drop + clipboard-paste reuse the picker ingest. VU covers MIME/kind/limits/marker building + drag-drop + paste; real video decode and the frames→vision round-trip are manual-smoke only (jsdom has no video codec). WD E2E is a follow-up |
### 4.3 Tool Invocation
@@ -280,27 +280,27 @@ End-to-end coverage of the agent harness via the web-chat RPC surface against an
### 6.3 Sub-agent Orchestration
-| ID | Feature | Layer | Test path(s) | Status | Notes |
-| ----- | ---------------------------------------------------------------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| 6.3.1 | Steer a running sub-agent | RU | `src/openhuman/agent_orchestration/running_subagents.rs`, `src/openhuman/agent_orchestration/tools/steer_subagent.rs` | ✅ | `steer_subagent` injects a steer/collect message into a running async sub-agent's run-queue; registry enforces parent ownership + terminal guard. |
-| 6.3.2 | Wait for a sub-agent result | RU | `src/openhuman/agent_orchestration/running_subagents.rs`, `src/openhuman/agent_orchestration/tools/wait_subagent.rs` | ✅ | `wait_subagent` blocks on the completion `watch` with a timeout; prunes terminal entries, leaves entries intact on timeout. |
-| 6.3.3 | Steer lands in child history | RU | `src/openhuman/agent/harness/subagent_runner/ops_tests.rs::run_queue_steer_lands_in_subagent_history` | ✅ | End-to-end: a queued steer is drained by the child `run_turn_engine` and appears as a `[User steering message]` user turn in the provider request. |
-| 6.3.4 | Subconscious trigger pipeline (normalize → dedupe/rate → gate → queue) | RU+RI | `src/openhuman/subconscious_triggers/`, `tests/subconscious_triggers_e2e.rs` | ✅ | Event→Trigger normalization for cron/user/composio/sub-agent, dedupe TTL + per-source rate limit, LLM gate over `agent::triage`, priority queue with overflow eviction. |
-| 6.3.5 | Long-lived subconscious orchestrator session | RU | `src/openhuman/subconscious/session.rs`, `src/openhuman/subconscious/user_thread.rs` | ✅ | Persistent compressed session backed by a reserved thread; `notify_user` handoff to the user-facing thread; mode→autonomy config parity. |
-| 6.3.6 | Multi-party human↔subconscious↔sub-agent conversation | RI | `tests/subconscious_conversation_e2e.rs` | ✅ | Scripted Gate/SessionExecutor seam drives delegate→sub-agent→merge, failure/retry, interleaving, dedupe, and rate-limit scenarios through the real orchestrator. |
-| 6.3.7 | Full-stack trigger pipeline with mocked LLM | RI | `tests/subconscious_fullstack_e2e.rs` (feature `e2e-test-support`) | ✅ | Real `GatePass`+`LongLivedSession`+`Agent`+sub-agent run against a provider-layer mock (no network); promote/drop, persistence, real `spawn_subagent`. |
-| 6.3.8 | Subconscious Triggers debug/manage panel (Brain) | WD | `app/test/playwright/specs/subconscious-triggers.spec.ts` | ✅ | Brain→Subconscious panel: renders disabled baseline + hint + reserved thread ids; enable toggle → Pipeline Enabled + event_driven + orchestrator running; disable; refresh re-fetches. |
-| 6.3.9 | Vision sub-agent reads attached images | RU | `src/openhuman/agent_registry/agents/loader.rs::vision_agent_loads_on_vision_hint`, `src/openhuman/inference/provider/factory_tests.rs::vision_tier_is_vision_capable`, `src/openhuman/agent/harness/engine/core.rs::gate_tests`, `src/openhuman/agent/multimodal_tests.rs::extract_image_placeholders_pulls_att_tokens_in_order` | ✅ | Orchestrator (non-vision `chat-v1`) keeps the image as a placeholder, delegates to `vision_agent` on the `vision-v1` tier, which rehydrates the on-disk attachment and reads it. Engine gate prefers per-tier `current_model_vision`; turn placeholders forwarded into the sub-agent prompt. |
-| 6.3.10 | Auto-accept contact requests from linked agents | RU | `src/openhuman/agent_orchestration/pairing.rs::{auto_accept_gate_accepts_linked_but_leaves_others_pending,auto_accept_gate_unifies_base58_and_base64_of_same_key,auto_accept_gate_accepts_nothing_with_empty_linked_set,auto_accept_fails_closed_on_unreadable_pairing_store,incoming_pending_requesters_filters_and_resolves_requester}` | ✅ | On an inbound tiny.place `contact_request`, OpenHuman auto-accepts iff the requester is in `linked_agent_ids()` (its own paired agents) and otherwise leaves it pending for the human — the e2e gate that stops the relay dropping a linked agent's `session_info` intro. Fail-closed: a pairing-store read error yields an empty linked set → nothing auto-accepted. base58/base64 encodings of the same key unify via the shared DM-ingest matcher. |
+| ID | Feature | Layer | Test path(s) | Status | Notes |
+| ------ | ---------------------------------------------------------------------- | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| 6.3.1 | Steer a running sub-agent | RU | `src/openhuman/agent_orchestration/running_subagents.rs`, `src/openhuman/agent_orchestration/tools/steer_subagent.rs` | ✅ | `steer_subagent` injects a steer/collect message into a running async sub-agent's run-queue; registry enforces parent ownership + terminal guard. |
+| 6.3.2 | Wait for a sub-agent result | RU | `src/openhuman/agent_orchestration/running_subagents.rs`, `src/openhuman/agent_orchestration/tools/wait_subagent.rs` | ✅ | `wait_subagent` blocks on the completion `watch` with a timeout; prunes terminal entries, leaves entries intact on timeout. |
+| 6.3.3 | Steer lands in child history | RU | `src/openhuman/agent/harness/subagent_runner/ops_tests.rs::run_queue_steer_lands_in_subagent_history` | ✅ | End-to-end: a queued steer is drained by the child `run_turn_engine` and appears as a `[User steering message]` user turn in the provider request. |
+| 6.3.4 | Subconscious trigger pipeline (normalize → dedupe/rate → gate → queue) | RU+RI | `src/openhuman/subconscious_triggers/`, `tests/subconscious_triggers_e2e.rs` | ✅ | Event→Trigger normalization for cron/user/composio/sub-agent, dedupe TTL + per-source rate limit, LLM gate over `agent::triage`, priority queue with overflow eviction. |
+| 6.3.5 | Long-lived subconscious orchestrator session | RU | `src/openhuman/subconscious/session.rs`, `src/openhuman/subconscious/user_thread.rs` | ✅ | Persistent compressed session backed by a reserved thread; `notify_user` handoff to the user-facing thread; mode→autonomy config parity. |
+| 6.3.6 | Multi-party human↔subconscious↔sub-agent conversation | RI | `tests/subconscious_conversation_e2e.rs` | ✅ | Scripted Gate/SessionExecutor seam drives delegate→sub-agent→merge, failure/retry, interleaving, dedupe, and rate-limit scenarios through the real orchestrator. |
+| 6.3.7 | Full-stack trigger pipeline with mocked LLM | RI | `tests/subconscious_fullstack_e2e.rs` (feature `e2e-test-support`) | ✅ | Real `GatePass`+`LongLivedSession`+`Agent`+sub-agent run against a provider-layer mock (no network); promote/drop, persistence, real `spawn_subagent`. |
+| 6.3.8 | Subconscious Triggers debug/manage panel (Brain) | WD | `app/test/playwright/specs/subconscious-triggers.spec.ts` | ✅ | Brain→Subconscious panel: renders disabled baseline + hint + reserved thread ids; enable toggle → Pipeline Enabled + event_driven + orchestrator running; disable; refresh re-fetches. |
+| 6.3.9 | Vision sub-agent reads attached images | RU | `src/openhuman/agent_registry/agents/loader.rs::vision_agent_loads_on_vision_hint`, `src/openhuman/inference/provider/factory_tests.rs::vision_tier_is_vision_capable`, `src/openhuman/agent/harness/engine/core.rs::gate_tests`, `src/openhuman/agent/multimodal_tests.rs::extract_image_placeholders_pulls_att_tokens_in_order` | ✅ | Orchestrator (non-vision `chat-v1`) keeps the image as a placeholder, delegates to `vision_agent` on the `vision-v1` tier, which rehydrates the on-disk attachment and reads it. Engine gate prefers per-tier `current_model_vision`; turn placeholders forwarded into the sub-agent prompt. |
+| 6.3.10 | Auto-accept contact requests from linked agents | RU | `src/openhuman/agent_orchestration/pairing.rs::{auto_accept_gate_accepts_linked_but_leaves_others_pending,auto_accept_gate_unifies_base58_and_base64_of_same_key,auto_accept_gate_accepts_nothing_with_empty_linked_set,auto_accept_fails_closed_on_unreadable_pairing_store,incoming_pending_requesters_filters_and_resolves_requester}` | ✅ | On an inbound tiny.place `contact_request`, OpenHuman auto-accepts iff the requester is in `linked_agent_ids()` (its own paired agents) and otherwise leaves it pending for the human — the e2e gate that stops the relay dropping a linked agent's `session_info` intro. Fail-closed: a pairing-store read error yields an empty linked set → nothing auto-accepted. base58/base64 encodings of the same key unify via the shared DM-ingest matcher. |
### 6.4 Managed Cloud File Storage
-| ID | Feature | Layer | Test path(s) | Status | Notes |
-| ----- | ---------------------------------------------------------------- | ----- | ------------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| 6.4.1 | `storage_upload_file` (multipart, quota/TTL args, path safety) | RU | `src/openhuman/file_storage/tools_tests.rs` | ✅ | wiremock upload against the backend envelope; rejects workspace-escaping/symlinked paths, bad visibility/ttl; surfaces backend errors (e.g. insufficient balance). |
-| 6.4.2 | `storage_download_file` (redirect follow + persist to workspace) | RU | `src/openhuman/file_storage/tools_tests.rs` | ✅ | Follows the 302 to the presigned URL, persists bytes under the action dir, honors explicit filename. |
-| 6.4.3 | `storage_list_files` / `storage_get_link` | RU | `src/openhuman/file_storage/tools_tests.rs` | ✅ | List + usage rendering; presigned link generation with expiry arg validation. |
-| 6.4.4 | `storage_set_visibility` / `storage_delete_file` | RU | `src/openhuman/file_storage/tools_tests.rs` | ✅ | Public/private toggle surfaces the stable public URL; delete confirms backend `deleted` flag; both are Write-level tools with external effect. |
+| ID | Feature | Layer | Test path(s) | Status | Notes |
+| ----- | ---------------------------------------------------------------- | ----- | ------------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| 6.4.1 | `storage_upload_file` (multipart, quota/TTL args, path safety) | RU | `src/openhuman/file_storage/tools_tests.rs` | ✅ | wiremock upload against the backend envelope; rejects workspace-escaping/symlinked paths, bad visibility/ttl; surfaces backend errors (e.g. insufficient balance). |
+| 6.4.2 | `storage_download_file` (redirect follow + persist to workspace) | RU | `src/openhuman/file_storage/tools_tests.rs` | ✅ | Follows the 302 to the presigned URL, persists bytes under the action dir, honors explicit filename. |
+| 6.4.3 | `storage_list_files` / `storage_get_link` | RU | `src/openhuman/file_storage/tools_tests.rs` | ✅ | List + usage rendering; presigned link generation with expiry arg validation. |
+| 6.4.4 | `storage_set_visibility` / `storage_delete_file` | RU | `src/openhuman/file_storage/tools_tests.rs` | ✅ | Public/private toggle surfaces the stable public URL; delete confirms backend `deleted` flag; both are Write-level tools with external effect. |
---
@@ -335,12 +335,13 @@ End-to-end coverage of the agent harness via the web-chat RPC surface against an
### 8.2 Memory Handling
-| ID | Feature | Layer | Test path(s) | Status | Notes |
-| ----- | -------------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | --------------------------------------------------------------------------------------------------- |
-| 8.2.1 | Context Injection | RI | `tests/autocomplete_memory_e2e.rs` | ✅ | |
-| 8.2.2 | Memory Consistency | RI | `tests/memory_graph_sync_e2e.rs`, `tests/worker_c_modules_e2e.rs` | ✅ | Worker C RPC E2E verifies memory-tree ingest is reflected by `memory_sync_status_list` |
-| 8.2.3 | Memory Scaling | RU | `src/openhuman/memory/ingestion_tests.rs` | 🟡 | Soak/scale benchmark not asserted |
-| 8.2.4 | Raw-archive sync reconcile | RU+RI | `src/openhuman/memory_sync/sources/rebuild.rs`, `src/openhuman/memory_sync/workspace/periodic.rs`, `tests/json_rpc_e2e.rs` (`json_rpc_memory_sources_reconcile_reports_pending_raw_files`), `tests/memory_sync_pipeline_e2e.rs` | ✅ | Coverage gate + incremental rebuild + workspace periodic scheduler + `memory_sources_reconcile` RPC |
+| ID | Feature | Layer | Test path(s) | Status | Notes |
+| ----- | -------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| 8.2.1 | Context Injection | RI | `tests/autocomplete_memory_e2e.rs` | ✅ | |
+| 8.2.2 | Memory Consistency | RI | `tests/memory_graph_sync_e2e.rs`, `tests/worker_c_modules_e2e.rs` | ✅ | Worker C RPC E2E verifies memory-tree ingest is reflected by `memory_sync_status_list` |
+| 8.2.3 | Memory Scaling | RU | `src/openhuman/memory/ingestion_tests.rs` | 🟡 | Soak/scale benchmark not asserted |
+| 8.2.4 | Raw-archive sync reconcile | RU+RI | `src/openhuman/memory_sync/sources/rebuild.rs`, `src/openhuman/memory_sync/workspace/periodic.rs`, `tests/json_rpc_e2e.rs` (`json_rpc_memory_sources_reconcile_reports_pending_raw_files`), `tests/memory_sync_pipeline_e2e.rs` | ✅ | Coverage gate + incremental rebuild + workspace periodic scheduler + `memory_sources_reconcile` RPC |
+| 8.2.5 | Coding-session persona ingestion | RU+RI+VU+WD | `src/openhuman/tinycortex/persona.rs`, `tests/coding_sessions_feature.rs`, `tests/json_rpc_e2e.rs`, `app/src/components/intelligence/__tests__/CodingSessionsCard.test.tsx`, `app/src/services/memorySourcesService.test.ts`, `app/test/e2e/specs/coding-session-memory.spec.ts`, `app/test/playwright/specs/coding-session-memory.spec.ts` | ✅ | Discovers Codex and Claude Code histories, excludes machine-authored turns, exposes status/ingest RPCs, and surfaces incremental ingestion on Brain > Sources |
### 8.3 Memory Retrieval Benchmarks
@@ -403,13 +404,13 @@ End-to-end coverage of the agent harness via the web-chat RPC surface against an
### 10.1 Integration Setup
-| ID | Feature | Layer | Test path(s) | Status | Notes |
-| ------ | ------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| 10.1.1 | Telegram Connection | WD | `telegram-flow.spec.ts` | ✅ | |
-| 10.1.2 | WhatsApp Connection | WD | `app/test/e2e/specs/whatsapp-flow.spec.ts` | ✅ | Was ❌ |
-| 10.1.3 | Gmail Connection | WD | `gmail-flow.spec.ts` | ✅ | |
-| 10.1.4 | Slack Connection | WD | `app/test/e2e/specs/slack-flow.spec.ts` | ✅ | Was ❌ |
-| 10.1.5 | Yuanbao Connection | RU | `src/openhuman/channels/providers/yuanbao/`, `src/openhuman/channels/controllers/ops.rs::tests::connect_yuanbao_*`, `src/openhuman/channels/runtime/startup.rs::yuanbao_secret_tests` | 🟡 | New API-key channel for Tencent Yuanbao. RU covers sign-token preflight (valid/invalid creds, env-override cluster routing), credentials store hydration (incl. stale app_key guard), and WS reconnect/shutdown. No WDIO spec yet — connect-flow UI is rendered via the generic `ChannelSetupModal` already exercised by other channel flow specs. |
+| ID | Feature | Layer | Test path(s) | Status | Notes |
+| ------ | ---------------------------- | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| 10.1.1 | Telegram Connection | WD | `telegram-flow.spec.ts` | ✅ | |
+| 10.1.2 | WhatsApp Connection | WD | `app/test/e2e/specs/whatsapp-flow.spec.ts` | ✅ | Was ❌ |
+| 10.1.3 | Gmail Connection | WD | `gmail-flow.spec.ts` | ✅ | |
+| 10.1.4 | Slack Connection | WD | `app/test/e2e/specs/slack-flow.spec.ts` | ✅ | Was ❌ |
+| 10.1.5 | Yuanbao Connection | RU | `src/openhuman/channels/providers/yuanbao/`, `src/openhuman/channels/controllers/ops.rs::tests::connect_yuanbao_*`, `src/openhuman/channels/runtime/startup.rs::yuanbao_secret_tests` | 🟡 | New API-key channel for Tencent Yuanbao. RU covers sign-token preflight (valid/invalid creds, env-override cluster routing), credentials store hydration (incl. stale app_key guard), and WS reconnect/shutdown. No WDIO spec yet — connect-flow UI is rendered via the generic `ChannelSetupModal` already exercised by other channel flow specs. |
| 10.1.6 | Email (IMAP/SMTP) Connection | RU+VU | `src/openhuman/channels/controllers/definitions_tests.rs::email_*`, `src/openhuman/channels/controllers/ops/connect.rs::email_config_tests`, `src/openhuman/channels/controllers/ops_tests.rs::{persist_email_config_*,disconnect_email_*,connect_email_rejects_invalid_port_*,test_channel_email_rejects_invalid_port_*}`, `app/src/components/channels/CredentialChannelConfig.test.tsx`, `app/src/components/channels/ChannelConfigPanel.test.tsx` | 🟡 | #4280 — native IMAP/SMTP for non-Gmail/Outlook mailboxes surfacing the existing `EmailChannel`. RU covers credentials→`EmailConfig` mapping/defaults, port/sender parsing, definition/validation, config persist + disconnect, and pre-network invalid-port rejection. VU covers the connect form rendering/submit + panel routing. Live IMAP verify + WDIO connect-flow are follow-ups. |
### 10.2 Authentication & Authorization
@@ -422,12 +423,12 @@ End-to-end coverage of the agent harness via the web-chat RPC surface against an
### 10.3 Message Sync & Ingestion
-| ID | Feature | Layer | Test path(s) | Status | Notes |
-| ------ | ------------------------- | ----- | ------------------------------------------------------------------------------------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
-| 10.3.1 | Incoming Message Sync | RU+WD | `src/openhuman/channels/tests/`, `gmail-flow.spec.ts` | ✅ | |
-| 10.3.2 | Message Deduplication | RU | `src/openhuman/channels/tests/` | ✅ | |
+| ID | Feature | Layer | Test path(s) | Status | Notes |
+| ------ | ------------------------- | ----- | ------------------------------------------------------------------------------------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| 10.3.1 | Incoming Message Sync | RU+WD | `src/openhuman/channels/tests/`, `gmail-flow.spec.ts` | ✅ | |
+| 10.3.2 | Message Deduplication | RU | `src/openhuman/channels/tests/` | ✅ | |
| 10.3.3 | WhatsApp Agent Retrieval | RU | `src/openhuman/whatsapp_data/tools/`, `tests/json_rpc_e2e.rs::whatsapp_data_agent_tools_e2e_1341` | ✅ | Three read-only agent tools wrap the local SQLite store; ingest stays internal-only. See [`src/openhuman/whatsapp_data/README.md`](../src/openhuman/whatsapp_data/README.md). |
-| 10.3.4 | Real-Time vs Delayed Sync | RU | `src/openhuman/channels/tests/runtime_dispatch.rs` | ✅ | |
+| 10.3.4 | Real-Time vs Delayed Sync | RU | `src/openhuman/channels/tests/runtime_dispatch.rs` | ✅ | |
### 10.4 Messaging Operations
@@ -483,7 +484,7 @@ End-to-end coverage of the agent harness via the web-chat RPC surface against an
| 11.1.11 | MCP env reconfigure + registry creds | RI/VU | `tests/json_rpc_e2e.rs` (`mcp_clients_registry_settings_roundtrip`), `src/openhuman/mcp_registry/registries/mcp_official.rs`, `app/src/components/channels/mcp/InstalledServerDetail.test.tsx` (#3039) | ✅ | `update_env` persist+reconnect; `registry_settings` get/set with secrets write-only (config-first, env-fallback); reconfigure form validation |
| 11.1.12 | MCP UI surface + setup-agent client | VU/RU | `app/src/components/channels/mcp/InstallDialog.test.tsx`, `app/src/components/channels/mcp/McpServersTab.test.tsx`, `app/src/services/api/mcpClientsApi.test.ts`, `app/src/services/api/mcpSetupApi.test.ts`, `src/openhuman/mcp_registry/{curation,registry,registries/mcp_official}.rs` (#3039, #4272) | ✅ | Skills `?tab=mcp` renders `McpServersTab` (not Coming Soon); auto-connect on install (best-effort); typed `mcpSetupApi` wrapper; curated "perfect server" catalog (declared website + named credential) with official-vendor badge + official-first order; namespace-stripped relevance search + server-side Stdio/Hosted transport filter; clickable Website/Repo links; wired connection health toolbar (Retry all / Disconnect all) (#4272) |
| 11.1.13 | MCP HTTP-remote auth (token / Bearer / OAuth) + redirect resolution | RU/VU | `src/openhuman/mcp_registry/connections.rs` (`build_http_auth*`, `resolve_final_url`), `src/openhuman/mcp_registry/oauth.rs` (PKCE/token/bundle/callback port), `app/src/components/channels/mcp/ConnectAuthModal.test.tsx` (#3495) | ✅ | Bearer/raw scheme + custom headers; redirect-final-URL resolved before auth; OAuth dynamic client registration + PKCE + refresh; tokens MERGED into stored env; credentials stored encrypted locally, never sent to backend |
-| 11.1.14 | MCP "Help & configure" assistant | VU/RU | `app/src/components/channels/mcp/ConfigAssistantPanel.test.tsx`, `app/src/components/channels/mcp/ConfigHelpModal.test.tsx`, `src/openhuman/mcp_registry/ops.rs` (`invoke_config_assist_agent`) (#3495) | ✅ | Server-specific prompt offered as a one-click "Get step-by-step setup help" action (on-demand, no longer auto-run on open — #4272), running an agentic turn scoped to web_search_tool/web_fetch/curl only; markdown-rendered reply; per-MCP chat persisted while on the detail page |
+| 11.1.14 | MCP "Help & configure" assistant | VU/RU | `app/src/components/channels/mcp/ConfigAssistantPanel.test.tsx`, `app/src/components/channels/mcp/ConfigHelpModal.test.tsx`, `src/openhuman/mcp_registry/ops.rs` (`invoke_config_assist_agent`) (#3495) | ✅ | Server-specific prompt offered as a one-click "Get step-by-step setup help" action (on-demand, no longer auto-run on open — #4272), running an agentic turn scoped to web_search_tool/web_fetch/curl only; markdown-rendered reply; per-MCP chat persisted while on the detail page |
| 11.1.15 | Agent uses connected MCP servers in chat | RU | `src/openhuman/agent_registry/agents/loader.rs` (`orchestrator_subagents_include_mcp_agent`, `mcp_agent_drives_connected_servers_without_install_or_shell`, `planner_has_readonly_mcp_discovery_not_execute`), `src/openhuman/agent_registry/agents/orchestrator/prompt.rs` (`connected_mcp_block_*`), `src/openhuman/agent/harness/session/turn_tests.rs` (`mcp_announcement_fires_once_for_new_server`), `src/openhuman/mcp_registry/{tools,connections}.rs` (#3495) | ✅ | `use_mcp_server` delegate → `mcp_agent` worker (discover→list→call); `mcp_registry_list_tools` read-only discovery; orchestrator `## Connected MCP Servers` prompt block + mid-session connect announcement on the user turn; planner read-only MCP discovery (no `tool_call`) |
@@ -498,10 +499,10 @@ End-to-end coverage of the agent harness via the web-chat RPC surface against an
### 11.3 Hosted Orchestration
-| ID | Feature | Layer | Test path(s) | Status | Notes |
-| ------ | ---------------------------------------------------------------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| ID | Feature | Layer | Test path(s) | Status | Notes |
+| ------ | --------------------------------------------------------------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 11.3.1 | Hosted-only orchestration (client = trigger + effects + render) | RI+VU | `tests/orchestration_hosted_client.rs`, `app/src/components/intelligence/TinyPlaceOrchestrationTab.test.tsx`, `app/src/components/orchestration/__tests__/AgentChatPanel.test.tsx` | ✅ | Local wake-graph brain retired (frontend_agent/graph/master_agent/reasoning_agent deleted). Client forwards events to the hosted brain (`POST /orchestration/v1/events`), uploads world-diffs, syncs hosted sessions/messages/steering into the render cache, and executes `send_dm`/`evict` socket effects; cloud-unreachable banner on outage. |
-| 11.3.2 | Direct paid Medulla orchestration with local OpenHuman tools | RU | `src/openhuman/orchestration/medulla.rs`, `src/openhuman/orchestration/schemas.rs` | ✅ | `openhuman.orchestration_run` checks the active paid plan, starts a hosted Medulla cycle, executes requested contact/session/send tools locally, and continues pending/tool-use events to a final result. Mocked HTTP tests cover direct and tool-loop success plus plan, pending, backend-error, unknown-tool, and tool-failure paths. |
+| 11.3.2 | Direct paid Medulla orchestration with local OpenHuman tools | RU | `src/openhuman/orchestration/medulla.rs`, `src/openhuman/orchestration/schemas.rs` | ✅ | `openhuman.orchestration_run` checks the active paid plan, starts a hosted Medulla cycle, executes requested contact/session/send tools locally, and continues pending/tool-use events to a final result. Mocked HTTP tests cover direct and tool-loop success plus plan, pending, backend-error, unknown-tool, and tool-failure paths. |
---
diff --git a/src/openhuman/about_app/catalog_data.rs b/src/openhuman/about_app/catalog_data.rs
index a5ce31534d..fb9bad1b24 100644
--- a/src/openhuman/about_app/catalog_data.rs
+++ b/src/openhuman/about_app/catalog_data.rs
@@ -14,6 +14,12 @@ const DERIVED_TO_BACKEND: Option = Some(CapabilityPrivacy {
destinations: &["OpenHuman backend", "TinyHumans Neocortex"],
});
+const CODING_SESSION_TO_BACKEND: Option = Some(CapabilityPrivacy {
+ leaves_device: true,
+ data_kind: PrivacyDataKind::Raw,
+ destinations: &["Configured OpenHuman inference provider"],
+});
+
// Vision sub-agent ships the attached image (raw pixels) to the managed
// multimodal model for analysis.
const IMAGE_TO_BACKEND: Option = Some(CapabilityPrivacy {
@@ -507,6 +513,16 @@ pub(super) const CAPABILITIES: &[Capability] = &[
status: CapabilityStatus::Beta,
privacy: LOCAL_RAW,
},
+ Capability {
+ id: "intelligence.coding_session_memory",
+ name: "Coding-Agent Session Memory",
+ domain: "memory_sources",
+ category: CapabilityCategory::Intelligence,
+ description: "Discover local Codex and Claude Code session histories, retain only human-authored decisions and corrections, and distill them into a durable TinyCortex persona memory pack. Tool output, reasoning, developer prompts, and subagent traffic are excluded before inference.",
+ how_to: "Brain > Sources > Coding-agent sessions > Ingest new sessions. Programmatic: openhuman.memory_sources_coding_session_status and openhuman.memory_sources_ingest_coding_sessions (RPC).",
+ status: CapabilityStatus::Beta,
+ privacy: CODING_SESSION_TO_BACKEND,
+ },
Capability {
id: "intelligence.memory_sync_schedule",
name: "Memory Sync Schedule",
diff --git a/src/openhuman/about_app/catalog_tests.rs b/src/openhuman/about_app/catalog_tests.rs
index 8618cf65a4..ca5fcb4246 100644
--- a/src/openhuman/about_app/catalog_tests.rs
+++ b/src/openhuman/about_app/catalog_tests.rs
@@ -158,6 +158,7 @@ fn catalog_includes_additional_user_facing_surfaces() {
"intelligence.embedding_provider_test",
"intelligence.github_repo_memory_source",
"intelligence.memory_source_sync_controls",
+ "intelligence.coding_session_memory",
"conversation.subagent_mascots",
] {
assert!(
@@ -167,6 +168,22 @@ fn catalog_includes_additional_user_facing_surfaces() {
}
}
+#[test]
+fn coding_session_memory_discloses_inference_boundary() {
+ let capability = lookup("intelligence.coding_session_memory")
+ .expect("coding-session memory capability registered");
+ assert_eq!(capability.domain, "memory_sources");
+ assert!(capability.description.contains("Codex"));
+ assert!(capability.description.contains("Claude Code"));
+ let privacy = capability.privacy.expect("privacy disclosure");
+ assert!(privacy.leaves_device);
+ assert_eq!(privacy.data_kind, PrivacyDataKind::Raw);
+ assert_eq!(
+ privacy.destinations,
+ &["Configured OpenHuman inference provider"]
+ );
+}
+
/// The two embeddings entries surface a Settings-side configuration panel.
/// They share the same domain (`embeddings`) but are listed under the
/// Intelligence umbrella so they sit next to memory_tree_retrieval / mcp_server
diff --git a/src/openhuman/agent/harness/archivist/recap.rs b/src/openhuman/agent/harness/archivist/recap.rs
index 2ad95b3ec0..add7d9cacf 100644
--- a/src/openhuman/agent/harness/archivist/recap.rs
+++ b/src/openhuman/agent/harness/archivist/recap.rs
@@ -117,6 +117,9 @@ impl ArchivistHook {
tree_kind: TreeKind::Source,
target_level: 0,
token_budget: 2_000,
+ input_token_budget: tinycortex::memory::config::INPUT_TOKEN_BUDGET,
+ overhead_reserve_tokens: tinycortex::memory::config::SUMMARY_OVERHEAD_RESERVE_TOKENS,
+ ask: None,
};
let first = entries.first().map(|e| e.content.as_str()).unwrap_or("");
diff --git a/src/openhuman/memory/tree_source/file.rs b/src/openhuman/memory/tree_source/file.rs
index c7981150ab..080b17a584 100644
--- a/src/openhuman/memory/tree_source/file.rs
+++ b/src/openhuman/memory/tree_source/file.rs
@@ -150,6 +150,7 @@ mod tests {
id: "source:abc".into(),
kind: TreeKind::Source,
scope: scope.into(),
+ ask: None,
root_id: None,
max_level: 0,
status: TreeStatus::Active,
diff --git a/src/openhuman/memory_search/tools/hybrid_search.rs b/src/openhuman/memory_search/tools/hybrid_search.rs
index 462c4d777b..117823e0f9 100644
--- a/src/openhuman/memory_search/tools/hybrid_search.rs
+++ b/src/openhuman/memory_search/tools/hybrid_search.rs
@@ -110,7 +110,12 @@ impl Tool for MemoryHybridSearchTool {
));
}
- let profile = WeightProfile::by_name(&parsed.mode);
+ let profile = WeightProfile::by_name(&parsed.mode).ok_or_else(|| {
+ anyhow::anyhow!(
+ "memory_hybrid_search: unknown mode '{}'; expected balanced, semantic, lexical, or graph_first",
+ parsed.mode
+ )
+ })?;
let limit = parsed.limit.clamp(1, 50);
log::debug!(
diff --git a/src/openhuman/memory_search/tools/vector_search.rs b/src/openhuman/memory_search/tools/vector_search.rs
index ad04fa4121..4fa1065512 100644
--- a/src/openhuman/memory_search/tools/vector_search.rs
+++ b/src/openhuman/memory_search/tools/vector_search.rs
@@ -152,6 +152,7 @@ impl Tool for MemoryVectorSearchTool {
since_ms,
until_ms: None,
limit: Some(1000),
+ offset: None,
source_scope: crate::openhuman::memory::source_scope::current_source_scope(),
exclude_dropped: false,
};
diff --git a/src/openhuman/memory_sources/rpc.rs b/src/openhuman/memory_sources/rpc.rs
index 1910115940..89d2e53e1a 100644
--- a/src/openhuman/memory_sources/rpc.rs
+++ b/src/openhuman/memory_sources/rpc.rs
@@ -6,6 +6,59 @@ use crate::openhuman::memory_sources::registry::{self, MemorySourcePatch};
use crate::openhuman::memory_sources::types::{MemorySourceEntry, SourceKind};
use crate::rpc::RpcOutcome;
+#[derive(Debug, serde::Serialize)]
+pub struct CodingSessionStatusResponse {
+ pub sources: Vec,
+}
+
+pub async fn coding_session_status_rpc() -> Result, String>
+{
+ tracing::debug!("[memory_sources] coding_session_status_rpc: entry");
+ let sources = tokio::task::spawn_blocking(crate::openhuman::tinycortex::coding_session_status)
+ .await
+ .map_err(|error| format!("join coding-session discovery: {error}"))?;
+ tracing::debug!(
+ sources = sources.len(),
+ files = sources
+ .iter()
+ .map(|source| source.session_files)
+ .sum::(),
+ "[memory_sources] coding_session_status_rpc: exit"
+ );
+ Ok(RpcOutcome::new(
+ CodingSessionStatusResponse { sources },
+ vec![],
+ ))
+}
+
+pub async fn ingest_coding_sessions_rpc(
+ req: crate::openhuman::tinycortex::CodingSessionIngestRequest,
+) -> Result, String> {
+ tracing::info!("[memory_sources] ingest_coding_sessions_rpc: entry");
+ let config = crate::openhuman::config::Config::load_or_init()
+ .await
+ .map_err(|error| format!("load config for coding-session ingestion: {error}"))?;
+ // TinyCortex's persona pipeline intentionally carries borrowed path state
+ // and is not `Send`. Drive it from a blocking worker while its async I/O
+ // remains attached to the ambient Tokio runtime, keeping the controller
+ // future itself Send-safe for the registry.
+ let runtime = tokio::runtime::Handle::current();
+ let response = tokio::task::spawn_blocking(move || {
+ runtime.block_on(crate::openhuman::tinycortex::ingest_coding_sessions(
+ &config, req,
+ ))
+ })
+ .await
+ .map_err(|error| format!("join coding-session ingestion: {error}"))?
+ .map_err(|error| format!("ingest coding sessions: {error:#}"))?;
+ tracing::info!(
+ processed = response.sessions_processed,
+ failed = response.sessions_failed,
+ "[memory_sources] ingest_coding_sessions_rpc: exit"
+ );
+ Ok(RpcOutcome::new(response, vec![]))
+}
+
// ── List ──
#[derive(Debug, serde::Serialize)]
diff --git a/src/openhuman/memory_sources/schemas.rs b/src/openhuman/memory_sources/schemas.rs
index 176a733c2c..f3f8c46a48 100644
--- a/src/openhuman/memory_sources/schemas.rs
+++ b/src/openhuman/memory_sources/schemas.rs
@@ -135,6 +135,8 @@ pub fn all_controller_schemas() -> Vec {
schemas("estimate_sync_cost"),
schemas("monthly_cost_summary"),
schemas("apply_all_in"),
+ schemas("coding_session_status"),
+ schemas("ingest_coding_sessions"),
]
}
@@ -200,6 +202,14 @@ pub fn all_registered_controllers() -> Vec {
schema: schemas("apply_all_in"),
handler: handle_apply_all_in,
},
+ RegisteredController {
+ schema: schemas("coding_session_status"),
+ handler: handle_coding_session_status,
+ },
+ RegisteredController {
+ schema: schemas("ingest_coding_sessions"),
+ handler: handle_ingest_coding_sessions,
+ },
]
}
@@ -586,6 +596,48 @@ pub fn schemas(function: &str) -> ControllerSchema {
},
],
},
+ "coding_session_status" => ControllerSchema {
+ namespace: NAMESPACE,
+ function: "coding_session_status",
+ description: "Discover local Codex and Claude Code session histories and report the human-authored evidence available for memory ingestion.",
+ inputs: vec![],
+ outputs: vec![FieldSchema {
+ name: "sources",
+ ty: TypeSchema::Array(Box::new(TypeSchema::Ref("CodingSessionSourceStatus"))),
+ comment: "Discovery and evidence counts for each supported coding-agent session source.",
+ required: true,
+ }],
+ },
+ "ingest_coding_sessions" => ControllerSchema {
+ namespace: NAMESPACE,
+ function: "ingest_coding_sessions",
+ description: "Distill human-authored turns from local Codex and Claude Code sessions into the TinyCortex persona memory layer.",
+ inputs: vec![
+ FieldSchema {
+ name: "backfill",
+ ty: TypeSchema::Bool,
+ comment: "When true, reprocess all discovered sessions; otherwise ingest only changed sessions.",
+ required: false,
+ },
+ FieldSchema {
+ name: "max_sessions",
+ ty: TypeSchema::U64,
+ comment: "Maximum session digests for this run (clamped to 1,000).",
+ required: false,
+ },
+ ],
+ outputs: vec![
+ FieldSchema { name: "mode", ty: TypeSchema::String, comment: "Executed run mode.", required: true },
+ FieldSchema { name: "files_seen", ty: TypeSchema::U64, comment: "Discovered coding-session files.", required: true },
+ FieldSchema { name: "sessions_processed", ty: TypeSchema::U64, comment: "Coding sessions distilled successfully.", required: true },
+ FieldSchema { name: "sessions_skipped", ty: TypeSchema::U64, comment: "Unchanged sessions skipped during an incremental run.", required: true },
+ FieldSchema { name: "sessions_failed", ty: TypeSchema::U64, comment: "Sessions retained for retry after provider failure.", required: true },
+ FieldSchema { name: "evidence_units", ty: TypeSchema::U64, comment: "Human-authored evidence units extracted.", required: true },
+ FieldSchema { name: "observations", ty: TypeSchema::U64, comment: "Persona observations distilled.", required: true },
+ FieldSchema { name: "budget_hit", ty: TypeSchema::Bool, comment: "Whether the run stopped at its session/call budget.", required: true },
+ FieldSchema { name: "pack_path", ty: TypeSchema::Option(Box::new(TypeSchema::String)), comment: "Compiled persona pack path when written.", required: false },
+ ],
+ },
other => panic!("unknown memory_sources schema function: {other}"),
}
}
@@ -677,6 +729,19 @@ fn handle_apply_all_in(_params: Map) -> ControllerFuture {
Box::pin(async move { to_json(rpc::apply_all_in_rpc().await?) })
}
+fn handle_coding_session_status(_params: Map) -> ControllerFuture {
+ Box::pin(async move { to_json(rpc::coding_session_status_rpc().await?) })
+}
+
+fn handle_ingest_coding_sessions(params: Map) -> ControllerFuture {
+ Box::pin(async move {
+ let req = parse_value::(
+ Value::Object(params),
+ )?;
+ to_json(rpc::ingest_coding_sessions_rpc(req).await?)
+ })
+}
+
fn parse_value(v: Value) -> Result {
serde_json::from_value(v).map_err(|e| format!("invalid params: {e}"))
}
diff --git a/src/openhuman/memory_store/retrieval/mod.rs b/src/openhuman/memory_store/retrieval/mod.rs
index 1e7fd632be..b819362b6a 100644
--- a/src/openhuman/memory_store/retrieval/mod.rs
+++ b/src/openhuman/memory_store/retrieval/mod.rs
@@ -128,6 +128,7 @@ impl RetrievalFacade {
since_ms: filters.since_ms,
until_ms: filters.until_ms,
limit: filters.limit,
+ offset: None,
source_scope: None,
exclude_dropped: false,
};
diff --git a/src/openhuman/memory_store/tools/raw_chunks.rs b/src/openhuman/memory_store/tools/raw_chunks.rs
index cdcc3ef430..88ca2e03a2 100644
--- a/src/openhuman/memory_store/tools/raw_chunks.rs
+++ b/src/openhuman/memory_store/tools/raw_chunks.rs
@@ -101,6 +101,7 @@ impl Tool for MemoryStoreRawChunksTool {
since_ms: parsed.since_ms,
until_ms: parsed.until_ms,
limit: parsed.limit,
+ offset: None,
source_scope: crate::openhuman::memory::source_scope::current_source_scope(),
exclude_dropped: false,
};
diff --git a/src/openhuman/memory_store/traits.rs b/src/openhuman/memory_store/traits.rs
index 7a5819fc78..2b9002a56e 100644
--- a/src/openhuman/memory_store/traits.rs
+++ b/src/openhuman/memory_store/traits.rs
@@ -252,6 +252,7 @@ mod tests {
id: "tree-1".into(),
kind: crate::openhuman::memory_store::trees::TreeKind::Topic,
scope: "topic:phoenix".into(),
+ ask: None,
root_id: Some("summary-root".into()),
max_level: 2,
status: crate::openhuman::memory_store::trees::TreeStatus::Active,
diff --git a/src/openhuman/memory_store/trees/store_tests.rs b/src/openhuman/memory_store/trees/store_tests.rs
index e498710efd..50f929ce19 100644
--- a/src/openhuman/memory_store/trees/store_tests.rs
+++ b/src/openhuman/memory_store/trees/store_tests.rs
@@ -16,6 +16,7 @@ fn sample_tree(id: &str, scope: &str) -> Tree {
id: id.to_string(),
kind: TreeKind::Source,
scope: scope.to_string(),
+ ask: None,
root_id: None,
max_level: 0,
status: TreeStatus::Active,
diff --git a/src/openhuman/memory_tree/tree/registry.rs b/src/openhuman/memory_tree/tree/registry.rs
index ec6b7386a8..f4032ccd79 100644
--- a/src/openhuman/memory_tree/tree/registry.rs
+++ b/src/openhuman/memory_tree/tree/registry.rs
@@ -35,6 +35,7 @@ pub fn get_or_create_tree(config: &Config, kind: TreeKind, scope: &str) -> Resul
id: new_tree_id(kind),
kind,
scope: scope.to_string(),
+ ask: None,
root_id: None,
max_level: 0,
status: TreeStatus::Active,
@@ -201,6 +202,7 @@ mod tests {
id: "source:preexisting".into(),
kind: TreeKind::Source,
scope: "slack:#eng".into(),
+ ask: None,
root_id: None,
max_level: 0,
status: TreeStatus::Active,
diff --git a/src/openhuman/memory_tree/tree/rpc.rs b/src/openhuman/memory_tree/tree/rpc.rs
index 88301c201a..153d136fd7 100644
--- a/src/openhuman/memory_tree/tree/rpc.rs
+++ b/src/openhuman/memory_tree/tree/rpc.rs
@@ -139,6 +139,7 @@ pub async fn list_chunks_rpc(
since_ms: req.since_ms,
until_ms: req.until_ms,
limit: req.limit,
+ offset: None,
source_scope: None,
exclude_dropped: false,
};
diff --git a/src/openhuman/tinycortex/ingest.rs b/src/openhuman/tinycortex/ingest.rs
index 0420013e84..ede735fea0 100644
--- a/src/openhuman/tinycortex/ingest.rs
+++ b/src/openhuman/tinycortex/ingest.rs
@@ -1,29 +1,32 @@
//! Host adapters for tinycortex on-demand ingestion.
-use tinycortex::memory::ingest::TreeJobSink;
+use rusqlite::Transaction;
+use tinycortex::memory::ingest::{QueueJobSink, TreeJobSink};
use tinycortex::memory::score::extract::{LlmEntityExtractor, LlmExtractorConfig};
use tinycortex::memory::score::ScoringConfig;
use crate::openhuman::config::Config;
-use crate::openhuman::memory_queue::{self, ExtractChunkPayload, NewJob};
-
-pub struct HostTreeJobSink {
- config: Config,
-}
+pub struct HostTreeJobSink;
impl HostTreeJobSink {
- pub fn new(config: Config) -> Self {
- Self { config }
+ pub fn new(_config: Config) -> Self {
+ Self
}
}
impl TreeJobSink for HostTreeJobSink {
- fn enqueue_extract(&self, chunk_id: &str) -> anyhow::Result<()> {
- let job = NewJob::extract_chunk(&ExtractChunkPayload {
- chunk_id: chunk_id.into(),
- })?;
- memory_queue::enqueue(&self.config, &job)?;
- Ok(())
+ fn enqueue_extract_tx(
+ &self,
+ tx: &Transaction<'_>,
+ chunk_id: &str,
+ default_max_attempts: u32,
+ ) -> anyhow::Result {
+ tracing::trace!(
+ chunk_id,
+ default_max_attempts,
+ "[memory:ingest] enqueue extract job in chunk transaction"
+ );
+ QueueJobSink.enqueue_extract_tx(tx, chunk_id, default_max_attempts)
}
}
diff --git a/src/openhuman/tinycortex/mod.rs b/src/openhuman/tinycortex/mod.rs
index b013e42d94..99d5a36d86 100644
--- a/src/openhuman/tinycortex/mod.rs
+++ b/src/openhuman/tinycortex/mod.rs
@@ -39,6 +39,7 @@ mod embeddings;
mod ingest;
#[cfg(test)]
mod parity;
+mod persona;
mod queue_driver;
mod seal;
mod summariser;
@@ -48,6 +49,10 @@ pub use chat::{build_chat_provider, SeamChatProvider};
pub use config::memory_config_from;
pub use embeddings::SeamEmbedder;
pub use ingest::{context as ingest_context, HostTreeJobSink};
+pub use persona::{
+ coding_session_status, coding_session_status_for_roots, ingest_coding_sessions,
+ CodingSessionIngestRequest, CodingSessionIngestResponse, CodingSessionSourceStatus,
+};
pub use queue_driver::{
classify_worker_error, HostQueueDelegates, WorkerErrorAction, WorkerReport,
};
diff --git a/src/openhuman/tinycortex/parity.rs b/src/openhuman/tinycortex/parity.rs
index aed98124f9..e152c17de7 100644
--- a/src/openhuman/tinycortex/parity.rs
+++ b/src/openhuman/tinycortex/parity.rs
@@ -69,7 +69,7 @@ mod tests {
assert_eq!(bytes.len(), v.len() * 4);
assert_eq!(hex(&bytes), "0000803f000000c00000003f");
// Round-trips exactly.
- assert_eq!(bytes_to_vec(&bytes), v);
+ assert_eq!(bytes_to_vec(&bytes).expect("valid packed f32 bytes"), v);
}
/// P6 — vault paths sanitize IDs to cross-platform-safe filenames. Chunk IDs
diff --git a/src/openhuman/tinycortex/persona.rs b/src/openhuman/tinycortex/persona.rs
new file mode 100644
index 0000000000..f4963ec559
--- /dev/null
+++ b/src/openhuman/tinycortex/persona.rs
@@ -0,0 +1,243 @@
+//! Host orchestration for TinyCortex coding-session persona ingestion.
+
+use std::path::{Path, PathBuf};
+
+use serde::{Deserialize, Serialize};
+use tinycortex::memory::persona::readers::{claude_code, codex, RawSession};
+use tinycortex::memory::persona::state::FileStateStore;
+use tinycortex::memory::persona::{PersonaConfig, Pipeline, RunMode};
+
+use crate::openhuman::config::Config;
+
+const DEFAULT_MAX_SESSIONS: usize = 100;
+const MAX_MAX_SESSIONS: usize = 1_000;
+
+#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
+pub struct CodingSessionSourceStatus {
+ pub kind: String,
+ pub available: bool,
+ pub session_files: usize,
+ pub evidence_units: usize,
+ pub invalid_files: usize,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct CodingSessionIngestRequest {
+ #[serde(default)]
+ pub backfill: bool,
+ #[serde(default = "default_max_sessions")]
+ pub max_sessions: usize,
+}
+
+fn default_max_sessions() -> usize {
+ DEFAULT_MAX_SESSIONS
+}
+
+#[derive(Debug, Clone, Serialize)]
+pub struct CodingSessionIngestResponse {
+ pub mode: String,
+ pub files_seen: usize,
+ pub sessions_processed: usize,
+ pub sessions_skipped: usize,
+ pub sessions_failed: usize,
+ pub evidence_units: usize,
+ pub observations: usize,
+ pub budget_hit: bool,
+ pub pack_path: Option,
+}
+
+fn roots_from_environment() -> (PathBuf, PathBuf) {
+ let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
+ let claude_home = std::env::var_os("CLAUDE_CONFIG_DIR")
+ .map(PathBuf::from)
+ .unwrap_or_else(|| home.join(".claude"));
+ let codex_home = std::env::var_os("CODEX_HOME")
+ .map(PathBuf::from)
+ .unwrap_or_else(|| home.join(".codex"));
+ (claude_home.join("projects"), codex_home.join("sessions"))
+}
+
+fn source_status(
+ kind: &str,
+ root: &Path,
+ discover: impl Fn(&Path) -> Vec,
+ read: impl Fn(&Path) -> anyhow::Result,
+) -> CodingSessionSourceStatus {
+ let files = discover(root);
+ let mut evidence_units = 0;
+ let mut invalid_files = 0;
+ for path in &files {
+ match read(path) {
+ Ok(session) => evidence_units += session.evidence.len(),
+ Err(_error) => {
+ invalid_files += 1;
+ tracing::debug!(
+ source = kind,
+ reason = "read-or-parse-failed",
+ "[memory_persona] skipped unreadable coding session"
+ );
+ }
+ }
+ }
+ CodingSessionSourceStatus {
+ kind: kind.to_string(),
+ available: root.is_dir(),
+ session_files: files.len(),
+ evidence_units,
+ invalid_files,
+ }
+}
+
+pub fn coding_session_status_for_roots(
+ claude_root: &Path,
+ codex_root: &Path,
+) -> Vec {
+ tracing::debug!("[memory_persona] coding session scan: entry");
+ let statuses = vec![
+ source_status(
+ "claude_code",
+ claude_root,
+ claude_code::discover,
+ claude_code::read_session,
+ ),
+ source_status("codex", codex_root, codex::discover, codex::read_session),
+ ];
+ tracing::debug!(
+ files = statuses
+ .iter()
+ .map(|status| status.session_files)
+ .sum::(),
+ evidence = statuses
+ .iter()
+ .map(|status| status.evidence_units)
+ .sum::(),
+ invalid = statuses
+ .iter()
+ .map(|status| status.invalid_files)
+ .sum::(),
+ "[memory_persona] coding session scan: exit"
+ );
+ statuses
+}
+
+pub fn coding_session_status() -> Vec {
+ let (claude_root, codex_root) = roots_from_environment();
+ coding_session_status_for_roots(&claude_root, &codex_root)
+}
+
+pub async fn ingest_coding_sessions(
+ config: &Config,
+ request: CodingSessionIngestRequest,
+) -> anyhow::Result {
+ let (claude_root, codex_root) = roots_from_environment();
+ let max_sessions = request.max_sessions.clamp(1, MAX_MAX_SESSIONS);
+ let mode = if request.backfill {
+ RunMode::Backfill
+ } else {
+ RunMode::Incremental
+ };
+ tracing::info!(
+ mode = if request.backfill {
+ "backfill"
+ } else {
+ "incremental"
+ },
+ max_sessions,
+ "[memory_persona] coding session ingestion: entry"
+ );
+
+ let memory_config = super::memory_config_from(config, config.workspace_dir.clone());
+ let mut persona = PersonaConfig::with_home(
+ dirs::home_dir()
+ .as_deref()
+ .unwrap_or_else(|| Path::new(".")),
+ "OpenHuman user",
+ );
+ persona.claude_code_root = Some(claude_root);
+ persona.codex_root = Some(codex_root);
+ // This product surface is deliberately scoped to coding-session history.
+ // Repository history and instruction files can be wired separately with
+ // their own disclosure and cost controls.
+ persona.project_roots.clear();
+ persona.global_instruction_files.clear();
+ persona.author_emails.clear();
+ persona.run_budget.max_sessions = max_sessions;
+ persona.run_budget.max_llm_calls = max_sessions as u32;
+
+ let provider = super::build_chat_provider(config)?;
+ let summariser = super::HostSummariser::new(config.clone());
+ let store = FileStateStore::open_in_workspace(&config.workspace_dir)?;
+ let report = Pipeline {
+ config: &memory_config,
+ persona: &persona,
+ provider: provider.as_ref(),
+ summariser: &summariser,
+ store: &store,
+ }
+ .run(mode)
+ .await?;
+
+ tracing::info!(
+ files_seen = report.files_seen,
+ sessions_processed = report.sessions_processed,
+ sessions_failed = report.sessions_failed,
+ evidence_units = report.evidence_units,
+ observations = report.observations,
+ budget_hit = report.budget_hit,
+ "[memory_persona] coding session ingestion: exit"
+ );
+ Ok(CodingSessionIngestResponse {
+ mode: report.mode,
+ files_seen: report.files_seen,
+ sessions_processed: report.sessions_processed,
+ sessions_skipped: report.sessions_skipped,
+ sessions_failed: report.sessions_failed,
+ evidence_units: report.evidence_units,
+ observations: report.observations,
+ budget_hit: report.budget_hit,
+ pack_path: report.pack_path,
+ })
+}
+
+#[cfg(test)]
+mod tests {
+ use std::fs;
+
+ use tempfile::tempdir;
+
+ use super::*;
+
+ #[test]
+ fn scans_codex_and_claude_sessions_and_filters_machine_content() {
+ let temp = tempdir().unwrap();
+ let claude = temp.path().join("claude");
+ let codex = temp.path().join("codex/2026/07/14");
+ fs::create_dir_all(&claude).unwrap();
+ fs::create_dir_all(&codex).unwrap();
+ fs::write(
+ claude.join("session.jsonl"),
+ concat!(
+ "{\"type\":\"assistant\",\"message\":{\"content\":[{\"type\":\"text\",\"text\":\"machine\"}]}}\n",
+ "{\"type\":\"user\",\"sessionId\":\"c1\",\"cwd\":\"/repo\",\"timestamp\":\"2026-07-14T00:00:00Z\",\"message\":{\"content\":\"Prefer small modules\"}}\n"
+ ),
+ )
+ .unwrap();
+ fs::write(
+ codex.join("rollout-test.jsonl"),
+ concat!(
+ "{\"type\":\"session_meta\",\"payload\":{\"id\":\"x1\",\"cwd\":\"/repo\"}}\n",
+ "{\"type\":\"response_item\",\"timestamp\":\"2026-07-14T00:00:00Z\",\"payload\":{\"type\":\"message\",\"role\":\"developer\",\"content\":[{\"type\":\"input_text\",\"text\":\"secret scaffolding\"}]}}\n",
+ "{\"type\":\"response_item\",\"timestamp\":\"2026-07-14T00:00:01Z\",\"payload\":{\"type\":\"message\",\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Run focused tests first\"}]}}\n"
+ ),
+ )
+ .unwrap();
+
+ let statuses = coding_session_status_for_roots(&claude, &temp.path().join("codex"));
+ assert_eq!(statuses.len(), 2);
+ assert_eq!(statuses[0].session_files, 1);
+ assert_eq!(statuses[0].evidence_units, 1);
+ assert_eq!(statuses[1].session_files, 1);
+ assert_eq!(statuses[1].evidence_units, 1);
+ assert_eq!(statuses[0].invalid_files + statuses[1].invalid_files, 0);
+ }
+}
diff --git a/tests/coding_sessions_feature.rs b/tests/coding_sessions_feature.rs
new file mode 100644
index 0000000000..e63f54b753
--- /dev/null
+++ b/tests/coding_sessions_feature.rs
@@ -0,0 +1,49 @@
+//! Feature contract for TinyCortex Codex/Claude session discovery through the
+//! OpenHuman adapter seam.
+
+use std::fs;
+
+use tempfile::tempdir;
+
+use openhuman_core::openhuman::tinycortex::coding_session_status_for_roots;
+
+#[test]
+fn coding_session_sources_extract_human_turns_from_both_harnesses() {
+ let temp = tempdir().expect("tempdir");
+ let claude_root = temp.path().join("claude/projects/repo");
+ let codex_root = temp.path().join("codex/sessions/2026/07/14");
+ fs::create_dir_all(&claude_root).expect("claude root");
+ fs::create_dir_all(&codex_root).expect("codex root");
+
+ fs::write(
+ claude_root.join("claude-session.jsonl"),
+ concat!(
+ "{\"type\":\"user\",\"sessionId\":\"claude-1\",\"cwd\":\"/repo\",\"timestamp\":\"2026-07-14T10:00:00Z\",\"message\":{\"content\":\"Use behavior-driven tests\"}}\n",
+ "{\"type\":\"user\",\"isSidechain\":true,\"message\":{\"content\":\"subagent machine traffic\"}}\n"
+ ),
+ )
+ .expect("claude fixture");
+ fs::write(
+ codex_root.join("rollout-codex-session.jsonl"),
+ concat!(
+ "{\"type\":\"session_meta\",\"payload\":{\"id\":\"codex-1\",\"cwd\":\"/repo\"}}\n",
+ "{\"type\":\"response_item\",\"timestamp\":\"2026-07-14T10:00:00Z\",\"payload\":{\"type\":\"message\",\"role\":\"developer\",\"content\":[{\"type\":\"input_text\",\"text\":\"machine policy\"}]}}\n",
+ "{\"type\":\"response_item\",\"timestamp\":\"2026-07-14T10:00:01Z\",\"payload\":{\"type\":\"message\",\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Keep modules below 500 lines\"}]}}\n"
+ ),
+ )
+ .expect("codex fixture");
+
+ let statuses = coding_session_status_for_roots(
+ &temp.path().join("claude/projects"),
+ &temp.path().join("codex/sessions"),
+ );
+
+ assert_eq!(statuses.len(), 2);
+ assert_eq!(statuses[0].kind, "claude_code");
+ assert_eq!(statuses[0].evidence_units, 1, "sidechain must be excluded");
+ assert_eq!(statuses[1].kind, "codex");
+ assert_eq!(
+ statuses[1].evidence_units, 1,
+ "developer policy must be excluded"
+ );
+}
diff --git a/tests/json_rpc_e2e.rs b/tests/json_rpc_e2e.rs
index 90668c3263..0d781563b7 100644
--- a/tests/json_rpc_e2e.rs
+++ b/tests/json_rpc_e2e.rs
@@ -1122,6 +1122,51 @@ fn ensure_test_rpc_auth() {
});
}
+#[tokio::test]
+async fn json_rpc_discovers_codex_and_claude_sessions_for_memory_ingestion() {
+ let _env_lock = json_rpc_e2e_env_lock();
+ let tmp = tempdir().expect("tempdir");
+ let claude_home = tmp.path().join("claude");
+ let codex_home = tmp.path().join("codex");
+ let claude_root = claude_home.join("projects/repo");
+ let codex_root = codex_home.join("sessions/2026/07/14");
+ std::fs::create_dir_all(&claude_root).expect("claude fixture root");
+ std::fs::create_dir_all(&codex_root).expect("codex fixture root");
+ std::fs::write(
+ claude_root.join("session.jsonl"),
+ "{\"type\":\"user\",\"timestamp\":\"2026-07-14T00:00:00Z\",\"message\":{\"content\":\"Prefer focused tests\"}}\n",
+ )
+ .expect("claude fixture");
+ std::fs::write(
+ codex_root.join("rollout-session.jsonl"),
+ "{\"type\":\"response_item\",\"timestamp\":\"2026-07-14T00:00:00Z\",\"payload\":{\"type\":\"message\",\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Prefer small modules\"}]}}\n",
+ )
+ .expect("codex fixture");
+
+ let _claude_guard = EnvVarGuard::set_to_path("CLAUDE_CONFIG_DIR", &claude_home);
+ let _codex_guard = EnvVarGuard::set_to_path("CODEX_HOME", &codex_home);
+ let (rpc_addr, rpc_join) = serve_on_ephemeral(build_core_http_router(false)).await;
+ let rpc_base = format!("http://{rpc_addr}");
+
+ let response = post_json_rpc(
+ &rpc_base,
+ 4_914_001,
+ "openhuman.memory_sources_coding_session_status",
+ json!({}),
+ )
+ .await;
+ let result = peel_logs_envelope(assert_no_jsonrpc_error(
+ &response,
+ "memory_sources_coding_session_status",
+ ));
+ let sources = result["sources"].as_array().expect("sources array");
+ assert_eq!(sources.len(), 2);
+ assert!(sources.iter().all(|source| source["session_files"] == 1));
+ assert!(sources.iter().all(|source| source["evidence_units"] == 1));
+
+ rpc_join.abort();
+}
+
#[tokio::test]
async fn json_rpc_config_update_browser_settings_persists_backend() {
let _env_lock = json_rpc_e2e_env_lock();
diff --git a/vendor/tinycortex b/vendor/tinycortex
index 671e78a014..9a0603afbe 160000
--- a/vendor/tinycortex
+++ b/vendor/tinycortex
@@ -1 +1 @@
-Subproject commit 671e78a01411de5bcda8f3d1816ac6c67485d694
+Subproject commit 9a0603afbebac608eac2ca0fa606caecd31ed1e7
From eff1f788a057ab6e2ed4d450e403e91f595b5a42 Mon Sep 17 00:00:00 2001
From: Steven Enamakel
Date: Tue, 14 Jul 2026 14:01:59 +0000
Subject: [PATCH 02/28] chore(tauri): sync TinyCortex lockfile
---
app/src-tauri/Cargo.lock | 23 ++++++++++++++++++++---
1 file changed, 20 insertions(+), 3 deletions(-)
diff --git a/app/src-tauri/Cargo.lock b/app/src-tauri/Cargo.lock
index 848295dca6..504f664033 100644
--- a/app/src-tauri/Cargo.lock
+++ b/app/src-tauri/Cargo.lock
@@ -5654,7 +5654,7 @@ dependencies = [
"tar",
"tempfile",
"thiserror 2.0.18",
- "tinyagents",
+ "tinyagents 1.9.0",
"tinychannels",
"tinycortex",
"tinyflows",
@@ -9169,6 +9169,23 @@ dependencies = [
"tracing",
]
+[[package]]
+name = "tinyagents"
+version = "2.0.0"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "chrono",
+ "futures",
+ "reqwest 0.12.28",
+ "serde",
+ "serde_json",
+ "sha2 0.11.0",
+ "thiserror 2.0.18",
+ "tokio",
+ "tracing",
+]
+
[[package]]
name = "tinychannels"
version = "0.1.0"
@@ -9229,7 +9246,7 @@ dependencies = [
"serde_json",
"sha2 0.10.9",
"thiserror 2.0.18",
- "tinyagents",
+ "tinyagents 2.0.0",
"tokio",
"toml 0.8.2",
"tracing",
@@ -9249,7 +9266,7 @@ dependencies = [
"serde",
"serde_json",
"thiserror 2.0.18",
- "tinyagents",
+ "tinyagents 1.9.0",
"tracing",
]
From e9035c26441bd59332f2af186d476728859da19b Mon Sep 17 00:00:00 2001
From: Steven Enamakel
Date: Tue, 14 Jul 2026 14:08:56 +0000
Subject: [PATCH 03/28] ci: initialize nested TinyCortex submodules
---
.github/workflows/ci-lite.yml | 2 +-
.github/workflows/release-production.yml | 2 +-
.github/workflows/release-staging.yml | 2 +-
.github/workflows/test-reusable.yml | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/ci-lite.yml b/.github/workflows/ci-lite.yml
index 870276ae6d..545acced6c 100644
--- a/.github/workflows/ci-lite.yml
+++ b/.github/workflows/ci-lite.yml
@@ -687,7 +687,7 @@ jobs:
- name: Init tinycortex submodule
run: |
git config --global --add safe.directory "$GITHUB_WORKSPACE"
- git submodule update --init vendor/tinycortex
+ git submodule update --init --recursive vendor/tinycortex
- name: Cache TinyCortex build artifacts
uses: Swatinem/rust-cache@v2
diff --git a/.github/workflows/release-production.yml b/.github/workflows/release-production.yml
index c15c7a9013..7ad744ce5e 100644
--- a/.github/workflows/release-production.yml
+++ b/.github/workflows/release-production.yml
@@ -424,7 +424,7 @@ jobs:
# fork the core image doesn't need. The Dockerfile COPYs vendor/ because
# [patch.crates-io] resolves Rust SDK crates from vendor/.
- name: Init vendored Rust submodules
- run: git submodule update --init vendor/tinyagents vendor/tinyflows vendor/tinycortex vendor/tinyjuice vendor/tinychannels vendor/tinyplace
+ run: git submodule update --init --recursive vendor/tinyagents vendor/tinyflows vendor/tinycortex vendor/tinyjuice vendor/tinychannels vendor/tinyplace
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to GHCR
diff --git a/.github/workflows/release-staging.yml b/.github/workflows/release-staging.yml
index 053c97b5b0..58700d2107 100644
--- a/.github/workflows/release-staging.yml
+++ b/.github/workflows/release-staging.yml
@@ -319,7 +319,7 @@ jobs:
# fork the core image doesn't need. The Dockerfile COPYs vendor/ because
# [patch.crates-io] resolves Rust SDK crates from vendor/.
- name: Init vendored Rust submodules
- run: git submodule update --init vendor/tinyagents vendor/tinyflows vendor/tinycortex vendor/tinyjuice vendor/tinychannels vendor/tinyplace
+ run: git submodule update --init --recursive vendor/tinyagents vendor/tinyflows vendor/tinycortex vendor/tinyjuice vendor/tinychannels vendor/tinyplace
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Build image (no push)
diff --git a/.github/workflows/test-reusable.yml b/.github/workflows/test-reusable.yml
index 04265f8280..c87b3de089 100644
--- a/.github/workflows/test-reusable.yml
+++ b/.github/workflows/test-reusable.yml
@@ -211,7 +211,7 @@ jobs:
- name: Init tinycortex submodule
run: |
git config --global --add safe.directory "$GITHUB_WORKSPACE"
- git submodule update --init vendor/tinycortex
+ git submodule update --init --recursive vendor/tinycortex
- name: Cache TinyCortex build artifacts
uses: Swatinem/rust-cache@v2
From 6f0fa4fe087b6e48bc7e5c1246763e28d0bc954b Mon Sep 17 00:00:00 2001
From: Steven Enamakel
Date: Tue, 14 Jul 2026 14:18:06 +0000
Subject: [PATCH 04/28] fix(memory): cap coding-session status scans
---
app/src/services/memorySourcesService.ts | 1 +
src/openhuman/tinycortex/persona.rs | 48 +++++++++++++++++++++++-
2 files changed, 47 insertions(+), 2 deletions(-)
diff --git a/app/src/services/memorySourcesService.ts b/app/src/services/memorySourcesService.ts
index 9bd41b1131..26b8072771 100644
--- a/app/src/services/memorySourcesService.ts
+++ b/app/src/services/memorySourcesService.ts
@@ -207,6 +207,7 @@ export interface CodingSessionSourceStatus {
session_files: number;
evidence_units: number;
invalid_files: number;
+ scan_truncated?: boolean;
}
export interface CodingSessionIngestResult {
diff --git a/src/openhuman/tinycortex/persona.rs b/src/openhuman/tinycortex/persona.rs
index f4963ec559..6b76f7c970 100644
--- a/src/openhuman/tinycortex/persona.rs
+++ b/src/openhuman/tinycortex/persona.rs
@@ -11,6 +11,7 @@ use crate::openhuman::config::Config;
const DEFAULT_MAX_SESSIONS: usize = 100;
const MAX_MAX_SESSIONS: usize = 1_000;
+const MAX_STATUS_SESSION_FILES: usize = 1_000;
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct CodingSessionSourceStatus {
@@ -19,6 +20,7 @@ pub struct CodingSessionSourceStatus {
pub session_files: usize,
pub evidence_units: usize,
pub invalid_files: usize,
+ pub scan_truncated: bool,
}
#[derive(Debug, Clone, Deserialize)]
@@ -60,10 +62,20 @@ fn roots_from_environment() -> (PathBuf, PathBuf) {
fn source_status(
kind: &str,
root: &Path,
+ max_files: usize,
discover: impl Fn(&Path) -> Vec,
read: impl Fn(&Path) -> anyhow::Result,
) -> CodingSessionSourceStatus {
- let files = discover(root);
+ let mut files = discover(root);
+ let scan_truncated = files.len() > max_files;
+ files.truncate(max_files);
+ if scan_truncated {
+ tracing::debug!(
+ source = kind,
+ max_files,
+ "[memory_persona] coding session status scan capped"
+ );
+ }
let mut evidence_units = 0;
let mut invalid_files = 0;
for path in &files {
@@ -85,6 +97,7 @@ fn source_status(
session_files: files.len(),
evidence_units,
invalid_files,
+ scan_truncated,
}
}
@@ -97,10 +110,17 @@ pub fn coding_session_status_for_roots(
source_status(
"claude_code",
claude_root,
+ MAX_STATUS_SESSION_FILES,
claude_code::discover,
claude_code::read_session,
),
- source_status("codex", codex_root, codex::discover, codex::read_session),
+ source_status(
+ "codex",
+ codex_root,
+ MAX_STATUS_SESSION_FILES,
+ codex::discover,
+ codex::read_session,
+ ),
];
tracing::debug!(
files = statuses
@@ -240,4 +260,28 @@ mod tests {
assert_eq!(statuses[1].evidence_units, 1);
assert_eq!(statuses[0].invalid_files + statuses[1].invalid_files, 0);
}
+
+ #[test]
+ fn status_scan_stops_parsing_at_the_configured_limit() {
+ let paths = vec![PathBuf::from("one"), PathBuf::from("two")];
+ let reads = std::cell::Cell::new(0);
+ let status = source_status(
+ "fixture",
+ Path::new("."),
+ 1,
+ |_| paths.clone(),
+ |_| {
+ reads.set(reads.get() + 1);
+ Ok(RawSession::new(
+ tinycortex::memory::persona::types::EvidenceSource::new(
+ tinycortex::memory::persona::types::PersonaSourceKind::Codex,
+ ),
+ ))
+ },
+ );
+
+ assert_eq!(reads.get(), 1);
+ assert_eq!(status.session_files, 1);
+ assert!(status.scan_truncated);
+ }
}
From 313e7c35d76b36bdc1d2eaf80edf1c5fbdd82e82 Mon Sep 17 00:00:00 2001
From: M3gA-Mind
Date: Tue, 14 Jul 2026 20:59:57 +0530
Subject: [PATCH 05/28] fix(memory): parse stored category through FromStr to
strip custom: prefix
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The tinycortex bump in this PR changes MemoryCategory's wire/Display format so
Custom(name) renders as `custom:{name}` (keeping Custom("core") distinct from
Core). The store persists a category via its Display form, but the read helper
`memory_category_from_stored` wrapped the raw stored string in Custom(_) without
stripping the prefix, so a round-trip produced Custom("custom:project") instead
of Custom("project") — breaking memory::tools::store::store_with_custom_category.
Parse back through FromStr (the true inverse of Display) so the prefix is stripped
symmetrically. Also update the stale memory_category_display_outputs_expected_values
test to the new custom: Display form.
---
src/openhuman/memory/traits.rs | 5 ++++-
src/openhuman/memory_store/memory_trait.rs | 17 +++++++++++------
2 files changed, 15 insertions(+), 7 deletions(-)
diff --git a/src/openhuman/memory/traits.rs b/src/openhuman/memory/traits.rs
index 6c63498e0a..94b0553fc5 100644
--- a/src/openhuman/memory/traits.rs
+++ b/src/openhuman/memory/traits.rs
@@ -36,9 +36,12 @@ mod tests {
assert_eq!(MemoryCategory::Core.to_string(), "core");
assert_eq!(MemoryCategory::Daily.to_string(), "daily");
assert_eq!(MemoryCategory::Conversation.to_string(), "conversation");
+ // TinyCortex renders `Custom(name)` with a `custom:` prefix so it stays
+ // distinct from the built-in variants and `Display`/`FromStr` are true
+ // inverses (see `memory_category_from_stored`).
assert_eq!(
MemoryCategory::Custom("project_notes".into()).to_string(),
- "project_notes"
+ "custom:project_notes"
);
}
diff --git a/src/openhuman/memory_store/memory_trait.rs b/src/openhuman/memory_store/memory_trait.rs
index d209aa04a4..6d34ad026f 100644
--- a/src/openhuman/memory_store/memory_trait.rs
+++ b/src/openhuman/memory_store/memory_trait.rs
@@ -45,13 +45,18 @@ fn normalize_namespace(namespace: Option<&str>) -> &str {
}
/// Helper to convert a raw string category from the database into a `MemoryCategory`.
+///
+/// The store persists a category via its `Display` form, and the current
+/// TinyCortex format renders `Custom(name)` as `custom:{name}` (so `Custom("core")`
+/// stays distinct from `Core`). Parse back through `FromStr` — the true inverse of
+/// `Display` — so the `custom:` prefix is stripped symmetrically. Wrapping the raw
+/// string in `Custom(_)` instead (the previous behaviour) double-prefixed on
+/// read-back once the wire format gained the prefix. An empty stored value has no
+/// `FromStr` mapping, so it falls back to an empty `Custom` (matching the prior
+/// catch-all for that degenerate case).
fn memory_category_from_stored(raw: &str) -> MemoryCategory {
- match raw {
- "core" => MemoryCategory::Core,
- "daily" => MemoryCategory::Daily,
- "conversation" => MemoryCategory::Conversation,
- other => MemoryCategory::Custom(other.to_string()),
- }
+ raw.parse()
+ .unwrap_or_else(|_| MemoryCategory::Custom(raw.to_string()))
}
#[async_trait]
From 92c309cd08c89ff02dff7f6951fbc7f93352a66b Mon Sep 17 00:00:00 2001
From: Steven Enamakel
Date: Tue, 14 Jul 2026 15:27:07 +0000
Subject: [PATCH 06/28] fix(memory): parse TinyCortex custom categories
---
src/openhuman/memory/traits.rs | 13 +++++++++++++
src/openhuman/memory_store/memory_trait.rs | 10 ++++++++--
2 files changed, 21 insertions(+), 2 deletions(-)
diff --git a/src/openhuman/memory/traits.rs b/src/openhuman/memory/traits.rs
index 94b0553fc5..3d6040425a 100644
--- a/src/openhuman/memory/traits.rs
+++ b/src/openhuman/memory/traits.rs
@@ -45,6 +45,19 @@ mod tests {
);
}
+ #[test]
+ fn memory_category_custom_wire_values_round_trip_and_accept_legacy_bare_values() {
+ let current: MemoryCategory = "custom:project_notes".parse().unwrap();
+ let legacy: MemoryCategory = "project_notes".parse().unwrap();
+
+ assert_eq!(current, MemoryCategory::Custom("project_notes".into()));
+ assert_eq!(legacy, MemoryCategory::Custom("project_notes".into()));
+ assert_eq!(
+ serde_json::to_string(¤t).unwrap(),
+ "\"custom:project_notes\""
+ );
+ }
+
#[test]
fn memory_category_serde_uses_snake_case() {
let core = serde_json::to_string(&MemoryCategory::Core).unwrap();
diff --git a/src/openhuman/memory_store/memory_trait.rs b/src/openhuman/memory_store/memory_trait.rs
index 6d34ad026f..64f9d2cbc6 100644
--- a/src/openhuman/memory_store/memory_trait.rs
+++ b/src/openhuman/memory_store/memory_trait.rs
@@ -55,8 +55,14 @@ fn normalize_namespace(namespace: Option<&str>) -> &str {
/// `FromStr` mapping, so it falls back to an empty `Custom` (matching the prior
/// catch-all for that degenerate case).
fn memory_category_from_stored(raw: &str) -> MemoryCategory {
- raw.parse()
- .unwrap_or_else(|_| MemoryCategory::Custom(raw.to_string()))
+ raw.parse().unwrap_or_else(|error| {
+ tracing::debug!(
+ category_chars = raw.chars().count(),
+ reason = %error,
+ "[memory_store] invalid stored category; preserving as custom"
+ );
+ MemoryCategory::Custom(raw.to_string())
+ })
}
#[async_trait]
From 003065b6648f7b32e4ae0edbddda7708a88d6d04 Mon Sep 17 00:00:00 2001
From: M3gA-Mind
Date: Tue, 14 Jul 2026 21:30:18 +0530
Subject: [PATCH 07/28] fix(memory): strip custom: prefix when storing echoed
wire categories
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Review (#4863): this PR made Display/memory_recall emit the `custom:`
wire form, but memory_store still wrapped any non-builtin string as
Custom(other), so echoing back `custom:project_notes` stored
Custom("custom:project_notes") — which Displays as custom:custom:project_notes
and no longer matches the original category on recall/filter. Route the custom
arm through FromStr (which strips the prefix and still accepts legacy bare
names), with a raw-string fallback. Adds a store-level round-trip regression.
---
src/openhuman/memory/tools/store.rs | 38 ++++++++++++++++++++++++++++-
1 file changed, 37 insertions(+), 1 deletion(-)
diff --git a/src/openhuman/memory/tools/store.rs b/src/openhuman/memory/tools/store.rs
index deda5b58fd..afe784563b 100644
--- a/src/openhuman/memory/tools/store.rs
+++ b/src/openhuman/memory/tools/store.rs
@@ -80,7 +80,16 @@ impl Tool for MemoryStoreTool {
Some("core") | None => MemoryCategory::Core,
Some("daily") => MemoryCategory::Daily,
Some("conversation") => MemoryCategory::Conversation,
- Some(other) => MemoryCategory::Custom(other.to_string()),
+ // Route custom categories through `FromStr` so a `custom:`
+ // wire value — the form `memory_recall`/`Display` now emit — resolves
+ // back to `Custom("")` instead of `Custom("custom:")`
+ // (which would `Display` as `custom:custom:` and stop matching
+ // the original category on recall/filter). Legacy bare names still
+ // parse to the same `Custom(name)`; an unparseable value falls back
+ // to the raw string. (review: prefixed-custom round-trip)
+ Some(other) => other
+ .parse()
+ .unwrap_or_else(|_| MemoryCategory::Custom(other.to_string())),
};
if let Err(error) = self
@@ -204,6 +213,33 @@ mod tests {
assert_eq!(entry.category, MemoryCategory::Custom("project".into()));
}
+ /// Regression: a `custom:` wire value (the form `memory_recall` and
+ /// `Display` now emit) must store as `Custom("")`, not the
+ /// double-prefixed `Custom("custom:")` — otherwise it would `Display`
+ /// as `custom:custom:` and stop matching the original category.
+ #[tokio::test]
+ async fn store_strips_custom_prefix_from_wire_category() {
+ let (_tmp, mem) = test_mem();
+ let tool = MemoryStoreTool::new(mem.clone(), test_security());
+ let result = tool
+ .execute(json!({
+ "namespace": "global",
+ "key": "proj_note",
+ "content": "Uses async runtime",
+ "category": "custom:project"
+ }))
+ .await
+ .unwrap();
+ assert!(!result.is_error);
+
+ let entry = mem.get("global", "proj_note").await.unwrap().unwrap();
+ assert_eq!(
+ entry.category,
+ MemoryCategory::Custom("project".into()),
+ "the `custom:` wire prefix must be stripped, not double-stored"
+ );
+ }
+
#[tokio::test]
async fn store_rejects_secret_like_content() {
let (_tmp, mem) = test_mem();
From 73985a8ab15ccaa1202e08c11ea8ce8f48a3dc63 Mon Sep 17 00:00:00 2001
From: Steven Enamakel
Date: Tue, 14 Jul 2026 16:44:53 +0000
Subject: [PATCH 08/28] test(memory): include coding session controllers
---
tests/config_auth_app_state_connectivity_e2e.rs | 2 ++
1 file changed, 2 insertions(+)
diff --git a/tests/config_auth_app_state_connectivity_e2e.rs b/tests/config_auth_app_state_connectivity_e2e.rs
index 33b5064657..841563d163 100644
--- a/tests/config_auth_app_state_connectivity_e2e.rs
+++ b/tests/config_auth_app_state_connectivity_e2e.rs
@@ -2878,8 +2878,10 @@ async fn worker_a_controller_schemas_are_fully_exposed() {
vec![
"openhuman.memory_sources_add",
"openhuman.memory_sources_apply_all_in",
+ "openhuman.memory_sources_coding_session_status",
"openhuman.memory_sources_estimate_sync_cost",
"openhuman.memory_sources_get",
+ "openhuman.memory_sources_ingest_coding_sessions",
"openhuman.memory_sources_list",
"openhuman.memory_sources_list_items",
"openhuman.memory_sources_monthly_cost_summary",
From 8296d300126dc080d746f1cee641c29384ba5d79 Mon Sep 17 00:00:00 2001
From: M3gA-Mind
Date: Tue, 14 Jul 2026 22:43:13 +0530
Subject: [PATCH 09/28] =?UTF-8?q?fix(memory):=20address=20review=20?=
=?UTF-8?q?=E2=80=94=20ingestion=20timeout,=20error=20diagnostics,=20i18n?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- memory_sources/rpc.rs: wrap ingest_coding_sessions in a budget-proportional
tokio::time::timeout (120s + 30s/session, capped) so a stalled provider call
can't keep the RPC waiting indefinitely, without killing a legit large
backfill (CodeRabbit Major).
- tinycortex/persona.rs: add error-path tracing::error! diagnostics on the three
fallible external calls (build_chat_provider, FileStateStore::open_in_workspace,
Pipeline::run) via inspect_err (CodeRabbit Major — repo diagnostics convention).
- i18n ko/zh-CN/ru: restore 'Claude Code' product branding for the .claude label,
and align the {evidence} count label to the canonical en semantics ('human
turns') instead of 'user input'/'user messages' (CodeRabbit Minor).
---
app/src/lib/i18n/ko.ts | 2 +-
app/src/lib/i18n/ru.ts | 4 ++--
app/src/lib/i18n/zh-CN.ts | 4 ++--
src/openhuman/memory_sources/rpc.rs | 31 ++++++++++++++++++++++++-----
src/openhuman/tinycortex/persona.rs | 22 +++++++++++++++++---
5 files changed, 50 insertions(+), 13 deletions(-)
diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts
index 6143a88b80..7a1df9c761 100644
--- a/app/src/lib/i18n/ko.ts
+++ b/app/src/lib/i18n/ko.ts
@@ -7157,7 +7157,7 @@ const messages: TranslationMap = {
'Codex와 Claude Code의 결정 및 수정 사항을 비공개 페르소나 메모리로 변환합니다.',
'memorySources.codingSessions.ingest': '새 세션 수집',
'memorySources.codingSessions.ingesting': '수집 중…',
- 'memorySources.codingSessions.claude': '클로드 코드',
+ 'memorySources.codingSessions.claude': 'Claude Code',
'memorySources.codingSessions.codex': 'Codex',
'memorySources.codingSessions.counts': '세션 {files}개 · 사용자 입력 {evidence}개',
'memorySources.codingSessions.notFound': '로컬 기록을 찾지 못했습니다',
diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts
index 90c32f08a7..b169dd97a8 100644
--- a/app/src/lib/i18n/ru.ts
+++ b/app/src/lib/i18n/ru.ts
@@ -7317,9 +7317,9 @@ const messages: TranslationMap = {
'Превратите решения и исправления из Codex и Claude Code в приватную память персоны.',
'memorySources.codingSessions.ingest': 'Загрузить новые сеансы',
'memorySources.codingSessions.ingesting': 'Загрузка…',
- 'memorySources.codingSessions.claude': 'Клод Код',
+ 'memorySources.codingSessions.claude': 'Claude Code',
'memorySources.codingSessions.codex': 'Codex',
- 'memorySources.codingSessions.counts': '{files} сеансов · {evidence} сообщений пользователя',
+ 'memorySources.codingSessions.counts': '{files} сеансов · {evidence} ходов человека',
'memorySources.codingSessions.notFound': 'Локальная история не найдена',
'memorySources.codingSessions.scanning': 'Сканирование локальной истории…',
'memorySources.codingSessions.complete': 'Сеансы программирования загружены',
diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts
index 3a93269a24..63ea4dab65 100644
--- a/app/src/lib/i18n/zh-CN.ts
+++ b/app/src/lib/i18n/zh-CN.ts
@@ -6848,9 +6848,9 @@ const messages: TranslationMap = {
'将 Codex 和 Claude Code 中的决策与纠正转化为私有人格记忆。',
'memorySources.codingSessions.ingest': '摄取新会话',
'memorySources.codingSessions.ingesting': '正在摄取…',
- 'memorySources.codingSessions.claude': '克劳德代码',
+ 'memorySources.codingSessions.claude': 'Claude Code',
'memorySources.codingSessions.codex': 'Codex',
- 'memorySources.codingSessions.counts': '{files} 个会话 · {evidence} 条用户输入',
+ 'memorySources.codingSessions.counts': '{files} 个会话 · {evidence} 轮人类对话',
'memorySources.codingSessions.notFound': '未找到本地历史记录',
'memorySources.codingSessions.scanning': '正在扫描本地会话历史…',
'memorySources.codingSessions.complete': '编程会话已摄取',
diff --git a/src/openhuman/memory_sources/rpc.rs b/src/openhuman/memory_sources/rpc.rs
index 89d2e53e1a..08b6f3299e 100644
--- a/src/openhuman/memory_sources/rpc.rs
+++ b/src/openhuman/memory_sources/rpc.rs
@@ -43,12 +43,33 @@ pub async fn ingest_coding_sessions_rpc(
// remains attached to the ambient Tokio runtime, keeping the controller
// future itself Send-safe for the registry.
let runtime = tokio::runtime::Handle::current();
- let response = tokio::task::spawn_blocking(move || {
- runtime.block_on(crate::openhuman::tinycortex::ingest_coding_sessions(
- &config, req,
- ))
- })
+ // Wall-clock ceiling so a stalled provider call or a wedged session step
+ // can't keep the RPC (and its blocking worker) waiting indefinitely (#4863
+ // review). Scale to the requested budget — each session drives at most one
+ // LLM call — so a large backfill isn't killed mid-flight while a genuine
+ // infinite hang still terminates. `max_sessions` is untrusted, so cap the
+ // multiplier before computing the budget.
+ let ingest_timeout =
+ std::time::Duration::from_secs(120 + (req.max_sessions.min(1_000) as u64) * 30);
+ let response = tokio::time::timeout(
+ ingest_timeout,
+ tokio::task::spawn_blocking(move || {
+ runtime.block_on(crate::openhuman::tinycortex::ingest_coding_sessions(
+ &config, req,
+ ))
+ }),
+ )
.await
+ .map_err(|_elapsed| {
+ tracing::error!(
+ timeout_secs = ingest_timeout.as_secs(),
+ "[memory_sources] ingest_coding_sessions_rpc: timed out"
+ );
+ format!(
+ "ingest coding sessions: timed out after {}s",
+ ingest_timeout.as_secs()
+ )
+ })?
.map_err(|error| format!("join coding-session ingestion: {error}"))?
.map_err(|error| format!("ingest coding sessions: {error:#}"))?;
tracing::info!(
diff --git a/src/openhuman/tinycortex/persona.rs b/src/openhuman/tinycortex/persona.rs
index 6b76f7c970..14330bfad5 100644
--- a/src/openhuman/tinycortex/persona.rs
+++ b/src/openhuman/tinycortex/persona.rs
@@ -184,9 +184,19 @@ pub async fn ingest_coding_sessions(
persona.run_budget.max_sessions = max_sessions;
persona.run_budget.max_llm_calls = max_sessions as u32;
- let provider = super::build_chat_provider(config)?;
+ let provider = super::build_chat_provider(config).inspect_err(|error| {
+ tracing::error!(
+ error = %error,
+ "[memory_persona] coding session ingestion: build_chat_provider failed"
+ );
+ })?;
let summariser = super::HostSummariser::new(config.clone());
- let store = FileStateStore::open_in_workspace(&config.workspace_dir)?;
+ let store = FileStateStore::open_in_workspace(&config.workspace_dir).inspect_err(|error| {
+ tracing::error!(
+ error = %error,
+ "[memory_persona] coding session ingestion: open state store failed"
+ );
+ })?;
let report = Pipeline {
config: &memory_config,
persona: &persona,
@@ -195,7 +205,13 @@ pub async fn ingest_coding_sessions(
store: &store,
}
.run(mode)
- .await?;
+ .await
+ .inspect_err(|error| {
+ tracing::error!(
+ error = %error,
+ "[memory_persona] coding session ingestion: pipeline run failed"
+ );
+ })?;
tracing::info!(
files_seen = report.files_seen,
From 998d297de0caf5a6acc25e5a35cdf7ebcb649d1e Mon Sep 17 00:00:00 2001
From: M3gA-Mind
Date: Tue, 14 Jul 2026 22:44:26 +0530
Subject: [PATCH 10/28] fix(memory): log enqueue-extract outcome + error path
The enqueue branch only traced entry; add the returned enqueued/already-queued
bool and an error-path diagnostic so the ingest flow is observable end-to-end
(CodeRabbit nitpick, repo diagnostics convention).
---
src/openhuman/tinycortex/ingest.rs | 16 +++++++++++++++-
1 file changed, 15 insertions(+), 1 deletion(-)
diff --git a/src/openhuman/tinycortex/ingest.rs b/src/openhuman/tinycortex/ingest.rs
index ede735fea0..7c8cc757b2 100644
--- a/src/openhuman/tinycortex/ingest.rs
+++ b/src/openhuman/tinycortex/ingest.rs
@@ -26,7 +26,21 @@ impl TreeJobSink for HostTreeJobSink {
default_max_attempts,
"[memory:ingest] enqueue extract job in chunk transaction"
);
- QueueJobSink.enqueue_extract_tx(tx, chunk_id, default_max_attempts)
+ let enqueued = QueueJobSink
+ .enqueue_extract_tx(tx, chunk_id, default_max_attempts)
+ .inspect_err(|error| {
+ tracing::error!(
+ chunk_id,
+ error = %error,
+ "[memory:ingest] enqueue extract job failed"
+ );
+ })?;
+ tracing::trace!(
+ chunk_id,
+ enqueued,
+ "[memory:ingest] enqueue extract job outcome (false = already queued)"
+ );
+ Ok(enqueued)
}
}
From f6b0b80c8a8106a5f55bcd47a8815a1c0c0b7759 Mon Sep 17 00:00:00 2001
From: Steven Enamakel
Date: Tue, 14 Jul 2026 17:23:46 +0000
Subject: [PATCH 11/28] fix(memory): address ingestion review feedback
---
.../intelligence/CodingSessionsCard.tsx | 5 ++
.../__tests__/CodingSessionsCard.test.tsx | 39 +++++++++++
app/src/lib/i18n/ar.ts | 1 +
app/src/lib/i18n/bn.ts | 1 +
app/src/lib/i18n/de.ts | 2 +
app/src/lib/i18n/en.ts | 1 +
app/src/lib/i18n/es.ts | 2 +
app/src/lib/i18n/fr.ts | 2 +
app/src/lib/i18n/hi.ts | 1 +
app/src/lib/i18n/id.ts | 1 +
app/src/lib/i18n/it.ts | 2 +
app/src/lib/i18n/ko.ts | 1 +
app/src/lib/i18n/pl.ts | 2 +
app/src/lib/i18n/pt.ts | 2 +
app/src/lib/i18n/ru.ts | 5 +-
app/src/lib/i18n/zh-CN.ts | 3 +-
.../memory_search/tools/hybrid_search.rs | 27 ++++++++
src/openhuman/tinycortex/ingest.rs | 5 +-
src/openhuman/tinycortex/persona.rs | 69 +++++++++++++++++--
19 files changed, 159 insertions(+), 12 deletions(-)
diff --git a/app/src/components/intelligence/CodingSessionsCard.tsx b/app/src/components/intelligence/CodingSessionsCard.tsx
index 559236ae93..f95eaae4ec 100644
--- a/app/src/components/intelligence/CodingSessionsCard.tsx
+++ b/app/src/components/intelligence/CodingSessionsCard.tsx
@@ -121,6 +121,11 @@ export function CodingSessionsCard({ onToast }: CodingSessionsCardProps) {
.replace('{evidence}', String(source.evidence_units))
: t('memorySources.codingSessions.notFound')}
+ {source.available && source.scan_truncated && (
+
+ {t('memorySources.codingSessions.truncated')}
+
+ )}
))}
diff --git a/app/src/components/intelligence/__tests__/CodingSessionsCard.test.tsx b/app/src/components/intelligence/__tests__/CodingSessionsCard.test.tsx
index c9633e8392..21e06b4f19 100644
--- a/app/src/components/intelligence/__tests__/CodingSessionsCard.test.tsx
+++ b/app/src/components/intelligence/__tests__/CodingSessionsCard.test.tsx
@@ -79,4 +79,43 @@ describe('CodingSessionsCard', () => {
expect(await screen.findByText('No local history found')).toBeInTheDocument();
expect(screen.getByTestId('coding-sessions-ingest')).toBeDisabled();
});
+
+ it('shows status failures as an alert', async () => {
+ mockedStatus.mockRejectedValue(new Error('session scan failed'));
+ renderWithProviders();
+
+ expect(await screen.findByRole('alert')).toHaveTextContent('session scan failed');
+ });
+
+ it('reports ingestion failures through the error toast', async () => {
+ mockedIngest.mockRejectedValue(new Error('persona pipeline failed'));
+ const onToast = vi.fn();
+ renderWithProviders();
+
+ fireEvent.click(await screen.findByTestId('coding-sessions-ingest'));
+
+ await waitFor(() =>
+ expect(onToast).toHaveBeenCalledWith({
+ type: 'error',
+ title: 'Coding-session ingestion failed',
+ message: 'persona pipeline failed',
+ })
+ );
+ });
+
+ it('warns when a source scan reaches its file cap', async () => {
+ mockedStatus.mockResolvedValue([
+ {
+ kind: 'codex',
+ available: true,
+ session_files: 1000,
+ evidence_units: 1200,
+ invalid_files: 0,
+ scan_truncated: true,
+ },
+ ]);
+ renderWithProviders();
+
+ expect(await screen.findByText('Scan limited to the first 1,000 session files.')).toBeVisible();
+ });
});
diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts
index 826bcd20ed..006f371548 100644
--- a/app/src/lib/i18n/ar.ts
+++ b/app/src/lib/i18n/ar.ts
@@ -7080,6 +7080,7 @@ const messages: TranslationMap = {
'memorySources.codingSessions.counts': '{files} جلسات · {evidence} مداخلات بشرية',
'memorySources.codingSessions.notFound': 'لم يُعثر على سجل محلي',
'memorySources.codingSessions.scanning': 'جارٍ فحص سجل الجلسات المحلي…',
+ 'memorySources.codingSessions.truncated': 'اقتصر الفحص على أول 1,000 ملف جلسة.',
'memorySources.codingSessions.complete': 'تم استيعاب جلسات البرمجة',
'memorySources.codingSessions.completeMessage':
'أنتجت {processed} جلسات {observations} ملاحظات شخصية.',
diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts
index 97609eec63..09f8b11dd8 100644
--- a/app/src/lib/i18n/bn.ts
+++ b/app/src/lib/i18n/bn.ts
@@ -7244,6 +7244,7 @@ const messages: TranslationMap = {
'memorySources.codingSessions.counts': '{files}টি সেশন · {evidence}টি মানব বার্তা',
'memorySources.codingSessions.notFound': 'কোনো স্থানীয় ইতিহাস পাওয়া যায়নি',
'memorySources.codingSessions.scanning': 'স্থানীয় সেশন ইতিহাস স্ক্যান করা হচ্ছে…',
+ 'memorySources.codingSessions.truncated': 'স্ক্যানটি প্রথম ১,০০০টি সেশন ফাইলে সীমাবদ্ধ ছিল।',
'memorySources.codingSessions.complete': 'কোডিং সেশন গ্রহণ সম্পন্ন',
'memorySources.codingSessions.completeMessage':
'{processed}টি সেশন থেকে {observations}টি পারসোনা পর্যবেক্ষণ তৈরি হয়েছে।',
diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts
index 05bc389bd1..73df370c2d 100644
--- a/app/src/lib/i18n/de.ts
+++ b/app/src/lib/i18n/de.ts
@@ -7459,6 +7459,8 @@ const messages: TranslationMap = {
'memorySources.codingSessions.counts': '{files} Sitzungen · {evidence} menschliche Beiträge',
'memorySources.codingSessions.notFound': 'Kein lokaler Verlauf gefunden',
'memorySources.codingSessions.scanning': 'Lokaler Sitzungsverlauf wird durchsucht…',
+ 'memorySources.codingSessions.truncated':
+ 'Der Scan wurde auf die ersten 1.000 Sitzungsdateien begrenzt.',
'memorySources.codingSessions.complete': 'Coding-Sitzungen eingelesen',
'memorySources.codingSessions.completeMessage':
'{processed} Sitzungen ergaben {observations} Persona-Beobachtungen.',
diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts
index 37e8d3c335..e735a44355 100644
--- a/app/src/lib/i18n/en.ts
+++ b/app/src/lib/i18n/en.ts
@@ -7581,6 +7581,7 @@ const en: TranslationMap = {
'memorySources.codingSessions.counts': '{files} sessions · {evidence} human turns',
'memorySources.codingSessions.notFound': 'No local history found',
'memorySources.codingSessions.scanning': 'Scanning local session history…',
+ 'memorySources.codingSessions.truncated': 'Scan limited to the first 1,000 session files.',
'memorySources.codingSessions.complete': 'Coding sessions ingested',
'memorySources.codingSessions.completeMessage':
'{processed} sessions produced {observations} persona observations.',
diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts
index fe8f70172c..9c848b3403 100644
--- a/app/src/lib/i18n/es.ts
+++ b/app/src/lib/i18n/es.ts
@@ -7393,6 +7393,8 @@ const messages: TranslationMap = {
'memorySources.codingSessions.counts': '{files} sesiones · {evidence} intervenciones humanas',
'memorySources.codingSessions.notFound': 'No se encontró historial local',
'memorySources.codingSessions.scanning': 'Buscando historial local de sesiones…',
+ 'memorySources.codingSessions.truncated':
+ 'El análisis se limitó a los primeros 1000 archivos de sesión.',
'memorySources.codingSessions.complete': 'Sesiones de programación ingeridas',
'memorySources.codingSessions.completeMessage':
'{processed} sesiones produjeron {observations} observaciones de personalidad.',
diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts
index bcc168e096..14a9a577de 100644
--- a/app/src/lib/i18n/fr.ts
+++ b/app/src/lib/i18n/fr.ts
@@ -7427,6 +7427,8 @@ const messages: TranslationMap = {
'memorySources.codingSessions.counts': '{files} sessions · {evidence} interventions humaines',
'memorySources.codingSessions.notFound': 'Aucun historique local trouvé',
'memorySources.codingSessions.scanning': 'Analyse de l’historique local…',
+ 'memorySources.codingSessions.truncated':
+ 'L’analyse a été limitée aux 1 000 premiers fichiers de session.',
'memorySources.codingSessions.complete': 'Sessions de programmation ingérées',
'memorySources.codingSessions.completeMessage':
'{processed} sessions ont produit {observations} observations de persona.',
diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts
index c914dc2eea..f8531044de 100644
--- a/app/src/lib/i18n/hi.ts
+++ b/app/src/lib/i18n/hi.ts
@@ -7242,6 +7242,7 @@ const messages: TranslationMap = {
'memorySources.codingSessions.counts': '{files} सत्र · {evidence} मानवीय संदेश',
'memorySources.codingSessions.notFound': 'कोई स्थानीय इतिहास नहीं मिला',
'memorySources.codingSessions.scanning': 'स्थानीय सत्र इतिहास स्कैन हो रहा है…',
+ 'memorySources.codingSessions.truncated': 'स्कैन पहले 1,000 सत्र फ़ाइलों तक सीमित था।',
'memorySources.codingSessions.complete': 'कोडिंग सत्र शामिल हो गए',
'memorySources.codingSessions.completeMessage':
'{processed} सत्रों से {observations} व्यक्तित्व अवलोकन बने।',
diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts
index e7151f4e57..39f0a075bb 100644
--- a/app/src/lib/i18n/id.ts
+++ b/app/src/lib/i18n/id.ts
@@ -7276,6 +7276,7 @@ const messages: TranslationMap = {
'memorySources.codingSessions.counts': '{files} sesi · {evidence} masukan manusia',
'memorySources.codingSessions.notFound': 'Riwayat lokal tidak ditemukan',
'memorySources.codingSessions.scanning': 'Memindai riwayat sesi lokal…',
+ 'memorySources.codingSessions.truncated': 'Pemindaian dibatasi pada 1.000 file sesi pertama.',
'memorySources.codingSessions.complete': 'Sesi pemrograman telah diserap',
'memorySources.codingSessions.completeMessage':
'{processed} sesi menghasilkan {observations} pengamatan persona.',
diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts
index 4ac10d1e34..f0f2a3e7f1 100644
--- a/app/src/lib/i18n/it.ts
+++ b/app/src/lib/i18n/it.ts
@@ -7383,6 +7383,8 @@ const messages: TranslationMap = {
'memorySources.codingSessions.counts': '{files} sessioni · {evidence} interventi umani',
'memorySources.codingSessions.notFound': 'Nessuna cronologia locale trovata',
'memorySources.codingSessions.scanning': 'Scansione della cronologia locale…',
+ 'memorySources.codingSessions.truncated':
+ 'La scansione è stata limitata ai primi 1.000 file di sessione.',
'memorySources.codingSessions.complete': 'Sessioni di programmazione acquisite',
'memorySources.codingSessions.completeMessage':
'{processed} sessioni hanno prodotto {observations} osservazioni della persona.',
diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts
index c9081a21a8..07f62df8f1 100644
--- a/app/src/lib/i18n/ko.ts
+++ b/app/src/lib/i18n/ko.ts
@@ -7162,6 +7162,7 @@ const messages: TranslationMap = {
'memorySources.codingSessions.counts': '세션 {files}개 · 사용자 입력 {evidence}개',
'memorySources.codingSessions.notFound': '로컬 기록을 찾지 못했습니다',
'memorySources.codingSessions.scanning': '로컬 세션 기록을 검색하는 중…',
+ 'memorySources.codingSessions.truncated': '스캔이 처음 1,000개 세션 파일로 제한되었습니다.',
'memorySources.codingSessions.complete': '코딩 세션 수집 완료',
'memorySources.codingSessions.completeMessage':
'세션 {processed}개에서 페르소나 관찰 {observations}개를 만들었습니다.',
diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts
index 55fdca6b9e..0dd1b1c3db 100644
--- a/app/src/lib/i18n/pl.ts
+++ b/app/src/lib/i18n/pl.ts
@@ -7352,6 +7352,8 @@ const messages: TranslationMap = {
'memorySources.codingSessions.counts': '{files} sesji · {evidence} wypowiedzi użytkownika',
'memorySources.codingSessions.notFound': 'Nie znaleziono lokalnej historii',
'memorySources.codingSessions.scanning': 'Skanowanie lokalnej historii sesji…',
+ 'memorySources.codingSessions.truncated':
+ 'Skanowanie ograniczono do pierwszych 1000 plików sesji.',
'memorySources.codingSessions.complete': 'Sesje programistyczne wczytane',
'memorySources.codingSessions.completeMessage':
'{processed} sesji utworzyło {observations} obserwacji persony.',
diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts
index 8da4813f7a..19440e1167 100644
--- a/app/src/lib/i18n/pt.ts
+++ b/app/src/lib/i18n/pt.ts
@@ -7366,6 +7366,8 @@ const messages: TranslationMap = {
'memorySources.codingSessions.counts': '{files} sessões · {evidence} mensagens humanas',
'memorySources.codingSessions.notFound': 'Nenhum histórico local encontrado',
'memorySources.codingSessions.scanning': 'Verificando o histórico local…',
+ 'memorySources.codingSessions.truncated':
+ 'A verificação foi limitada aos primeiros 1.000 arquivos de sessão.',
'memorySources.codingSessions.complete': 'Sessões de programação ingeridas',
'memorySources.codingSessions.completeMessage':
'{processed} sessões produziram {observations} observações de persona.',
diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts
index 7666fddfde..24791c0246 100644
--- a/app/src/lib/i18n/ru.ts
+++ b/app/src/lib/i18n/ru.ts
@@ -7319,12 +7319,13 @@ const messages: TranslationMap = {
'memorySources.codingSessions.ingesting': 'Загрузка…',
'memorySources.codingSessions.claude': 'Claude Code',
'memorySources.codingSessions.codex': 'Codex',
- 'memorySources.codingSessions.counts': '{files} сеансов · {evidence} ходов человека',
+ 'memorySources.codingSessions.counts': 'Сеансы: {files} · Сообщения пользователя: {evidence}',
'memorySources.codingSessions.notFound': 'Локальная история не найдена',
'memorySources.codingSessions.scanning': 'Сканирование локальной истории…',
+ 'memorySources.codingSessions.truncated': 'Сканирование ограничено первыми 1000 файлами сеансов.',
'memorySources.codingSessions.complete': 'Сеансы программирования загружены',
'memorySources.codingSessions.completeMessage':
- '{processed} сеансов дали {observations} наблюдений персоны.',
+ 'Обработано сеансов: {processed}; наблюдений персоны: {observations}.',
'memorySources.codingSessions.failed': 'Не удалось загрузить сеансы программирования',
// Privacy status pill + per-action egress disclosure (#4437 / S3)
diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts
index 6a73bae601..ef0ae126c2 100644
--- a/app/src/lib/i18n/zh-CN.ts
+++ b/app/src/lib/i18n/zh-CN.ts
@@ -6850,9 +6850,10 @@ const messages: TranslationMap = {
'memorySources.codingSessions.ingesting': '正在摄取…',
'memorySources.codingSessions.claude': 'Claude Code',
'memorySources.codingSessions.codex': 'Codex',
- 'memorySources.codingSessions.counts': '{files} 个会话 · {evidence} 轮人类对话',
+ 'memorySources.codingSessions.counts': '{files} 个会话 · {evidence} 条证据',
'memorySources.codingSessions.notFound': '未找到本地历史记录',
'memorySources.codingSessions.scanning': '正在扫描本地会话历史…',
+ 'memorySources.codingSessions.truncated': '扫描仅限前 1,000 个会话文件。',
'memorySources.codingSessions.complete': '编程会话已摄取',
'memorySources.codingSessions.completeMessage':
'{processed} 个会话生成了 {observations} 条人格观察。',
diff --git a/src/openhuman/memory_search/tools/hybrid_search.rs b/src/openhuman/memory_search/tools/hybrid_search.rs
index 117823e0f9..eda4b46987 100644
--- a/src/openhuman/memory_search/tools/hybrid_search.rs
+++ b/src/openhuman/memory_search/tools/hybrid_search.rs
@@ -111,6 +111,10 @@ impl Tool for MemoryHybridSearchTool {
}
let profile = WeightProfile::by_name(&parsed.mode).ok_or_else(|| {
+ log::warn!(
+ "[tool][memory_hybrid_search] rejected unknown mode={}",
+ parsed.mode
+ );
anyhow::anyhow!(
"memory_hybrid_search: unknown mode '{}'; expected balanced, semantic, lexical, or graph_first",
parsed.mode
@@ -214,3 +218,26 @@ impl Tool for MemoryHybridSearchTool {
Ok(ToolResult::success(output))
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[tokio::test]
+ async fn rejects_unknown_mode_before_opening_external_search_resources() {
+ let error = MemoryHybridSearchTool
+ .execute(json!({
+ "query": "release checklist",
+ "namespace": "global",
+ "mode": "mystery"
+ }))
+ .await
+ .expect_err("an unknown mode must fail validation");
+
+ let message = error.to_string();
+ assert!(message.contains("unknown mode 'mystery'"), "{message}");
+ // Validation runs before config, provider, and store setup. Reaching any
+ // external search path would replace this precise validation error.
+ assert!(!message.contains("load config failed"), "{message}");
+ }
+}
diff --git a/src/openhuman/tinycortex/ingest.rs b/src/openhuman/tinycortex/ingest.rs
index 7c8cc757b2..8131e9cbc3 100644
--- a/src/openhuman/tinycortex/ingest.rs
+++ b/src/openhuman/tinycortex/ingest.rs
@@ -6,10 +6,11 @@ use tinycortex::memory::score::extract::{LlmEntityExtractor, LlmExtractorConfig}
use tinycortex::memory::score::ScoringConfig;
use crate::openhuman::config::Config;
+
pub struct HostTreeJobSink;
impl HostTreeJobSink {
- pub fn new(_config: Config) -> Self {
+ pub fn new() -> Self {
Self
}
}
@@ -69,7 +70,7 @@ pub fn context(
) {
(
super::memory_config_from(config, config.workspace_dir.clone()),
- HostTreeJobSink::new(config.clone()),
+ HostTreeJobSink::new(),
scoring_config(config),
)
}
diff --git a/src/openhuman/tinycortex/persona.rs b/src/openhuman/tinycortex/persona.rs
index 14330bfad5..b6958ebb43 100644
--- a/src/openhuman/tinycortex/persona.rs
+++ b/src/openhuman/tinycortex/persona.rs
@@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize};
use tinycortex::memory::persona::readers::{claude_code, codex, RawSession};
use tinycortex::memory::persona::state::FileStateStore;
use tinycortex::memory::persona::{PersonaConfig, Pipeline, RunMode};
+use walkdir::WalkDir;
use crate::openhuman::config::Config;
@@ -63,12 +64,10 @@ fn source_status(
kind: &str,
root: &Path,
max_files: usize,
- discover: impl Fn(&Path) -> Vec,
+ discover: impl Fn(&Path, usize) -> (Vec, bool),
read: impl Fn(&Path) -> anyhow::Result,
) -> CodingSessionSourceStatus {
- let mut files = discover(root);
- let scan_truncated = files.len() > max_files;
- files.truncate(max_files);
+ let (files, scan_truncated) = discover(root, max_files);
if scan_truncated {
tracing::debug!(
source = kind,
@@ -101,6 +100,48 @@ fn source_status(
}
}
+fn discover_session_files(
+ root: &Path,
+ max_files: usize,
+ is_candidate: impl Fn(&Path) -> bool,
+) -> (Vec, bool) {
+ let mut files = Vec::with_capacity(max_files.min(64));
+ for entry in WalkDir::new(root)
+ .sort_by_file_name()
+ .into_iter()
+ .filter_map(Result::ok)
+ .filter(|entry| entry.file_type().is_file())
+ {
+ let path = entry.path();
+ if !is_candidate(path) {
+ continue;
+ }
+ if files.len() == max_files {
+ return (files, true);
+ }
+ files.push(path.to_path_buf());
+ }
+ (files, false)
+}
+
+fn discover_claude_sessions(root: &Path, max_files: usize) -> (Vec, bool) {
+ discover_session_files(root, max_files, |path| {
+ path.extension()
+ .is_some_and(|extension| extension == "jsonl")
+ })
+}
+
+fn discover_codex_sessions(root: &Path, max_files: usize) -> (Vec, bool) {
+ discover_session_files(root, max_files, |path| {
+ path.extension()
+ .is_some_and(|extension| extension == "jsonl")
+ && path
+ .file_name()
+ .and_then(|name| name.to_str())
+ .is_some_and(|name| name.starts_with("rollout-"))
+ })
+}
+
pub fn coding_session_status_for_roots(
claude_root: &Path,
codex_root: &Path,
@@ -111,14 +152,14 @@ pub fn coding_session_status_for_roots(
"claude_code",
claude_root,
MAX_STATUS_SESSION_FILES,
- claude_code::discover,
+ discover_claude_sessions,
claude_code::read_session,
),
source_status(
"codex",
codex_root,
MAX_STATUS_SESSION_FILES,
- codex::discover,
+ discover_codex_sessions,
codex::read_session,
),
];
@@ -285,7 +326,7 @@ mod tests {
"fixture",
Path::new("."),
1,
- |_| paths.clone(),
+ |_, max_files| (paths[..max_files].to_vec(), paths.len() > max_files),
|_| {
reads.set(reads.get() + 1);
Ok(RawSession::new(
@@ -300,4 +341,18 @@ mod tests {
assert_eq!(status.session_files, 1);
assert!(status.scan_truncated);
}
+
+ #[test]
+ fn bounded_discovery_stops_after_finding_one_extra_candidate() {
+ let temp = tempdir().unwrap();
+ fs::write(temp.path().join("a.jsonl"), "").unwrap();
+ fs::write(temp.path().join("b.jsonl"), "").unwrap();
+ fs::write(temp.path().join("ignored.txt"), "").unwrap();
+
+ let (files, truncated) = discover_claude_sessions(temp.path(), 1);
+
+ assert_eq!(files.len(), 1);
+ assert_eq!(files[0].file_name().unwrap(), "a.jsonl");
+ assert!(truncated);
+ }
}
From 5f20d8bad9cb4562806774803ee7ae28e72f74be Mon Sep 17 00:00:00 2001
From: Steven Enamakel
Date: Tue, 14 Jul 2026 18:14:34 +0000
Subject: [PATCH 12/28] test(memory): align raw coverage with tinycortex
---
tests/raw_coverage/memory_raw_coverage_e2e.rs | 3 +++
.../memory_sources_closure_round23_raw_coverage_e2e.rs | 2 +-
tests/raw_coverage/memory_sync_sources_raw_coverage_e2e.rs | 2 +-
.../memory_sync_tree_round21_raw_coverage_e2e.rs | 1 +
tests/raw_coverage/memory_threads_raw_coverage_e2e.rs | 5 +++++
tests/raw_coverage/memory_tree_sync_deep_raw_coverage_e2e.rs | 1 +
6 files changed, 12 insertions(+), 2 deletions(-)
diff --git a/tests/raw_coverage/memory_raw_coverage_e2e.rs b/tests/raw_coverage/memory_raw_coverage_e2e.rs
index 5ca4c65b1d..cd9b2777dd 100644
--- a/tests/raw_coverage/memory_raw_coverage_e2e.rs
+++ b/tests/raw_coverage/memory_raw_coverage_e2e.rs
@@ -270,6 +270,9 @@ fn memory_tree_types_and_fallback_summary_cover_budget_and_legacy_parse_paths()
tree_kind: openhuman_core::openhuman::memory_store::trees::types::TreeKind::Global,
target_level: 2,
token_budget: 128,
+ input_token_budget: tinycortex::memory::config::INPUT_TOKEN_BUDGET,
+ overhead_reserve_tokens: tinycortex::memory::config::SUMMARY_OVERHEAD_RESERVE_TOKENS,
+ ask: None,
};
assert_eq!(ctx.tree_id, "tree-coverage");
assert_eq!(ctx.target_level, 2);
diff --git a/tests/raw_coverage/memory_sources_closure_round23_raw_coverage_e2e.rs b/tests/raw_coverage/memory_sources_closure_round23_raw_coverage_e2e.rs
index 1c5d017327..6eaebb9e7d 100644
--- a/tests/raw_coverage/memory_sources_closure_round23_raw_coverage_e2e.rs
+++ b/tests/raw_coverage/memory_sources_closure_round23_raw_coverage_e2e.rs
@@ -167,7 +167,7 @@ async fn round23_memory_sources_status_registry_and_readers_cover_remaining_edge
MemorySourcePatch {
label: Some("Round23 Folder Updated".to_string()),
enabled: Some(false),
- glob: Some("**/*.md".to_string()),
+ glob: Some(Some("**/*.md".to_string())),
..MemorySourcePatch::default()
},
)
diff --git a/tests/raw_coverage/memory_sync_sources_raw_coverage_e2e.rs b/tests/raw_coverage/memory_sync_sources_raw_coverage_e2e.rs
index 82ddecd2f4..1b1b052ca8 100644
--- a/tests/raw_coverage/memory_sync_sources_raw_coverage_e2e.rs
+++ b/tests/raw_coverage/memory_sync_sources_raw_coverage_e2e.rs
@@ -166,7 +166,7 @@ async fn memory_sources_registry_persists_crud_and_composio_upserts() {
MemorySourcePatch {
label: Some("Renamed notes".to_string()),
enabled: Some(false),
- glob: Some("*.txt".to_string()),
+ glob: Some(Some("*.txt".to_string())),
..MemorySourcePatch::default()
},
)
diff --git a/tests/raw_coverage/memory_sync_tree_round21_raw_coverage_e2e.rs b/tests/raw_coverage/memory_sync_tree_round21_raw_coverage_e2e.rs
index 538d652b96..e6fdc9434e 100644
--- a/tests/raw_coverage/memory_sync_tree_round21_raw_coverage_e2e.rs
+++ b/tests/raw_coverage/memory_sync_tree_round21_raw_coverage_e2e.rs
@@ -513,6 +513,7 @@ fn seed_source_summary(
id: format!("tree:{summary_id}"),
kind: TreeKind::Source,
scope: scope.to_string(),
+ ask: None,
root_id: Some(summary_id.to_string()),
max_level: 1,
status: TreeStatus::Active,
diff --git a/tests/raw_coverage/memory_threads_raw_coverage_e2e.rs b/tests/raw_coverage/memory_threads_raw_coverage_e2e.rs
index ce3f478019..c2c377f604 100644
--- a/tests/raw_coverage/memory_threads_raw_coverage_e2e.rs
+++ b/tests/raw_coverage/memory_threads_raw_coverage_e2e.rs
@@ -1921,6 +1921,9 @@ async fn memory_read_rpc_score_index_and_summary_helpers_cover_dashboard_paths()
tree_kind: TreeKind::Global,
target_level: 1,
token_budget: 100,
+ input_token_budget: tinycortex::memory::config::INPUT_TOKEN_BUDGET,
+ overhead_reserve_tokens: tinycortex::memory::config::SUMMARY_OVERHEAD_RESERVE_TOKENS,
+ ask: None,
};
let empty =
openhuman_core::openhuman::memory_tree::summarise::summarise(&config, &[], &empty_ctx)
@@ -1967,6 +1970,7 @@ fn memory_retrieval_embedding_and_rpc_model_helpers_round_trip() {
id: "tree-1".into(),
kind: TreeKind::Topic,
scope: "topic:coverage".into(),
+ ask: None,
root_id: Some("sum-1".into()),
max_level: 2,
status: StoredTreeStatus::Active,
@@ -2777,6 +2781,7 @@ fn memory_tree_io_contract_types_round_trip_leaf_read_and_write_shapes() {
id: "empty-tree".into(),
kind: TreeKind::Source,
scope: "source:contract".into(),
+ ask: None,
root_id: None,
max_level: 0,
status: StoredTreeStatus::Active,
diff --git a/tests/raw_coverage/memory_tree_sync_deep_raw_coverage_e2e.rs b/tests/raw_coverage/memory_tree_sync_deep_raw_coverage_e2e.rs
index bee2ea04ea..2342c7f2a9 100644
--- a/tests/raw_coverage/memory_tree_sync_deep_raw_coverage_e2e.rs
+++ b/tests/raw_coverage/memory_tree_sync_deep_raw_coverage_e2e.rs
@@ -169,6 +169,7 @@ fn seed_topic_summary(
id: format!("tree:{summary_id}"),
kind: TreeKind::Topic,
scope: entity_id.to_string(),
+ ask: None,
root_id: Some(summary_id.to_string()),
max_level: 2,
status: TreeStatus::Active,
From d4f05f5f7b679d85572f34642a0ca7f11d024525 Mon Sep 17 00:00:00 2001
From: Steven Enamakel
Date: Tue, 14 Jul 2026 18:43:22 +0000
Subject: [PATCH 13/28] test(composio): isolate provider mock backend
---
src/openhuman/composio/ops_tests.rs | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/openhuman/composio/ops_tests.rs b/src/openhuman/composio/ops_tests.rs
index a2ce40467d..be050c62b8 100644
--- a/src/openhuman/composio/ops_tests.rs
+++ b/src/openhuman/composio/ops_tests.rs
@@ -1017,6 +1017,10 @@ async fn composio_get_user_profile_via_mock_returns_provider_profile() {
}),
);
let base = start_mock_backend(app).await;
+ // ProviderContext reloads the saved config and applies runtime env
+ // overlays. Pin the backend override to the mock so CI's BACKEND_URL
+ // cannot redirect this request to the hosted API.
+ let _backend_url_guard = EnvVarGuard::set("BACKEND_URL", &base);
let tmp = tempfile::tempdir().unwrap();
let config = config_with_backend(&tmp, base);
let _workspace_env_guard = WorkspaceEnvGuard::set(tmp.path());
@@ -1168,6 +1172,9 @@ async fn composio_sync_gmail_via_mock_stores_skill_document_and_updates_outcome(
}),
);
let base = start_mock_backend(app).await;
+ // The provider action reloads config with env overlays before executing.
+ // Keep that reload on the mock even when the runner exports BACKEND_URL.
+ let _backend_url_guard = EnvVarGuard::set("BACKEND_URL", &base);
let tmp = tempfile::tempdir().unwrap();
let mut config = config_with_backend(&tmp, base);
config.memory_tree.embedding_strict = false;
From 5331e3296b242ac6d3f4a527347b8793ed17387f Mon Sep 17 00:00:00 2001
From: Steven Enamakel
Date: Tue, 14 Jul 2026 18:55:27 +0000
Subject: [PATCH 14/28] fix(memory): bound coding session ingestion batches
---
.../intelligence/CodingSessionsCard.tsx | 16 +++++----
.../__tests__/CodingSessionsCard.test.tsx | 28 ++++++++++++++++
app/src/lib/i18n/ar.ts | 2 ++
app/src/lib/i18n/bn.ts | 2 ++
app/src/lib/i18n/de.ts | 2 ++
app/src/lib/i18n/en.ts | 2 ++
app/src/lib/i18n/es.ts | 2 ++
app/src/lib/i18n/fr.ts | 2 ++
app/src/lib/i18n/hi.ts | 2 ++
app/src/lib/i18n/id.ts | 2 ++
app/src/lib/i18n/it.ts | 2 ++
app/src/lib/i18n/ko.ts | 2 ++
app/src/lib/i18n/pl.ts | 2 ++
app/src/lib/i18n/pt.ts | 2 ++
app/src/lib/i18n/ru.ts | 2 ++
app/src/lib/i18n/zh-CN.ts | 2 ++
app/src/services/memorySourcesService.test.ts | 5 +--
app/src/services/memorySourcesService.ts | 33 ++++++++++++++++---
src/openhuman/memory_sources/rpc.rs | 20 ++++++-----
19 files changed, 108 insertions(+), 22 deletions(-)
diff --git a/app/src/components/intelligence/CodingSessionsCard.tsx b/app/src/components/intelligence/CodingSessionsCard.tsx
index f95eaae4ec..4b864f537e 100644
--- a/app/src/components/intelligence/CodingSessionsCard.tsx
+++ b/app/src/components/intelligence/CodingSessionsCard.tsx
@@ -59,16 +59,20 @@ export function CodingSessionsCard({ onToast }: CodingSessionsCardProps) {
try {
const result = await ingestCodingSessions(false);
console.debug(
- '[coding-sessions] ingest: exit processed=%d failed=%d',
+ '[coding-sessions] ingest: exit processed=%d failed=%d budget_hit=%s',
result.sessions_processed,
- result.sessions_failed
+ result.sessions_failed,
+ result.budget_hit
);
+ const incomplete = result.sessions_failed > 0 || result.budget_hit;
onToast?.({
- type: result.sessions_failed > 0 ? 'warning' : 'success',
+ type: incomplete ? 'warning' : 'success',
title: t('memorySources.codingSessions.complete'),
- message: t('memorySources.codingSessions.completeMessage')
- .replace('{processed}', String(result.sessions_processed))
- .replace('{observations}', String(result.observations)),
+ message: result.budget_hit
+ ? t('memorySources.codingSessions.moreRemaining')
+ : t('memorySources.codingSessions.completeMessage')
+ .replace('{processed}', String(result.sessions_processed))
+ .replace('{observations}', String(result.observations)),
});
await load();
} catch (cause) {
diff --git a/app/src/components/intelligence/__tests__/CodingSessionsCard.test.tsx b/app/src/components/intelligence/__tests__/CodingSessionsCard.test.tsx
index 21e06b4f19..0d92af37d1 100644
--- a/app/src/components/intelligence/__tests__/CodingSessionsCard.test.tsx
+++ b/app/src/components/intelligence/__tests__/CodingSessionsCard.test.tsx
@@ -80,6 +80,34 @@ describe('CodingSessionsCard', () => {
expect(screen.getByTestId('coding-sessions-ingest')).toBeDisabled();
});
+ it('warns when more coding sessions remain after the current batch', async () => {
+ mockedIngest.mockResolvedValue({
+ mode: 'incremental',
+ files_seen: 30,
+ sessions_processed: 15,
+ sessions_skipped: 0,
+ sessions_failed: 0,
+ evidence_units: 40,
+ observations: 20,
+ budget_hit: true,
+ pack_path: '/workspace/persona/PERSONA.md',
+ });
+ const onToast = vi.fn();
+ renderWithProviders();
+
+ fireEvent.click(await screen.findByTestId('coding-sessions-ingest'));
+
+ await waitFor(() =>
+ expect(onToast).toHaveBeenCalledWith(
+ expect.objectContaining({
+ type: 'warning',
+ message:
+ 'The session batch limit was reached. Run ingestion again to continue importing your history.',
+ })
+ )
+ );
+ });
+
it('shows status failures as an alert', async () => {
mockedStatus.mockRejectedValue(new Error('session scan failed'));
renderWithProviders();
diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts
index 006f371548..5910d028a7 100644
--- a/app/src/lib/i18n/ar.ts
+++ b/app/src/lib/i18n/ar.ts
@@ -7084,6 +7084,8 @@ const messages: TranslationMap = {
'memorySources.codingSessions.complete': 'تم استيعاب جلسات البرمجة',
'memorySources.codingSessions.completeMessage':
'أنتجت {processed} جلسات {observations} ملاحظات شخصية.',
+ 'memorySources.codingSessions.moreRemaining':
+ 'تم بلوغ حد دفعة الجلسات. شغّل الاستيعاب مرة أخرى لمتابعة استيراد سجلك.',
'memorySources.codingSessions.failed': 'فشل استيعاب جلسات البرمجة',
// Privacy status pill + per-action egress disclosure (#4437 / S3)
diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts
index 09f8b11dd8..1d0cfe51f5 100644
--- a/app/src/lib/i18n/bn.ts
+++ b/app/src/lib/i18n/bn.ts
@@ -7248,6 +7248,8 @@ const messages: TranslationMap = {
'memorySources.codingSessions.complete': 'কোডিং সেশন গ্রহণ সম্পন্ন',
'memorySources.codingSessions.completeMessage':
'{processed}টি সেশন থেকে {observations}টি পারসোনা পর্যবেক্ষণ তৈরি হয়েছে।',
+ 'memorySources.codingSessions.moreRemaining':
+ 'সেশন ব্যাচের সীমা পূর্ণ হয়েছে। আপনার ইতিহাস আমদানি চালিয়ে যেতে আবার গ্রহণ চালান।',
'memorySources.codingSessions.failed': 'কোডিং সেশন গ্রহণ ব্যর্থ হয়েছে',
// Privacy status pill + per-action egress disclosure (#4437 / S3)
diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts
index 73df370c2d..4b2048d10c 100644
--- a/app/src/lib/i18n/de.ts
+++ b/app/src/lib/i18n/de.ts
@@ -7464,6 +7464,8 @@ const messages: TranslationMap = {
'memorySources.codingSessions.complete': 'Coding-Sitzungen eingelesen',
'memorySources.codingSessions.completeMessage':
'{processed} Sitzungen ergaben {observations} Persona-Beobachtungen.',
+ 'memorySources.codingSessions.moreRemaining':
+ 'Das Sitzungslimit für diesen Durchlauf wurde erreicht. Starten Sie das Einlesen erneut, um den Import fortzusetzen.',
'memorySources.codingSessions.failed': 'Einlesen der Coding-Sitzungen fehlgeschlagen',
// Privacy status pill + per-action egress disclosure (#4437 / S3)
diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts
index e735a44355..2be41c2c04 100644
--- a/app/src/lib/i18n/en.ts
+++ b/app/src/lib/i18n/en.ts
@@ -7585,6 +7585,8 @@ const en: TranslationMap = {
'memorySources.codingSessions.complete': 'Coding sessions ingested',
'memorySources.codingSessions.completeMessage':
'{processed} sessions produced {observations} persona observations.',
+ 'memorySources.codingSessions.moreRemaining':
+ 'The session batch limit was reached. Run ingestion again to continue importing your history.',
'memorySources.codingSessions.failed': 'Coding-session ingestion failed',
};
diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts
index 9c848b3403..6942bb1474 100644
--- a/app/src/lib/i18n/es.ts
+++ b/app/src/lib/i18n/es.ts
@@ -7398,6 +7398,8 @@ const messages: TranslationMap = {
'memorySources.codingSessions.complete': 'Sesiones de programación ingeridas',
'memorySources.codingSessions.completeMessage':
'{processed} sesiones produjeron {observations} observaciones de personalidad.',
+ 'memorySources.codingSessions.moreRemaining':
+ 'Se alcanzó el límite de sesiones del lote. Ejecuta la ingesta de nuevo para seguir importando tu historial.',
'memorySources.codingSessions.failed': 'Falló la ingesta de sesiones de programación',
// Privacy status pill + per-action egress disclosure (#4437 / S3)
diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts
index 14a9a577de..dfd55c8123 100644
--- a/app/src/lib/i18n/fr.ts
+++ b/app/src/lib/i18n/fr.ts
@@ -7432,6 +7432,8 @@ const messages: TranslationMap = {
'memorySources.codingSessions.complete': 'Sessions de programmation ingérées',
'memorySources.codingSessions.completeMessage':
'{processed} sessions ont produit {observations} observations de persona.',
+ 'memorySources.codingSessions.moreRemaining':
+ 'La limite de sessions du lot a été atteinte. Relancez l’ingestion pour continuer à importer votre historique.',
'memorySources.codingSessions.failed': 'Échec de l’ingestion des sessions de programmation',
// Privacy status pill + per-action egress disclosure (#4437 / S3)
diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts
index f8531044de..9293edc724 100644
--- a/app/src/lib/i18n/hi.ts
+++ b/app/src/lib/i18n/hi.ts
@@ -7246,6 +7246,8 @@ const messages: TranslationMap = {
'memorySources.codingSessions.complete': 'कोडिंग सत्र शामिल हो गए',
'memorySources.codingSessions.completeMessage':
'{processed} सत्रों से {observations} व्यक्तित्व अवलोकन बने।',
+ 'memorySources.codingSessions.moreRemaining':
+ 'सत्र बैच की सीमा पूरी हो गई है। अपना इतिहास आयात करना जारी रखने के लिए फिर से अंतर्ग्रहण चलाएँ।',
'memorySources.codingSessions.failed': 'कोडिंग सत्र शामिल करना विफल रहा',
// Privacy status pill + per-action egress disclosure (#4437 / S3)
diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts
index 39f0a075bb..899e88a6b9 100644
--- a/app/src/lib/i18n/id.ts
+++ b/app/src/lib/i18n/id.ts
@@ -7280,6 +7280,8 @@ const messages: TranslationMap = {
'memorySources.codingSessions.complete': 'Sesi pemrograman telah diserap',
'memorySources.codingSessions.completeMessage':
'{processed} sesi menghasilkan {observations} pengamatan persona.',
+ 'memorySources.codingSessions.moreRemaining':
+ 'Batas batch sesi tercapai. Jalankan penyerapan lagi untuk melanjutkan impor riwayat Anda.',
'memorySources.codingSessions.failed': 'Gagal menyerap sesi pemrograman',
// Privacy status pill + per-action egress disclosure (#4437 / S3)
diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts
index f0f2a3e7f1..4ff78756ed 100644
--- a/app/src/lib/i18n/it.ts
+++ b/app/src/lib/i18n/it.ts
@@ -7388,6 +7388,8 @@ const messages: TranslationMap = {
'memorySources.codingSessions.complete': 'Sessioni di programmazione acquisite',
'memorySources.codingSessions.completeMessage':
'{processed} sessioni hanno prodotto {observations} osservazioni della persona.',
+ 'memorySources.codingSessions.moreRemaining':
+ 'È stato raggiunto il limite di sessioni del batch. Avvia di nuovo l’acquisizione per continuare a importare la cronologia.',
'memorySources.codingSessions.failed':
'Acquisizione delle sessioni di programmazione non riuscita',
diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts
index 07f62df8f1..f95ca5fa4f 100644
--- a/app/src/lib/i18n/ko.ts
+++ b/app/src/lib/i18n/ko.ts
@@ -7166,6 +7166,8 @@ const messages: TranslationMap = {
'memorySources.codingSessions.complete': '코딩 세션 수집 완료',
'memorySources.codingSessions.completeMessage':
'세션 {processed}개에서 페르소나 관찰 {observations}개를 만들었습니다.',
+ 'memorySources.codingSessions.moreRemaining':
+ '세션 배치 한도에 도달했습니다. 기록 가져오기를 계속하려면 수집을 다시 실행하세요.',
'memorySources.codingSessions.failed': '코딩 세션 수집 실패',
// Privacy status pill + per-action egress disclosure (#4437 / S3)
diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts
index 0dd1b1c3db..2cef42958e 100644
--- a/app/src/lib/i18n/pl.ts
+++ b/app/src/lib/i18n/pl.ts
@@ -7357,6 +7357,8 @@ const messages: TranslationMap = {
'memorySources.codingSessions.complete': 'Sesje programistyczne wczytane',
'memorySources.codingSessions.completeMessage':
'{processed} sesji utworzyło {observations} obserwacji persony.',
+ 'memorySources.codingSessions.moreRemaining':
+ 'Osiągnięto limit sesji w partii. Uruchom import ponownie, aby kontynuować wczytywanie historii.',
'memorySources.codingSessions.failed': 'Nie udało się wczytać sesji programistycznych',
// Privacy status pill + per-action egress disclosure (#4437 / S3)
diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts
index 19440e1167..013c805078 100644
--- a/app/src/lib/i18n/pt.ts
+++ b/app/src/lib/i18n/pt.ts
@@ -7371,6 +7371,8 @@ const messages: TranslationMap = {
'memorySources.codingSessions.complete': 'Sessões de programação ingeridas',
'memorySources.codingSessions.completeMessage':
'{processed} sessões produziram {observations} observações de persona.',
+ 'memorySources.codingSessions.moreRemaining':
+ 'O limite de sessões do lote foi atingido. Execute a ingestão novamente para continuar importando seu histórico.',
'memorySources.codingSessions.failed': 'Falha ao ingerir sessões de programação',
// Privacy status pill + per-action egress disclosure (#4437 / S3)
diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts
index 24791c0246..68cce968f7 100644
--- a/app/src/lib/i18n/ru.ts
+++ b/app/src/lib/i18n/ru.ts
@@ -7326,6 +7326,8 @@ const messages: TranslationMap = {
'memorySources.codingSessions.complete': 'Сеансы программирования загружены',
'memorySources.codingSessions.completeMessage':
'Обработано сеансов: {processed}; наблюдений персоны: {observations}.',
+ 'memorySources.codingSessions.moreRemaining':
+ 'Достигнут лимит сеансов в пакете. Запустите загрузку ещё раз, чтобы продолжить импорт истории.',
'memorySources.codingSessions.failed': 'Не удалось загрузить сеансы программирования',
// Privacy status pill + per-action egress disclosure (#4437 / S3)
diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts
index ef0ae126c2..0467e2b47b 100644
--- a/app/src/lib/i18n/zh-CN.ts
+++ b/app/src/lib/i18n/zh-CN.ts
@@ -6857,6 +6857,8 @@ const messages: TranslationMap = {
'memorySources.codingSessions.complete': '编程会话已摄取',
'memorySources.codingSessions.completeMessage':
'{processed} 个会话生成了 {observations} 条人格观察。',
+ 'memorySources.codingSessions.moreRemaining':
+ '已达到本批次的会话上限。请再次运行摄取以继续导入历史记录。',
'memorySources.codingSessions.failed': '编程会话摄取失败',
// Privacy status pill + per-action egress disclosure (#4437 / S3)
diff --git a/app/src/services/memorySourcesService.test.ts b/app/src/services/memorySourcesService.test.ts
index 7e5f93bead..ac2b3ca814 100644
--- a/app/src/services/memorySourcesService.test.ts
+++ b/app/src/services/memorySourcesService.test.ts
@@ -211,7 +211,7 @@ describe('memorySourcesService', () => {
expect(sources[0]).toMatchObject({ kind: 'codex', evidence_units: 5 });
});
- it('requests bounded incremental coding-session ingestion', async () => {
+ it('requests a timeout-aligned incremental coding-session batch', async () => {
mockedCall.mockResolvedValue({
result: {
mode: 'incremental',
@@ -230,7 +230,8 @@ describe('memorySourcesService', () => {
expect(mockedCall).toHaveBeenCalledWith({
method: 'openhuman.memory_sources_ingest_coding_sessions',
- params: { backfill: false, max_sessions: 25 },
+ params: { backfill: false, max_sessions: 15 },
+ timeoutMs: 585_000,
});
expect(result.sessions_processed).toBe(2);
});
diff --git a/app/src/services/memorySourcesService.ts b/app/src/services/memorySourcesService.ts
index 26b8072771..82db7ac964 100644
--- a/app/src/services/memorySourcesService.ts
+++ b/app/src/services/memorySourcesService.ts
@@ -222,6 +222,14 @@ export interface CodingSessionIngestResult {
pack_path?: string | null;
}
+// Keep interactive imports below the core RPC client's bounded ten-minute
+// ceiling. Larger histories are intentionally processed in repeatable batches;
+// `budget_hit` tells the card to invite the user to continue.
+const CODING_SESSION_BATCH_MAX = 15;
+const CODING_SESSION_BASE_TIMEOUT_MS = 120_000;
+const CODING_SESSION_PER_SESSION_TIMEOUT_MS = 30_000;
+const CODING_SESSION_RPC_GRACE_MS = 15_000;
+
export async function getCodingSessionStatus(): Promise {
log('coding_session_status: entry');
const resp = await callCoreRpc<{ sources: CodingSessionSourceStatus[] }>({
@@ -234,18 +242,33 @@ export async function getCodingSessionStatus(): Promise {
- log('ingest_coding_sessions: entry backfill=%s max_sessions=%d', backfill, maxSessions);
+ const boundedMaxSessions = Number.isFinite(maxSessions)
+ ? Math.min(Math.max(Math.trunc(maxSessions), 1), CODING_SESSION_BATCH_MAX)
+ : CODING_SESSION_BATCH_MAX;
+ const timeoutMs =
+ CODING_SESSION_BASE_TIMEOUT_MS +
+ boundedMaxSessions * CODING_SESSION_PER_SESSION_TIMEOUT_MS +
+ CODING_SESSION_RPC_GRACE_MS;
+ log(
+ 'ingest_coding_sessions: entry backfill=%s max_sessions=%d requested=%d timeout_ms=%d',
+ backfill,
+ boundedMaxSessions,
+ maxSessions,
+ timeoutMs
+ );
const resp = await callCoreRpc({
method: 'openhuman.memory_sources_ingest_coding_sessions',
- params: { backfill, max_sessions: maxSessions },
+ params: { backfill, max_sessions: boundedMaxSessions },
+ timeoutMs,
});
const data = unwrap(resp);
log(
- 'ingest_coding_sessions: exit processed=%d failed=%d',
+ 'ingest_coding_sessions: exit processed=%d failed=%d budget_hit=%s',
data.sessions_processed,
- data.sessions_failed
+ data.sessions_failed,
+ data.budget_hit
);
return data;
}
diff --git a/src/openhuman/memory_sources/rpc.rs b/src/openhuman/memory_sources/rpc.rs
index 08b6f3299e..64f41fc67c 100644
--- a/src/openhuman/memory_sources/rpc.rs
+++ b/src/openhuman/memory_sources/rpc.rs
@@ -51,15 +51,17 @@ pub async fn ingest_coding_sessions_rpc(
// multiplier before computing the budget.
let ingest_timeout =
std::time::Duration::from_secs(120 + (req.max_sessions.min(1_000) as u64) * 30);
- let response = tokio::time::timeout(
- ingest_timeout,
- tokio::task::spawn_blocking(move || {
- runtime.block_on(crate::openhuman::tinycortex::ingest_coding_sessions(
- &config, req,
- ))
- }),
- )
+ let response = tokio::task::spawn_blocking(move || {
+ runtime.block_on(async move {
+ tokio::time::timeout(
+ ingest_timeout,
+ crate::openhuman::tinycortex::ingest_coding_sessions(&config, req),
+ )
+ .await
+ })
+ })
.await
+ .map_err(|error| format!("join coding-session ingestion: {error}"))?
.map_err(|_elapsed| {
tracing::error!(
timeout_secs = ingest_timeout.as_secs(),
@@ -70,11 +72,11 @@ pub async fn ingest_coding_sessions_rpc(
ingest_timeout.as_secs()
)
})?
- .map_err(|error| format!("join coding-session ingestion: {error}"))?
.map_err(|error| format!("ingest coding sessions: {error:#}"))?;
tracing::info!(
processed = response.sessions_processed,
failed = response.sessions_failed,
+ budget_hit = response.budget_hit,
"[memory_sources] ingest_coding_sessions_rpc: exit"
);
Ok(RpcOutcome::new(response, vec![]))
From b871d096b65b02cd7efccb88ba1f912b4179a2ad Mon Sep 17 00:00:00 2001
From: Steven Enamakel
Date: Tue, 14 Jul 2026 19:21:54 +0000
Subject: [PATCH 15/28] test(tool-registry): serialize denial buffer cases
---
src/openhuman/tool_registry/denials.rs | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/src/openhuman/tool_registry/denials.rs b/src/openhuman/tool_registry/denials.rs
index 4e6ae832b1..9c6a1f6cb0 100644
--- a/src/openhuman/tool_registry/denials.rs
+++ b/src/openhuman/tool_registry/denials.rs
@@ -77,6 +77,12 @@ fn truncate_reason(reason: &str) -> String {
mod tests {
use super::*;
+ // These tests intentionally mutate the process-global denial buffer. Keep
+ // their clear/record/assert sequences atomic with respect to one another;
+ // the parallel libtest runner can otherwise clear a sibling's freshly
+ // recorded value between `record` and `list`.
+ static DENIAL_TEST_LOCK: Mutex<()> = Mutex::new(());
+
fn clear_denials_for_test() {
let mut buf = RECENT_DENIALS.lock().unwrap_or_else(|p| p.into_inner());
buf.clear();
@@ -84,6 +90,7 @@ mod tests {
#[test]
fn record_truncates_and_bounds() {
+ let _guard = DENIAL_TEST_LOCK.lock().unwrap_or_else(|p| p.into_inner());
clear_denials_for_test();
let long = "a".repeat(10_000);
for _ in 0..(MAX_DENIALS + 5) {
@@ -97,6 +104,7 @@ mod tests {
#[test]
fn record_ignores_empty_tool() {
+ let _guard = DENIAL_TEST_LOCK.lock().unwrap_or_else(|p| p.into_inner());
clear_denials_for_test();
record(" ", "policy", "denied", "reason");
// list() should not panic; we can't reliably assert length because tests may run in parallel.
@@ -105,6 +113,7 @@ mod tests {
#[test]
fn record_redacts_sensitive_reason_fragments() {
+ let _guard = DENIAL_TEST_LOCK.lock().unwrap_or_else(|p| p.into_inner());
clear_denials_for_test();
record(
"tool.secret",
From 29b1de84cba859eb0eb35ea036edbb37dfc80cc0 Mon Sep 17 00:00:00 2001
From: Steven Enamakel
Date: Tue, 14 Jul 2026 19:54:56 +0000
Subject: [PATCH 16/28] fix(agent): reject tool calls in wrap-up replies
---
.../agent/harness/session/turn/session_io.rs | 20 +++++++++++++++----
1 file changed, 16 insertions(+), 4 deletions(-)
diff --git a/src/openhuman/agent/harness/session/turn/session_io.rs b/src/openhuman/agent/harness/session/turn/session_io.rs
index cce7b01509..d308c6e3bc 100644
--- a/src/openhuman/agent/harness/session/turn/session_io.rs
+++ b/src/openhuman/agent/harness/session/turn/session_io.rs
@@ -160,12 +160,24 @@ impl Agent {
};
let usage = crate::openhuman::tinyagents::model::usage_info_from_response(&response);
let text = response.text();
- let checkpoint = if !text.trim().is_empty() {
+ // Tools are disabled for wrap-up calls, but text-protocol models can
+ // still ignore that instruction and emit an XML/P-format call in the
+ // response body. Treat both native and dispatcher-parsed calls as an
+ // invalid wrap-up so the caller uses its deterministic fallback.
+ let (_, parsed_tool_calls) = self.tool_dispatcher.parse_response(&response);
+ let attempted_tool_call =
+ !response.tool_calls().is_empty() || !parsed_tool_calls.is_empty();
+ let checkpoint = if attempted_tool_call {
+ tracing::warn!(
+ native_tool_calls = response.tool_calls().len(),
+ parsed_tool_calls = parsed_tool_calls.len(),
+ "[agent::session] wrap-up attempted a tool call; using deterministic fallback"
+ );
+ String::new()
+ } else if !text.trim().is_empty() {
text
- } else if response.tool_calls().is_empty() {
- streamed_text
} else {
- String::new()
+ streamed_text
};
(checkpoint, usage)
}
From 85514fb4d4fc978ef9fde729dcf5594ca327926b Mon Sep 17 00:00:00 2001
From: Steven Enamakel
Date: Tue, 14 Jul 2026 20:07:34 +0000
Subject: [PATCH 17/28] fix(agent): parse textual wrap-up tool calls
---
src/openhuman/agent/harness/session/turn/session_io.rs | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/openhuman/agent/harness/session/turn/session_io.rs b/src/openhuman/agent/harness/session/turn/session_io.rs
index d308c6e3bc..ec68508c34 100644
--- a/src/openhuman/agent/harness/session/turn/session_io.rs
+++ b/src/openhuman/agent/harness/session/turn/session_io.rs
@@ -161,10 +161,10 @@ impl Agent {
let usage = crate::openhuman::tinyagents::model::usage_info_from_response(&response);
let text = response.text();
// Tools are disabled for wrap-up calls, but text-protocol models can
- // still ignore that instruction and emit an XML/P-format call in the
- // response body. Treat both native and dispatcher-parsed calls as an
- // invalid wrap-up so the caller uses its deterministic fallback.
- let (_, parsed_tool_calls) = self.tool_dispatcher.parse_response(&response);
+ // still ignore that instruction and emit an XML tool call in the
+ // response body. Treat both native and text-parsed calls as an invalid
+ // wrap-up so the caller uses its deterministic fallback.
+ let (_, parsed_tool_calls) = crate::openhuman::agent::harness::parse_tool_calls(&text);
let attempted_tool_call =
!response.tool_calls().is_empty() || !parsed_tool_calls.is_empty();
let checkpoint = if attempted_tool_call {
From 9af3c229d483d3ab22b77f8b647c4e8a61548f0e Mon Sep 17 00:00:00 2001
From: Steven Enamakel
Date: Tue, 14 Jul 2026 20:34:34 +0000
Subject: [PATCH 18/28] test(memory): tolerate transient queue gate contention
---
src/openhuman/memory_queue/worker.rs | 30 ++++++++++++++++++++++++----
1 file changed, 26 insertions(+), 4 deletions(-)
diff --git a/src/openhuman/memory_queue/worker.rs b/src/openhuman/memory_queue/worker.rs
index da2cba7e3f..72dcf5a1fa 100644
--- a/src/openhuman/memory_queue/worker.rs
+++ b/src/openhuman/memory_queue/worker.rs
@@ -1030,10 +1030,32 @@ mod tests {
.unwrap()
.expect("enqueue backfill job");
- let processed = run_once(&cfg).await.unwrap();
- assert!(processed);
-
- let job = get_job(&cfg, &id).unwrap().expect("job should still exist");
+ // The TinyCortex LLM gate is process-global, so a parallel libtest can
+ // briefly own its single permit. In that case `run_once` legitimately
+ // defers this row for 50 ms with `llm concurrency gate busy` before the
+ // re-embed handler is reached. Retry that transient gate deferral so
+ // this test continues to pin the handler's own defer/reschedule path.
+ let mut job = None;
+ for _ in 0..20 {
+ let processed = run_once(&cfg).await.unwrap();
+ assert!(processed);
+ let current = get_job(&cfg, &id).unwrap().expect("job should still exist");
+ if current
+ .last_error
+ .as_deref()
+ .is_some_and(|reason| reason.contains("re-embed backfill"))
+ {
+ job = Some(current);
+ break;
+ }
+ assert_eq!(
+ current.last_error.as_deref(),
+ Some("llm concurrency gate busy"),
+ "unexpected defer reason before re-embed handler"
+ );
+ tokio::time::sleep(Duration::from_millis(60)).await;
+ }
+ let job = job.expect("re-embed handler should run after transient gate contention");
assert_eq!(job.kind, JobKind::ReembedBackfill);
assert_eq!(job.status, JobStatus::Ready);
assert_eq!(
From 2f88c98a2d95e5e9c47281b8224f82691dce48e3 Mon Sep 17 00:00:00 2001
From: Steven Enamakel
Date: Tue, 14 Jul 2026 21:12:43 +0000
Subject: [PATCH 19/28] test(memory): align raw fixtures with tinycortex
contracts
---
.../memory_threads_raw_coverage_e2e.rs | 44 ++++++++++---------
1 file changed, 23 insertions(+), 21 deletions(-)
diff --git a/tests/raw_coverage/memory_threads_raw_coverage_e2e.rs b/tests/raw_coverage/memory_threads_raw_coverage_e2e.rs
index c2c377f604..890267a67a 100644
--- a/tests/raw_coverage/memory_threads_raw_coverage_e2e.rs
+++ b/tests/raw_coverage/memory_threads_raw_coverage_e2e.rs
@@ -1601,11 +1601,18 @@ fn memory_tree_runtime_store_buffers_and_retrieval_wire_helpers() {
let summaries = tree_runtime_store::collect_root_summaries_with_caps(tmp.path(), 10, 12);
assert_eq!(summaries.len(), 1);
- assert_eq!(summaries[0].0, "slack_#eng");
+ let stored_namespace = tree_runtime_store::tree_dir(&config, namespace)
+ .parent()
+ .and_then(std::path::Path::file_name)
+ .and_then(std::ffi::OsStr::to_str)
+ .expect("sanitized namespace directory")
+ .to_string();
+ assert!(stored_namespace.starts_with("slack_#eng-"));
+ assert_eq!(summaries[0].0, stored_namespace);
assert!(summaries[0].1.contains("[... truncated]"));
assert_eq!(
tree_runtime_store::list_namespaces_with_root(&config).unwrap(),
- vec!["slack_#eng".to_string()]
+ vec![stored_namespace]
);
let ts = Utc.with_ymd_and_hms(2026, 5, 29, 13, 0, 0).unwrap();
@@ -2075,7 +2082,7 @@ fn memory_retrieval_embedding_and_rpc_model_helpers_round_trip() {
score: Some(0.9),
taint: Default::default(),
};
- assert_eq!(entry.category.to_string(), "testing");
+ assert_eq!(entry.category.to_string(), "custom:testing");
let opts = RecallOpts {
namespace: Some("default"),
category: Some(MemoryCategory::Conversation),
@@ -3956,8 +3963,7 @@ async fn memory_sources_registry_rpc_and_schema_handlers_cover_crud_edges() {
patch: serde_json::from_value(json!({
"label": "Disabled folder",
"enabled": false,
- "glob": "**/*.md",
- "max_items": 2
+ "glob": "**/*.md"
}))
.expect("patch"),
})
@@ -4703,17 +4709,15 @@ async fn memory_sources_types_registry_and_sync_state_cover_public_persistence_e
let patch: registry::MemorySourcePatch = serde_json::from_value(json!({
"label": "Updated repo",
"enabled": false,
- "toolkit": "github",
- "connection_id": "conn_repo",
- "path": "/tmp/repo",
- "glob": "**/*.md",
"url": "https://github.com/tinyhumansai/openhuman-skills",
"branch": "main",
"paths": ["skills", "README.md"],
- "query": "is:open",
- "since_days": 14,
- "max_items": 9,
- "selector": "main"
+ "max_tokens_per_sync": 1000,
+ "max_cost_per_sync_usd": 0.5,
+ "sync_depth_days": 30,
+ "max_commits": 5,
+ "max_issues": 6,
+ "max_prs": 7
}))
.expect("patch");
let updated = registry::update_source("src_repo", patch)
@@ -4721,20 +4725,18 @@ async fn memory_sources_types_registry_and_sync_state_cover_public_persistence_e
.expect("update repo source");
assert_eq!(updated.label, "Updated repo");
assert!(!updated.enabled);
- assert_eq!(updated.toolkit.as_deref(), Some("github"));
- assert_eq!(updated.connection_id.as_deref(), Some("conn_repo"));
- assert_eq!(updated.path.as_deref(), Some("/tmp/repo"));
- assert_eq!(updated.glob.as_deref(), Some("**/*.md"));
assert_eq!(
updated.url.as_deref(),
Some("https://github.com/tinyhumansai/openhuman-skills")
);
assert_eq!(updated.branch.as_deref(), Some("main"));
assert_eq!(updated.paths, vec!["skills", "README.md"]);
- assert_eq!(updated.query.as_deref(), Some("is:open"));
- assert_eq!(updated.since_days, Some(14));
- assert_eq!(updated.max_items, Some(9));
- assert_eq!(updated.selector.as_deref(), Some("main"));
+ assert_eq!(updated.max_tokens_per_sync, Some(1000));
+ assert_eq!(updated.max_cost_per_sync_usd, Some(0.5));
+ assert_eq!(updated.sync_depth_days, Some(30));
+ assert_eq!(updated.max_commits, Some(5));
+ assert_eq!(updated.max_issues, Some(6));
+ assert_eq!(updated.max_prs, Some(7));
let memory = Arc::new(
MemoryClient::from_workspace_dir(tmp.path().join("memory-sync-state"))
From 0ee940e8bbadedd1891699d5fb1f4da65561fc58 Mon Sep 17 00:00:00 2001
From: Steven Enamakel
Date: Tue, 14 Jul 2026 21:45:48 +0000
Subject: [PATCH 20/28] test(composio): isolate direct mode config reload
---
src/openhuman/composio/tools_tests.rs | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/openhuman/composio/tools_tests.rs b/src/openhuman/composio/tools_tests.rs
index 5a8849f03c..ec9779480f 100644
--- a/src/openhuman/composio/tools_tests.rs
+++ b/src/openhuman/composio/tools_tests.rs
@@ -881,10 +881,12 @@ async fn list_tools_in_direct_mode_returns_empty_without_hitting_backend() {
let tmp = tempfile::tempdir().expect("tempdir");
let _workspace_guard = WorkspaceEnvGuard::set(tmp.path());
+ let _home_guard = HomeEnvGuard::set(tmp.path());
let mut config = crate::openhuman::config::Config::default();
config.config_path = tmp.path().join("config.toml");
config.workspace_dir = tmp.path().join("workspace");
+ std::fs::create_dir_all(&config.workspace_dir).expect("create workspace dir");
config.composio.mode = crate::openhuman::config::schema::COMPOSIO_MODE_DIRECT.to_string();
config.composio.api_key = Some("test-direct-key".to_string());
config.save().await.expect("save fake config to disk");
From f8364be10ed3529bc5e0a7ed5da167bf39c5d715 Mon Sep 17 00:00:00 2001
From: Steven Enamakel
Date: Tue, 14 Jul 2026 22:22:20 +0000
Subject: [PATCH 21/28] fix(memory): address session ingestion review gaps
---
.../intelligence/CodingSessionsCard.tsx | 19 +++--
.../__tests__/CodingSessionsCard.test.tsx | 43 ++++++++++
app/src/lib/i18n/ar.ts | 2 +
app/src/lib/i18n/bn.ts | 2 +
app/src/lib/i18n/de.ts | 2 +
app/src/lib/i18n/en.ts | 2 +
app/src/lib/i18n/es.ts | 2 +
app/src/lib/i18n/fr.ts | 2 +
app/src/lib/i18n/hi.ts | 2 +
app/src/lib/i18n/id.ts | 2 +
app/src/lib/i18n/it.ts | 2 +
app/src/lib/i18n/ko.ts | 2 +
app/src/lib/i18n/pl.ts | 2 +
app/src/lib/i18n/pt.ts | 2 +
app/src/lib/i18n/ru.ts | 2 +
app/src/lib/i18n/zh-CN.ts | 2 +
.../agent/harness/session/turn/session_io.rs | 85 +++++++++++++++----
.../agent/harness/session/turn_tests.rs | 83 +++++++++++++++++-
src/openhuman/tinycortex/persona.rs | 8 +-
19 files changed, 239 insertions(+), 27 deletions(-)
diff --git a/app/src/components/intelligence/CodingSessionsCard.tsx b/app/src/components/intelligence/CodingSessionsCard.tsx
index 4b864f537e..9eaeaf26c6 100644
--- a/app/src/components/intelligence/CodingSessionsCard.tsx
+++ b/app/src/components/intelligence/CodingSessionsCard.tsx
@@ -51,6 +51,8 @@ export function CodingSessionsCard({ onToast }: CodingSessionsCardProps) {
}),
[sources]
);
+ const hasImportableHistory =
+ totals.files > 0 || sources.some(source => source.scan_truncated === true);
const ingest = useCallback(async () => {
console.debug('[coding-sessions] ingest: entry');
@@ -68,11 +70,16 @@ export function CodingSessionsCard({ onToast }: CodingSessionsCardProps) {
onToast?.({
type: incomplete ? 'warning' : 'success',
title: t('memorySources.codingSessions.complete'),
- message: result.budget_hit
- ? t('memorySources.codingSessions.moreRemaining')
- : t('memorySources.codingSessions.completeMessage')
- .replace('{processed}', String(result.sessions_processed))
- .replace('{observations}', String(result.observations)),
+ message:
+ result.sessions_failed > 0
+ ? t('memorySources.codingSessions.partialFailure')
+ .replace('{failed}', String(result.sessions_failed))
+ .replace('{processed}', String(result.sessions_processed))
+ : result.budget_hit
+ ? t('memorySources.codingSessions.moreRemaining')
+ : t('memorySources.codingSessions.completeMessage')
+ .replace('{processed}', String(result.sessions_processed))
+ .replace('{observations}', String(result.observations)),
});
await load();
} catch (cause) {
@@ -101,7 +108,7 @@ export function CodingSessionsCard({ onToast }: CodingSessionsCardProps) {