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) {