mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 23:14:37 +00:00
feat(memory): ingest Codex and Claude coding sessions (#4863)
Co-authored-by: M3gA-Mind <elvin@tinyhumans.ai>
This commit is contained in:
co-authored by
M3gA-Mind
parent
4be17632f0
commit
0b48b67099
@@ -706,7 +706,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
|
||||
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
with:
|
||||
ref: ${{ github.event.release.tag_name }}
|
||||
fetch-depth: 1
|
||||
submodules: true
|
||||
submodules: recursive
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@1.93.0
|
||||
- name: Cache Cargo
|
||||
|
||||
@@ -401,7 +401,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
|
||||
|
||||
@@ -297,7 +297,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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+20
-3
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -291,6 +291,7 @@ if should_run_suite "notifications"; then
|
||||
echo "## Running suite: notifications"
|
||||
run "test/e2e/specs/notifications.spec.ts" "notifications" "notifications"
|
||||
run "test/e2e/specs/memory-roundtrip.spec.ts" "memory-roundtrip" "notifications"
|
||||
run "test/e2e/specs/coding-session-memory.spec.ts" "coding-session-memory" "notifications"
|
||||
run "test/e2e/specs/cron-jobs-flow.spec.ts" "cron-jobs" "notifications"
|
||||
run "test/e2e/specs/autocomplete-flow.spec.ts" "autocomplete" "notifications"
|
||||
_mini_summary "notifications"
|
||||
|
||||
Generated
+20
-3
@@ -5662,7 +5662,7 @@ dependencies = [
|
||||
"tar",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tinyagents",
|
||||
"tinyagents 1.9.0",
|
||||
"tinychannels",
|
||||
"tinycortex",
|
||||
"tinyflows",
|
||||
@@ -9177,6 +9177,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"
|
||||
@@ -9237,7 +9254,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
"thiserror 2.0.18",
|
||||
"tinyagents",
|
||||
"tinyagents 2.0.0",
|
||||
"tokio",
|
||||
"toml 0.8.2",
|
||||
"tracing",
|
||||
@@ -9257,7 +9274,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
"tinyagents",
|
||||
"tinyagents 1.9.0",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
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<ToastNotification, 'id'>) => void;
|
||||
}
|
||||
|
||||
const SOURCE_LABEL_KEYS: Record<CodingSessionSourceStatus['kind'], string> = {
|
||||
claude_code: 'memorySources.codingSessions.claude',
|
||||
codex: 'memorySources.codingSessions.codex',
|
||||
};
|
||||
|
||||
export function CodingSessionsCard({ onToast }: CodingSessionsCardProps) {
|
||||
const { t } = useT();
|
||||
const [sources, setSources] = useState<CodingSessionSourceStatus[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [ingesting, setIngesting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(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 hasImportableHistory =
|
||||
totals.files > 0 || sources.some(source => source.scan_truncated === true);
|
||||
|
||||
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 budget_hit=%s',
|
||||
result.sessions_processed,
|
||||
result.sessions_failed,
|
||||
result.budget_hit
|
||||
);
|
||||
const incomplete = result.sessions_failed > 0 || result.budget_hit;
|
||||
onToast?.({
|
||||
type: incomplete ? 'warning' : 'success',
|
||||
title: t('memorySources.codingSessions.complete'),
|
||||
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) {
|
||||
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 (
|
||||
<section
|
||||
className="rounded-lg border border-line bg-surface p-4"
|
||||
data-testid="coding-sessions-card">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-content">
|
||||
{t('memorySources.codingSessions.title')}
|
||||
</h3>
|
||||
<p className="mt-1 text-xs text-content-secondary">
|
||||
{t('memorySources.codingSessions.description')}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
analyticsId="brain-sources-coding-sessions-ingest"
|
||||
size="sm"
|
||||
onClick={() => void ingest()}
|
||||
disabled={loading || ingesting || !hasImportableHistory}
|
||||
data-testid="coding-sessions-ingest">
|
||||
{ingesting
|
||||
? t('memorySources.codingSessions.ingesting')
|
||||
: t('memorySources.codingSessions.ingest')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 grid gap-2 sm:grid-cols-2">
|
||||
{sources.map(source => (
|
||||
<div
|
||||
key={source.kind}
|
||||
className="rounded-md border border-line-subtle bg-surface-secondary px-3 py-2"
|
||||
data-testid={`coding-session-source-${source.kind}`}>
|
||||
<div className="text-xs font-medium text-content">
|
||||
{t(SOURCE_LABEL_KEYS[source.kind])}
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-content-secondary">
|
||||
{source.available
|
||||
? t('memorySources.codingSessions.counts')
|
||||
.replace('{files}', String(source.session_files))
|
||||
.replace('{evidence}', String(source.evidence_units))
|
||||
: t('memorySources.codingSessions.notFound')}
|
||||
</div>
|
||||
{source.available && source.scan_truncated && (
|
||||
<div className="mt-1 text-xs text-amber-600 dark:text-amber-400">
|
||||
{t('memorySources.codingSessions.truncated')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{loading && (
|
||||
<p className="mt-3 text-xs text-content-secondary">
|
||||
{t('memorySources.codingSessions.scanning')}
|
||||
</p>
|
||||
)}
|
||||
{error && (
|
||||
<p className="mt-3 text-xs text-coral-600 dark:text-coral-400" role="alert">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
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<typeof import('../../../services/memorySourcesService')>(
|
||||
'../../../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(<CodingSessionsCard />);
|
||||
|
||||
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();
|
||||
expect(screen.getByTestId('coding-sessions-ingest')).toHaveAttribute(
|
||||
'data-analytics-id',
|
||||
'brain-sources-coding-sessions-ingest'
|
||||
);
|
||||
});
|
||||
|
||||
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(<CodingSessionsCard onToast={onToast} />);
|
||||
|
||||
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(<CodingSessionsCard />);
|
||||
|
||||
expect(await screen.findByText('No local history found')).toBeInTheDocument();
|
||||
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(<CodingSessionsCard onToast={onToast} />);
|
||||
|
||||
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('reports partial session failures in the warning toast', async () => {
|
||||
mockedIngest.mockResolvedValue({
|
||||
mode: 'incremental',
|
||||
files_seen: 5,
|
||||
sessions_processed: 3,
|
||||
sessions_skipped: 0,
|
||||
sessions_failed: 2,
|
||||
evidence_units: 8,
|
||||
observations: 4,
|
||||
budget_hit: false,
|
||||
pack_path: '/workspace/persona/PERSONA.md',
|
||||
});
|
||||
const onToast = vi.fn();
|
||||
renderWithProviders(<CodingSessionsCard onToast={onToast} />);
|
||||
|
||||
fireEvent.click(await screen.findByTestId('coding-sessions-ingest'));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(onToast).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'warning',
|
||||
message: '2 sessions failed while 3 were processed. Run ingestion again to retry them.',
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('shows status failures as an alert', async () => {
|
||||
mockedStatus.mockRejectedValue(new Error('session scan failed'));
|
||||
renderWithProviders(<CodingSessionsCard />);
|
||||
|
||||
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(<CodingSessionsCard onToast={onToast} />);
|
||||
|
||||
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(<CodingSessionsCard />);
|
||||
|
||||
expect(await screen.findByText('Scan limited to the first 1,000 session files.')).toBeVisible();
|
||||
});
|
||||
|
||||
it('keeps ingestion enabled when a capped scan has not found evidence yet', async () => {
|
||||
mockedStatus.mockResolvedValue([
|
||||
{
|
||||
kind: 'codex',
|
||||
available: true,
|
||||
session_files: 1000,
|
||||
evidence_units: 0,
|
||||
invalid_files: 1000,
|
||||
scan_truncated: true,
|
||||
},
|
||||
]);
|
||||
renderWithProviders(<CodingSessionsCard />);
|
||||
|
||||
expect(await screen.findByTestId('coding-sessions-ingest')).toBeEnabled();
|
||||
});
|
||||
});
|
||||
@@ -7101,6 +7101,25 @@ 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.truncated': 'اقتصر الفحص على أول 1,000 ملف جلسة.',
|
||||
'memorySources.codingSessions.complete': 'تم استيعاب جلسات البرمجة',
|
||||
'memorySources.codingSessions.completeMessage':
|
||||
'أنتجت {processed} جلسات {observations} ملاحظات شخصية.',
|
||||
'memorySources.codingSessions.partialFailure':
|
||||
'فشلت {failed} جلسات بينما تمت معالجة {processed}. شغّل الاستيعاب مرة أخرى لإعادة المحاولة.',
|
||||
'memorySources.codingSessions.moreRemaining':
|
||||
'تم بلوغ حد دفعة الجلسات. شغّل الاستيعاب مرة أخرى لمتابعة استيراد سجلك.',
|
||||
'memorySources.codingSessions.failed': 'فشل استيعاب جلسات البرمجة',
|
||||
'flows.canvas.sidePanelToggle': 'اللوحة الجانبية',
|
||||
'flows.canvas.legendTab': 'يدوي',
|
||||
|
||||
|
||||
@@ -7269,6 +7269,25 @@ 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.truncated': 'স্ক্যানটি প্রথম ১,০০০টি সেশন ফাইলে সীমাবদ্ধ ছিল।',
|
||||
'memorySources.codingSessions.complete': 'কোডিং সেশন গ্রহণ সম্পন্ন',
|
||||
'memorySources.codingSessions.completeMessage':
|
||||
'{processed}টি সেশন থেকে {observations}টি পারসোনা পর্যবেক্ষণ তৈরি হয়েছে।',
|
||||
'memorySources.codingSessions.partialFailure':
|
||||
'{processed}টি সেশন প্রক্রিয়া করার সময় {failed}টি ব্যর্থ হয়েছে। আবার চেষ্টা করতে গ্রহণ পুনরায় চালান।',
|
||||
'memorySources.codingSessions.moreRemaining':
|
||||
'সেশন ব্যাচের সীমা পূর্ণ হয়েছে। আপনার ইতিহাস আমদানি চালিয়ে যেতে আবার গ্রহণ চালান।',
|
||||
'memorySources.codingSessions.failed': 'কোডিং সেশন গ্রহণ ব্যর্থ হয়েছে',
|
||||
'flows.canvas.sidePanelToggle': 'সাইড প্যানেল',
|
||||
'flows.canvas.legendTab': 'ম্যানুয়াল',
|
||||
|
||||
|
||||
@@ -7482,6 +7482,26 @@ 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.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.',
|
||||
'memorySources.codingSessions.partialFailure':
|
||||
'{failed} Sitzungen sind fehlgeschlagen, während {processed} verarbeitet wurden. Starten Sie das Einlesen erneut.',
|
||||
'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',
|
||||
'flows.canvas.sidePanelToggle': 'Seitenleiste',
|
||||
'flows.canvas.legendTab': 'Manuell',
|
||||
|
||||
|
||||
@@ -7588,6 +7588,25 @@ 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.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.',
|
||||
'memorySources.codingSessions.partialFailure':
|
||||
'{failed} sessions failed while {processed} were processed. Run ingestion again to retry them.',
|
||||
'memorySources.codingSessions.moreRemaining':
|
||||
'The session batch limit was reached. Run ingestion again to continue importing your history.',
|
||||
'memorySources.codingSessions.failed': 'Coding-session ingestion failed',
|
||||
};
|
||||
|
||||
export default en;
|
||||
|
||||
@@ -7420,6 +7420,26 @@ 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.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.',
|
||||
'memorySources.codingSessions.partialFailure':
|
||||
'Fallaron {failed} sesiones mientras se procesaron {processed}. Ejecuta la ingesta de nuevo para reintentarlas.',
|
||||
'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',
|
||||
'flows.canvas.sidePanelToggle': 'Panel lateral',
|
||||
'flows.canvas.legendTab': 'Manual',
|
||||
|
||||
|
||||
@@ -7455,6 +7455,26 @@ 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.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.',
|
||||
'memorySources.codingSessions.partialFailure':
|
||||
'{failed} sessions ont échoué tandis que {processed} ont été traitées. Relancez l’ingestion pour réessayer.',
|
||||
'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',
|
||||
'flows.canvas.sidePanelToggle': 'Panneau latéral',
|
||||
'flows.canvas.legendTab': 'Manuel',
|
||||
|
||||
|
||||
@@ -7265,6 +7265,25 @@ 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.truncated': 'स्कैन पहले 1,000 सत्र फ़ाइलों तक सीमित था।',
|
||||
'memorySources.codingSessions.complete': 'कोडिंग सत्र शामिल हो गए',
|
||||
'memorySources.codingSessions.completeMessage':
|
||||
'{processed} सत्रों से {observations} व्यक्तित्व अवलोकन बने।',
|
||||
'memorySources.codingSessions.partialFailure':
|
||||
'{processed} सत्र संसाधित हुए, जबकि {failed} विफल रहे। दोबारा प्रयास करने के लिए अंतर्ग्रहण फिर चलाएँ।',
|
||||
'memorySources.codingSessions.moreRemaining':
|
||||
'सत्र बैच की सीमा पूरी हो गई है। अपना इतिहास आयात करना जारी रखने के लिए फिर से अंतर्ग्रहण चलाएँ।',
|
||||
'memorySources.codingSessions.failed': 'कोडिंग सत्र शामिल करना विफल रहा',
|
||||
'flows.canvas.sidePanelToggle': 'साइड पैनल',
|
||||
'flows.canvas.legendTab': 'मैनुअल',
|
||||
|
||||
|
||||
@@ -7303,6 +7303,25 @@ 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.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.',
|
||||
'memorySources.codingSessions.partialFailure':
|
||||
'{failed} sesi gagal sementara {processed} berhasil diproses. Jalankan penyerapan lagi untuk mencoba ulang.',
|
||||
'memorySources.codingSessions.moreRemaining':
|
||||
'Batas batch sesi tercapai. Jalankan penyerapan lagi untuk melanjutkan impor riwayat Anda.',
|
||||
'memorySources.codingSessions.failed': 'Gagal menyerap sesi pemrograman',
|
||||
'flows.canvas.sidePanelToggle': 'Panel samping',
|
||||
'flows.canvas.legendTab': 'Manual',
|
||||
|
||||
|
||||
@@ -7408,6 +7408,27 @@ 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.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.',
|
||||
'memorySources.codingSessions.partialFailure':
|
||||
'{failed} sessioni non sono riuscite mentre {processed} sono state elaborate. Avvia di nuovo l’acquisizione per riprovare.',
|
||||
'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',
|
||||
'flows.canvas.sidePanelToggle': 'Pannello laterale',
|
||||
'flows.canvas.legendTab': 'Manuale',
|
||||
|
||||
|
||||
@@ -7180,6 +7180,25 @@ 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.truncated': '스캔이 처음 1,000개 세션 파일로 제한되었습니다.',
|
||||
'memorySources.codingSessions.complete': '코딩 세션 수집 완료',
|
||||
'memorySources.codingSessions.completeMessage':
|
||||
'세션 {processed}개에서 페르소나 관찰 {observations}개를 만들었습니다.',
|
||||
'memorySources.codingSessions.partialFailure':
|
||||
'세션 {processed}개를 처리하는 동안 {failed}개가 실패했습니다. 다시 시도하려면 수집을 다시 실행하세요.',
|
||||
'memorySources.codingSessions.moreRemaining':
|
||||
'세션 배치 한도에 도달했습니다. 기록 가져오기를 계속하려면 수집을 다시 실행하세요.',
|
||||
'memorySources.codingSessions.failed': '코딩 세션 수집 실패',
|
||||
'flows.canvas.sidePanelToggle': '사이드 패널',
|
||||
'flows.canvas.legendTab': '수동',
|
||||
|
||||
|
||||
@@ -7378,6 +7378,26 @@ 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.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.',
|
||||
'memorySources.codingSessions.partialFailure':
|
||||
'Nie udało się przetworzyć {failed} sesji, a {processed} przetworzono. Uruchom import ponownie, aby spróbować jeszcze raz.',
|
||||
'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',
|
||||
'flows.canvas.sidePanelToggle': 'Panel boczny',
|
||||
'flows.canvas.legendTab': 'Ręczny',
|
||||
|
||||
|
||||
@@ -7389,6 +7389,26 @@ 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.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.',
|
||||
'memorySources.codingSessions.partialFailure':
|
||||
'{failed} sessões falharam enquanto {processed} foram processadas. Execute a ingestão novamente para tentar de novo.',
|
||||
'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',
|
||||
'flows.canvas.sidePanelToggle': 'Painel lateral',
|
||||
'flows.canvas.legendTab': 'Manual',
|
||||
|
||||
|
||||
@@ -7350,6 +7350,25 @@ 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.truncated': 'Сканирование ограничено первыми 1000 файлами сеансов.',
|
||||
'memorySources.codingSessions.complete': 'Сеансы программирования загружены',
|
||||
'memorySources.codingSessions.completeMessage':
|
||||
'Обработано сеансов: {processed}; наблюдений персоны: {observations}.',
|
||||
'memorySources.codingSessions.partialFailure':
|
||||
'Не удалось обработать сеансов: {failed}; обработано: {processed}. Запустите загрузку ещё раз для повтора.',
|
||||
'memorySources.codingSessions.moreRemaining':
|
||||
'Достигнут лимит сеансов в пакете. Запустите загрузку ещё раз, чтобы продолжить импорт истории.',
|
||||
'memorySources.codingSessions.failed': 'Не удалось загрузить сеансы программирования',
|
||||
'flows.canvas.sidePanelToggle': 'Боковая панель',
|
||||
'flows.canvas.legendTab': 'Вручную',
|
||||
|
||||
|
||||
@@ -6869,6 +6869,25 @@ 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.truncated': '扫描仅限前 1,000 个会话文件。',
|
||||
'memorySources.codingSessions.complete': '编程会话已摄取',
|
||||
'memorySources.codingSessions.completeMessage':
|
||||
'{processed} 个会话生成了 {observations} 条人格观察。',
|
||||
'memorySources.codingSessions.partialFailure':
|
||||
'{processed} 个会话已处理,{failed} 个失败。请再次运行摄取以重试。',
|
||||
'memorySources.codingSessions.moreRemaining':
|
||||
'已达到本批次的会话上限。请再次运行摄取以继续导入历史记录。',
|
||||
'memorySources.codingSessions.failed': '编程会话摄取失败',
|
||||
'flows.canvas.sidePanelToggle': '侧边栏',
|
||||
'flows.canvas.legendTab': '手动',
|
||||
|
||||
|
||||
@@ -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' && (
|
||||
<div className="space-y-5 animate-fade-up">
|
||||
<CodingSessionsCard onToast={addToast} />
|
||||
<MemorySourcesRegistry onToast={addToast} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -4,6 +4,8 @@ import { callCoreRpc } from './coreRpcClient';
|
||||
import {
|
||||
addMemorySource,
|
||||
applyAllIn,
|
||||
getCodingSessionStatus,
|
||||
ingestCodingSessions,
|
||||
listMemorySources,
|
||||
type MemorySourceEntry,
|
||||
removeMemorySource,
|
||||
@@ -190,4 +192,47 @@ 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 a timeout-aligned incremental coding-session batch', 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: 15 },
|
||||
timeoutMs: 585_000,
|
||||
});
|
||||
expect(result.sessions_processed).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -201,6 +201,78 @@ export async function applyAllIn(): Promise<ApplyAllInResult> {
|
||||
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;
|
||||
scan_truncated?: boolean;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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<CodingSessionSourceStatus[]> {
|
||||
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 = CODING_SESSION_BATCH_MAX
|
||||
): Promise<CodingSessionIngestResult> {
|
||||
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<CodingSessionIngestResult>({
|
||||
method: 'openhuman.memory_sources_ingest_coding_sessions',
|
||||
params: { backfill, max_sessions: boundedMaxSessions },
|
||||
timeoutMs,
|
||||
});
|
||||
const data = unwrap<CodingSessionIngestResult>(resp);
|
||||
log(
|
||||
'ingest_coding_sessions: exit processed=%d failed=%d budget_hit=%s',
|
||||
data.sessions_processed,
|
||||
data.sessions_failed,
|
||||
data.budget_hit
|
||||
);
|
||||
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
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -409,13 +410,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
|
||||
@@ -428,12 +429,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
|
||||
|
||||
@@ -489,7 +490,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`) |
|
||||
|
||||
<!-- 11.1.9 Vault Markdown Writes — removed: Knowledge Vaults dropped (vault domain + VaultPanel deleted). -->
|
||||
@@ -504,10 +505,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. |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -14,6 +14,12 @@ const DERIVED_TO_BACKEND: Option<CapabilityPrivacy> = Some(CapabilityPrivacy {
|
||||
destinations: &["OpenHuman backend", "TinyHumans Neocortex"],
|
||||
});
|
||||
|
||||
const CODING_SESSION_TO_BACKEND: Option<CapabilityPrivacy> = 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<CapabilityPrivacy> = 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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -118,6 +118,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("");
|
||||
|
||||
@@ -131,13 +131,11 @@ impl Agent {
|
||||
};
|
||||
|
||||
let mut streamed_text = String::new();
|
||||
let mut streamed_deltas = Vec::new();
|
||||
let mut completed = None;
|
||||
while let Some(item) = stream.next().await {
|
||||
match item {
|
||||
ModelStreamItem::MessageDelta(delta) if !delta.text.is_empty() => {
|
||||
streamed_text.push_str(&delta.text);
|
||||
streamed_deltas.push(delta.text);
|
||||
}
|
||||
ModelStreamItem::Completed(response) => completed = Some(response),
|
||||
ModelStreamItem::Failed(error) => {
|
||||
@@ -157,52 +155,84 @@ impl Agent {
|
||||
};
|
||||
let usage = crate::openhuman::tinyagents::model::usage_info_from_response(&response);
|
||||
let text = response.text();
|
||||
let checkpoint = if !text.trim().is_empty() {
|
||||
text
|
||||
} else if response.tool_calls().is_empty() {
|
||||
streamed_text.clone()
|
||||
} else {
|
||||
String::new()
|
||||
// Tools are disabled for wrap-up calls, but text-protocol models can
|
||||
// still ignore that instruction. Parse through the active dispatcher
|
||||
// so XML/JSON and registry-backed P-Format calls are all rejected. The
|
||||
// completed response and buffered deltas are checked independently:
|
||||
// some providers only preserve one of those representations.
|
||||
let parsed_call_count = |candidate: &str| {
|
||||
self.tool_dispatcher
|
||||
.parse_response(&ChatResponse {
|
||||
text: Some(candidate.to_string()),
|
||||
..ChatResponse::default()
|
||||
})
|
||||
.1
|
||||
.len()
|
||||
};
|
||||
if !checkpoint.trim().is_empty() {
|
||||
let mut provider_response = ChatResponse {
|
||||
text: Some(checkpoint.clone()),
|
||||
tool_calls: Vec::new(),
|
||||
usage: None,
|
||||
reasoning_content: None,
|
||||
};
|
||||
let (_, mut prompt_tool_calls) =
|
||||
self.tool_dispatcher.parse_response(&provider_response);
|
||||
if streamed_text != checkpoint {
|
||||
provider_response.text = Some(streamed_text);
|
||||
let (_, streamed_tool_calls) =
|
||||
self.tool_dispatcher.parse_response(&provider_response);
|
||||
prompt_tool_calls.extend(streamed_tool_calls);
|
||||
}
|
||||
if !prompt_tool_calls.is_empty() {
|
||||
tracing::warn!(
|
||||
parsed_tool_calls = prompt_tool_calls.len(),
|
||||
"[agent::session] wrap-up model returned a prompt-formatted tool call; using deterministic fallback"
|
||||
);
|
||||
return (String::new(), usage);
|
||||
}
|
||||
tracing::debug!(
|
||||
checkpoint_chars = checkpoint.chars().count(),
|
||||
buffered_deltas = streamed_deltas.len(),
|
||||
let parsed_response_calls = parsed_call_count(&text);
|
||||
let parsed_stream_calls = if streamed_text == text {
|
||||
parsed_response_calls
|
||||
} else {
|
||||
parsed_call_count(&streamed_text)
|
||||
};
|
||||
let native_tool_calls = response.tool_calls().len();
|
||||
let attempted_tool_call =
|
||||
native_tool_calls > 0 || parsed_response_calls > 0 || parsed_stream_calls > 0;
|
||||
let checkpoint = if attempted_tool_call {
|
||||
tracing::warn!(
|
||||
model = effective_model,
|
||||
iteration = iteration_for_stream,
|
||||
"[agent::session] wrap-up checkpoint validation passed"
|
||||
native_tool_calls,
|
||||
parsed_response_calls,
|
||||
parsed_stream_calls,
|
||||
"[agent::session] wrap-up attempted a tool call; using deterministic fallback"
|
||||
);
|
||||
String::new()
|
||||
} else if !text.trim().is_empty() {
|
||||
tracing::debug!(
|
||||
model = effective_model,
|
||||
iteration = iteration_for_stream,
|
||||
text_len = text.len(),
|
||||
"[agent::session] wrap-up selected completed response text"
|
||||
);
|
||||
text
|
||||
} else {
|
||||
tracing::debug!(
|
||||
model = effective_model,
|
||||
iteration = iteration_for_stream,
|
||||
text_len = streamed_text.len(),
|
||||
"[agent::session] wrap-up selected buffered stream text"
|
||||
);
|
||||
streamed_text
|
||||
};
|
||||
// Hold wrap-up deltas until protocol validation completes. Otherwise a
|
||||
// rejected XML/P-Format tool call briefly renders in chat even though
|
||||
// the caller subsequently replaces it with a deterministic fallback.
|
||||
if !checkpoint.is_empty() {
|
||||
if let Some(sink) = &self.on_progress {
|
||||
for delta in streamed_deltas {
|
||||
let _ = sink
|
||||
.send(AgentProgress::TextDelta {
|
||||
delta,
|
||||
iteration: iteration_for_stream,
|
||||
})
|
||||
.await;
|
||||
if let Err(error) = sink
|
||||
.send(AgentProgress::TextDelta {
|
||||
delta: checkpoint.clone(),
|
||||
iteration: iteration_for_stream,
|
||||
})
|
||||
.await
|
||||
{
|
||||
tracing::debug!(
|
||||
model = effective_model,
|
||||
iteration = iteration_for_stream,
|
||||
error = %error,
|
||||
"[agent::session] wrap-up progress sink closed"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
tracing::debug!(
|
||||
model = effective_model,
|
||||
iteration = iteration_for_stream,
|
||||
checkpoint_len = checkpoint.len(),
|
||||
used_deterministic_fallback = attempted_tool_call,
|
||||
"[agent::session] wrap-up checkpoint selection complete"
|
||||
);
|
||||
(checkpoint, usage)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use super::*;
|
||||
use crate::openhuman::agent::dispatcher::XmlToolDispatcher;
|
||||
use crate::openhuman::agent::dispatcher::{
|
||||
PFormatToolDispatcher, ToolDispatcher, XmlToolDispatcher,
|
||||
};
|
||||
use crate::openhuman::agent::hooks::{PostTurnHook, TurnContext};
|
||||
use crate::openhuman::agent::tool_policy::{
|
||||
GeneratedToolRuntimeContext, GeneratedToolRuntimeRisk, ToolPolicy, ToolPolicyDecision,
|
||||
@@ -340,6 +342,26 @@ fn make_agent_with_builder(
|
||||
post_turn_hooks: Vec<Arc<dyn PostTurnHook>>,
|
||||
config: crate::openhuman::config::AgentConfig,
|
||||
context_config: crate::openhuman::config::ContextConfig,
|
||||
) -> Agent {
|
||||
make_agent_with_builder_and_dispatcher(
|
||||
provider,
|
||||
tools,
|
||||
memory_loader,
|
||||
post_turn_hooks,
|
||||
config,
|
||||
context_config,
|
||||
Box::new(XmlToolDispatcher),
|
||||
)
|
||||
}
|
||||
|
||||
fn make_agent_with_builder_and_dispatcher(
|
||||
provider: Arc<dyn Provider>,
|
||||
tools: Vec<Box<dyn Tool>>,
|
||||
memory_loader: Box<dyn MemoryLoader>,
|
||||
post_turn_hooks: Vec<Arc<dyn PostTurnHook>>,
|
||||
config: crate::openhuman::config::AgentConfig,
|
||||
context_config: crate::openhuman::config::ContextConfig,
|
||||
tool_dispatcher: Box<dyn ToolDispatcher>,
|
||||
) -> Agent {
|
||||
let workspace = tempfile::TempDir::new().expect("temp workspace");
|
||||
let workspace_path = workspace.path().to_path_buf();
|
||||
@@ -357,7 +379,7 @@ fn make_agent_with_builder(
|
||||
.tools(tools)
|
||||
.memory(mem)
|
||||
.memory_loader(memory_loader)
|
||||
.tool_dispatcher(Box::new(XmlToolDispatcher))
|
||||
.tool_dispatcher(tool_dispatcher)
|
||||
.post_turn_hooks(post_turn_hooks)
|
||||
.config(config)
|
||||
.context_config(context_config)
|
||||
@@ -1036,6 +1058,63 @@ async fn turn_checkpoint_falls_back_to_deterministic_summary_when_model_summary_
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_checkpoint_rejects_pformat_wrapup_without_streaming_it() {
|
||||
let provider: Arc<dyn Provider> = Arc::new(SequenceProvider {
|
||||
responses: AsyncMutex::new(vec![
|
||||
Ok(ChatResponse {
|
||||
text: Some("<tool_call>{\"name\":\"echo\",\"arguments\":{}}</tool_call>".into()),
|
||||
..ChatResponse::default()
|
||||
}),
|
||||
Ok(ChatResponse {
|
||||
text: Some("<tool_call>echo[]</tool_call>".into()),
|
||||
..ChatResponse::default()
|
||||
}),
|
||||
]),
|
||||
requests: AsyncMutex::new(Vec::new()),
|
||||
});
|
||||
let tools: Vec<Box<dyn Tool>> = vec![Box::new(EchoTool)];
|
||||
let registry = crate::openhuman::agent::pformat::build_registry(&tools);
|
||||
let mut agent = make_agent_with_builder_and_dispatcher(
|
||||
provider,
|
||||
tools,
|
||||
Box::new(FixedMemoryLoader {
|
||||
context: String::new(),
|
||||
}),
|
||||
vec![],
|
||||
crate::openhuman::config::AgentConfig {
|
||||
max_tool_iterations: 1,
|
||||
..crate::openhuman::config::AgentConfig::default()
|
||||
},
|
||||
crate::openhuman::config::ContextConfig::default(),
|
||||
Box::new(PFormatToolDispatcher::new(registry)),
|
||||
);
|
||||
let (progress_tx, mut progress_rx) = tokio::sync::mpsc::channel(16);
|
||||
agent.set_on_progress(Some(progress_tx));
|
||||
|
||||
let reply = agent
|
||||
.turn("hello")
|
||||
.await
|
||||
.expect("P-Format wrap-up call should use the deterministic checkpoint");
|
||||
assert!(
|
||||
reply.contains("tool-call limit"),
|
||||
"P-Format wrap-up must be rejected, got: {reply}"
|
||||
);
|
||||
|
||||
agent.set_on_progress(None);
|
||||
let mut rendered_invalid_wrapup = false;
|
||||
while let Ok(progress) = progress_rx.try_recv() {
|
||||
if let crate::openhuman::agent::progress::AgentProgress::TextDelta { delta, .. } = progress
|
||||
{
|
||||
rendered_invalid_wrapup |= delta.contains("echo[]");
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
!rendered_invalid_wrapup,
|
||||
"rejected P-Format wrap-up must not be emitted to the progress sink"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_synthesizes_final_answer_when_tool_turn_yields_no_text() {
|
||||
// #4093: the model runs a tool and then yields a terminating response with
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -2851,7 +2851,7 @@ fn seeded_slack_send_message_contract_with_schema() -> ToolContract {
|
||||
output_fields: vec![],
|
||||
output_schema: None,
|
||||
primary_array_path: None,
|
||||
is_curated: true,
|
||||
is_curated: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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:<name>`
|
||||
// wire value — the form `memory_recall`/`Display` now emit — resolves
|
||||
// back to `Custom("<name>")` instead of `Custom("custom:<name>")`
|
||||
// (which would `Display` as `custom:custom:<name>` 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:<name>` wire value (the form `memory_recall` and
|
||||
/// `Display` now emit) must store as `Custom("<name>")`, not the
|
||||
/// double-prefixed `Custom("custom:<name>")` — otherwise it would `Display`
|
||||
/// as `custom:custom:<name>` 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();
|
||||
|
||||
@@ -36,9 +36,25 @@ 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"
|
||||
);
|
||||
}
|
||||
|
||||
#[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\""
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -110,7 +110,16 @@ impl Tool for MemoryHybridSearchTool {
|
||||
));
|
||||
}
|
||||
|
||||
let profile = WeightProfile::by_name(&parsed.mode);
|
||||
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
|
||||
)
|
||||
})?;
|
||||
let limit = parsed.limit.clamp(1, 50);
|
||||
|
||||
log::debug!(
|
||||
@@ -209,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}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -6,6 +6,82 @@ 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<crate::openhuman::tinycortex::CodingSessionSourceStatus>,
|
||||
}
|
||||
|
||||
pub async fn coding_session_status_rpc() -> Result<RpcOutcome<CodingSessionStatusResponse>, 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::<usize>(),
|
||||
"[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<RpcOutcome<crate::openhuman::tinycortex::CodingSessionIngestResponse>, 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();
|
||||
// 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::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(),
|
||||
"[memory_sources] ingest_coding_sessions_rpc: timed out"
|
||||
);
|
||||
format!(
|
||||
"ingest coding sessions: timed out after {}s",
|
||||
ingest_timeout.as_secs()
|
||||
)
|
||||
})?
|
||||
.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![]))
|
||||
}
|
||||
|
||||
// ── List ──
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
|
||||
@@ -135,6 +135,8 @@ pub fn all_controller_schemas() -> Vec<ControllerSchema> {
|
||||
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<RegisteredController> {
|
||||
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<String, Value>) -> ControllerFuture {
|
||||
Box::pin(async move { to_json(rpc::apply_all_in_rpc().await?) })
|
||||
}
|
||||
|
||||
fn handle_coding_session_status(_params: Map<String, Value>) -> ControllerFuture {
|
||||
Box::pin(async move { to_json(rpc::coding_session_status_rpc().await?) })
|
||||
}
|
||||
|
||||
fn handle_ingest_coding_sessions(params: Map<String, Value>) -> ControllerFuture {
|
||||
Box::pin(async move {
|
||||
let req = parse_value::<crate::openhuman::tinycortex::CodingSessionIngestRequest>(
|
||||
Value::Object(params),
|
||||
)?;
|
||||
to_json(rpc::ingest_coding_sessions_rpc(req).await?)
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_value<T: DeserializeOwned>(v: Value) -> Result<T, String> {
|
||||
serde_json::from_value(v).map_err(|e| format!("invalid params: {e}"))
|
||||
}
|
||||
|
||||
@@ -45,13 +45,24 @@ 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(|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]
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -1,29 +1,48 @@
|
||||
//! 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,
|
||||
}
|
||||
#[derive(Default)]
|
||||
pub struct HostTreeJobSink;
|
||||
|
||||
impl HostTreeJobSink {
|
||||
pub fn new(config: Config) -> Self {
|
||||
Self { config }
|
||||
pub fn new() -> 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<bool> {
|
||||
tracing::trace!(
|
||||
chunk_id,
|
||||
default_max_attempts,
|
||||
"[memory:ingest] enqueue extract job in chunk transaction"
|
||||
);
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +71,7 @@ pub fn context(
|
||||
) {
|
||||
(
|
||||
super::memory_config_from(config, config.workspace_dir.clone()),
|
||||
HostTreeJobSink::new(config.clone()),
|
||||
HostTreeJobSink::new(),
|
||||
scoring_config(config),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,446 @@
|
||||
//! 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 walkdir::WalkDir;
|
||||
|
||||
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;
|
||||
const MAX_STATUS_SESSION_FILE_BYTES: u64 = 4 * 1024 * 1024;
|
||||
const MAX_STATUS_TOTAL_BYTES: u64 = 16 * 1024 * 1024;
|
||||
|
||||
#[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,
|
||||
pub scan_truncated: bool,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
}
|
||||
|
||||
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,
|
||||
max_files: usize,
|
||||
discover: impl Fn(&Path, usize) -> (Vec<PathBuf>, bool),
|
||||
read: impl Fn(&Path) -> anyhow::Result<RawSession>,
|
||||
) -> CodingSessionSourceStatus {
|
||||
let (files, mut scan_truncated) = discover(root, 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;
|
||||
let mut bytes_scheduled = 0_u64;
|
||||
for path in &files {
|
||||
if let Ok(metadata) = path.metadata() {
|
||||
let file_bytes = metadata.len();
|
||||
if file_bytes > MAX_STATUS_SESSION_FILE_BYTES
|
||||
|| bytes_scheduled.saturating_add(file_bytes) > MAX_STATUS_TOTAL_BYTES
|
||||
{
|
||||
scan_truncated = true;
|
||||
tracing::debug!(
|
||||
source = kind,
|
||||
file_bytes,
|
||||
bytes_scheduled,
|
||||
max_file_bytes = MAX_STATUS_SESSION_FILE_BYTES,
|
||||
max_total_bytes = MAX_STATUS_TOTAL_BYTES,
|
||||
reason = "status-byte-budget",
|
||||
"[memory_persona] skipped coding session during bounded status scan"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
bytes_scheduled += file_bytes;
|
||||
}
|
||||
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,
|
||||
scan_truncated,
|
||||
}
|
||||
}
|
||||
|
||||
fn discover_session_files(
|
||||
root: &Path,
|
||||
max_files: usize,
|
||||
is_candidate: impl Fn(&Path) -> bool,
|
||||
) -> (Vec<PathBuf>, bool) {
|
||||
let mut files = Vec::with_capacity(max_files.min(64));
|
||||
// Keep traversal unsorted: `sort_by_file_name` buffers and sorts every
|
||||
// directory before yielding its first entry, which defeats `max_files`
|
||||
// for users with very large Codex day or Claude project directories.
|
||||
for entry in WalkDir::new(root)
|
||||
.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<PathBuf>, 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<PathBuf>, 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,
|
||||
) -> Vec<CodingSessionSourceStatus> {
|
||||
tracing::debug!("[memory_persona] coding session scan: entry");
|
||||
let statuses = vec![
|
||||
source_status(
|
||||
"claude_code",
|
||||
claude_root,
|
||||
MAX_STATUS_SESSION_FILES,
|
||||
discover_claude_sessions,
|
||||
claude_code::read_session,
|
||||
),
|
||||
source_status(
|
||||
"codex",
|
||||
codex_root,
|
||||
MAX_STATUS_SESSION_FILES,
|
||||
discover_codex_sessions,
|
||||
codex::read_session,
|
||||
),
|
||||
];
|
||||
tracing::debug!(
|
||||
files = statuses
|
||||
.iter()
|
||||
.map(|status| status.session_files)
|
||||
.sum::<usize>(),
|
||||
evidence = statuses
|
||||
.iter()
|
||||
.map(|status| status.evidence_units)
|
||||
.sum::<usize>(),
|
||||
invalid = statuses
|
||||
.iter()
|
||||
.map(|status| status.invalid_files)
|
||||
.sum::<usize>(),
|
||||
"[memory_persona] coding session scan: exit"
|
||||
);
|
||||
statuses
|
||||
}
|
||||
|
||||
pub fn coding_session_status() -> Vec<CodingSessionSourceStatus> {
|
||||
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<CodingSessionIngestResponse> {
|
||||
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).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).inspect_err(|error| {
|
||||
tracing::error!(
|
||||
error = %error,
|
||||
"[memory_persona] coding session ingestion: open state store failed"
|
||||
);
|
||||
})?;
|
||||
let report = Pipeline {
|
||||
config: &memory_config,
|
||||
persona: &persona,
|
||||
provider: provider.as_ref(),
|
||||
summariser: &summariser,
|
||||
store: &store,
|
||||
}
|
||||
.run(mode)
|
||||
.await
|
||||
.inspect_err(|error| {
|
||||
tracing::error!(
|
||||
error = %error,
|
||||
"[memory_persona] coding session ingestion: pipeline run failed"
|
||||
);
|
||||
})?;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
#[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,
|
||||
|_, max_files| (paths[..max_files].to_vec(), paths.len() > max_files),
|
||||
|_| {
|
||||
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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bounded_discovery_stops_after_finding_one_extra_candidate_without_ordering() {
|
||||
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].extension().unwrap(), "jsonl");
|
||||
assert!(truncated);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn status_scan_skips_oversized_sessions_without_parsing_them() {
|
||||
let temp = tempdir().unwrap();
|
||||
let oversized = temp.path().join("oversized.jsonl");
|
||||
let small = temp.path().join("small.jsonl");
|
||||
let file = fs::File::create(&oversized).unwrap();
|
||||
file.set_len(MAX_STATUS_SESSION_FILE_BYTES + 1).unwrap();
|
||||
fs::write(&small, "{}\n").unwrap();
|
||||
let reads = std::cell::Cell::new(0);
|
||||
|
||||
let status = source_status(
|
||||
"fixture",
|
||||
temp.path(),
|
||||
2,
|
||||
|_, _| (vec![oversized.clone(), small.clone()], false),
|
||||
|_| {
|
||||
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, 2);
|
||||
assert_eq!(status.invalid_files, 0);
|
||||
assert!(status.scan_truncated);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn status_scan_enforces_the_aggregate_byte_budget() {
|
||||
let temp = tempdir().unwrap();
|
||||
let paths = (0..5)
|
||||
.map(|index| {
|
||||
let path = temp.path().join(format!("session-{index}.jsonl"));
|
||||
let file = fs::File::create(&path).unwrap();
|
||||
file.set_len(MAX_STATUS_SESSION_FILE_BYTES).unwrap();
|
||||
path
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let reads = std::cell::Cell::new(0);
|
||||
|
||||
let status = source_status(
|
||||
"fixture",
|
||||
temp.path(),
|
||||
paths.len(),
|
||||
|_, _| (paths.clone(), false),
|
||||
|_| {
|
||||
reads.set(reads.get() + 1);
|
||||
Ok(RawSession::new(
|
||||
tinycortex::memory::persona::types::EvidenceSource::new(
|
||||
tinycortex::memory::persona::types::PersonaSourceKind::Codex,
|
||||
),
|
||||
))
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(reads.get(), 4);
|
||||
assert_eq!(status.session_files, 5);
|
||||
assert!(status.scan_truncated);
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -1113,6 +1113,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();
|
||||
|
||||
@@ -484,13 +484,16 @@ async fn max_iteration_checkpoint_uses_deterministic_fallback_and_hooks() {
|
||||
while let Ok(event) = progress_rx.try_recv() {
|
||||
streamed.push(event);
|
||||
}
|
||||
assert!(!streamed.iter().any(|event| matches!(
|
||||
event,
|
||||
openhuman_core::openhuman::agent::progress::AgentProgress::TextDelta {
|
||||
delta,
|
||||
iteration: 2
|
||||
} if delta == "checkpoint delta"
|
||||
)), "rejected checkpoint deltas must not leak to progress consumers");
|
||||
assert!(
|
||||
!streamed.iter().any(|event| matches!(
|
||||
event,
|
||||
openhuman_core::openhuman::agent::progress::AgentProgress::TextDelta {
|
||||
iteration: 2,
|
||||
..
|
||||
}
|
||||
)),
|
||||
"wrap-up deltas must stay buffered when the completed response contains an invalid tool call"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
@@ -1921,6 +1928,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 +1977,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,
|
||||
@@ -2071,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),
|
||||
@@ -2777,6 +2788,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,
|
||||
@@ -3951,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"),
|
||||
})
|
||||
@@ -4698,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)
|
||||
@@ -4716,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"))
|
||||
|
||||
@@ -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,
|
||||
|
||||
Vendored
+1
-1
Submodule vendor/tinycortex updated: 671e78a014...9a0603afbe
Reference in New Issue
Block a user