Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,12 @@ REFLEXIO_MAX_INTERACTION_CONTENT_TOKENS=512
# Concurrent durable extraction turns per process; acquired before a user lease.
# options: positive integer (optional; defaults to publish concurrency, currently 4)
REFLEXIO_PUBLISH_LEARNING_WORKERS=4
# Durable discovery timing is code-owned: 2-second polling, 300-second renewable lease.
# Pickup latency versus discovery read load.
# options: finite positive seconds within the platform wait range (default 2)
REFLEXIO_DURABLE_LEARNING_POLL_SECONDS=2
# Renewable background claim lifetime and worker-loss recovery.
# options: positive integral seconds within epoch/heartbeat headroom (default 300)
REFLEXIO_DURABLE_LEARNING_LEASE_SECONDS=300
# Inactivity delay in seconds before a quiet session is evaluated by agent-success
# evaluation. (optional; default 600 = 10 minutes)
GROUP_EVALUATION_DELAY_SECONDS=600
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,14 @@ Client (SDK / CLI / HTTP API)
└─ UnifiedSearchService → Profiles + user/agent playbooks
```

The import-light [inference launch resolver](reflexio/inference_launch.py) gives
providers and launchers one endpoint and process-ownership contract. An explicit
service URL attaches to an existing inference process; otherwise the launcher
owns a separate child. See [developer.md](developer.md) for migration and runtime details.
The shared legacy boolean parser lives in [env_utils.py](reflexio/env_utils.py)
so launchers can reuse it without importing server startup. The existing
`reflexio.server.env_utils.env_truthy` import remains a compatibility export.

See [developer.md](developer.md) for project structure, supported LLM providers, and development setup.

## Documentation
Expand Down
88 changes: 73 additions & 15 deletions developer.md
Original file line number Diff line number Diff line change
Expand Up @@ -803,24 +803,64 @@ instruction. Unprepared extraction keeps its turn-reference contract. Strict
source visibility validation, bounded corrective repair, and evidence-carrier
limits are unchanged.

### Durable extraction timing

`reflexio/durable_learning_policy.py` owns background discovery cadence (2 seconds)
and renewable user-claim duration (300 seconds). Library/server scheduling and
enterprise failure reporting share that policy. `REFLEXIO_DURABLE_LEARNING_POLL_SECONDS`
and `REFLEXIO_DURABLE_LEARNING_LEASE_SECONDS` are retired: unset/blank or former
default values remain accepted as ignored compatibility inputs; custom or invalid
values fail startup. Remove those entries before upgrading a deployment that
customized them. Worker capacity remains independently configurable through
`REFLEXIO_PUBLISH_LEARNING_WORKERS`; claim fencing and heartbeat renewal are unchanged.
### Supabase storage schema compatibility

`StorageConfigSupabase` accepts legacy `read_url`/`read_key` fields only to discard
them during normalization; they are absent from canonical output and the schema.
Enterprise Supabase reads use native SQL, so these HTTP reader fields did not
select a replica. `StorageConfigPostgres.read_db_url` and reader pool settings
remain supported for actual native replica routing.

### Stored configuration validation

Persisted reads use `validate_stored_config`: removed unknown fields are ignored
and missing fields use schema defaults, but invalid recognized values raise
`StoredConfigurationError` from `reflexio.config_migration`. Existing
`ConfigurationMigrationError` subclasses remain within this family. Local-file
loading and CLI storage discovery propagate these errors rather than creating or
writing a default policy. Correct the saved configuration before retrying.
Errors and adapter logs name only top-level schema settings; supplied values and
dynamic dictionary keys are omitted. API writes still use ordinary Pydantic
validation. Missing-file, unreadable JSON/decryption and explicit legacy-disk
handling are separate existing contracts, not changed by this validation rule.

### Durable extraction timing

Housekeeping timing and governance deletion batches live in
`reflexio/durable_learning_policy.py` resolves background discovery timing once
before worker construction. `REFLEXIO_DURABLE_LEARNING_POLL_SECONDS` controls
pickup latency and discovery read load (default 2 seconds);
`REFLEXIO_DURABLE_LEARNING_LEASE_SECONDS` controls renewable background claim
lifetime and worker-loss recovery (default 300 seconds). These are independent
supported deployment inputs again: the previous retirement fixed their values
without removing their operational purpose. Valid custom values now take effect;
review complete resolved deployment settings when upgrading from the retirement.

Blank values select defaults. Poll seconds must be finite and positive within
`threading.TIMEOUT_MAX`; lease seconds must be positive integral seconds within
signed-64-bit epoch and heartbeat wait headroom. Integral decimal spellings remain
accepted. Invalid values fail startup without disclosing supplied values.
`resolve_discovery_timing(mapping)` and `validate_discovery_timing_env(mapping)`
use the supplied mapping exclusively, even when empty. Scheduler and enterprise
failure reporting capture the same policy at construction; restart to change it.

Worker capacity remains independently configurable through
`REFLEXIO_PUBLISH_LEARNING_WORKERS`. Claim fencing and heartbeat renewal are
unchanged. Explicit/manual user leases and pending-run claims retain separate
lifetimes and are not controlled by these background discovery settings.


Housekeeping cadence and lineage timing migration checks live in
`reflexio/housekeeping_policy.py`. The former `lineage_gc.poll_interval_seconds`
and `governance_retention.audit_events_delete_batch_limit` settings accept only
legacy defaults (86400 and 500) during migration and are absent from canonical
configuration. Custom values fail validation; review them before upgrading.
Retention ages, grace windows and explicit holds remain supported.
accepts only its legacy default (86400) and is absent from canonical configuration;
custom lineage cadence still requires explicit migration. In contrast,
`governance_retention.audit_events_delete_batch_limit` is supported again: a
positive integer up to PostgreSQL int32, default 500. Enterprise audit cleanup
applies the configured limit separately to audit events and erasure tombstones
in the existing transaction. The earlier retirement did not eliminate this
throughput/transaction-size choice. Stored custom values are preserved; invalid
values fail loading without resetting other saved policy. Retention enablement,
ages, grace windows and holds remain independent. OSS accepts this enterprise
policy without executing audit-event cleanup.

### Session outcome cutovers

Expand All @@ -829,3 +869,21 @@ may mark `SessionJudgmentInputs.requests_ordered` to preserve database ordering
before public timestamps are converted to whole seconds. The internal
`OutcomePrefixPrecondition.verdict_settled_at` accepts fractional epoch seconds;
public outcome timestamps keep their existing integer representation.


### Inference endpoint and process ownership

`reflexio.inference_launch` is the import-light shared resolver for provider URLs,
CLI ordinary-child ownership, and enterprise Docker startup/health gating. An
explicit `REFLEXIO_EMBEDDING_SERVICE_URL`, including localhost, attaches to a
caller-owned process. With no URL, the launcher owns the separate inference
child; API workers never load the local model. The enterprise API-local query
child retains its independent reserved route.

The legacy daemon hostname and disable-local switch are retired. Custom legacy
hostname-only routes or disabled ordinary startup without an endpoint raise a
value-free `ConfigurationMigrationError`; specify the full URL to migrate.
Hostname-based local-service routes used a 30-second request default, while an
explicit URL selects the existing 2-second internal-service default. Set
`REFLEXIO_EMBEDDING_SERVICE_TIMEOUT_MS` explicitly when preserving that older
budget. Shadowed legacy settings do not override a canonical URL.
16 changes: 2 additions & 14 deletions docs/components/configure/config-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useSettings } from "@/hooks/use-settings";
import {
type ReflexioConfig,
defaultConfig,
hydrateConfig,
serializeConfig,
} from "@/lib/config-schema";
import {
Expand All @@ -29,19 +30,6 @@ type Status =
| { kind: "error"; message: string }
| { kind: "success"; message: string };

// Merge server response into our typed defaults. Server may omit optional
// fields; we rely on defaults so subsequent form renders stay stable.
function hydrate(raw: unknown): ReflexioConfig {
const base = defaultConfig();
if (!raw || typeof raw !== "object") return base;
const incoming = raw as Partial<ReflexioConfig>;
return {
...base,
...incoming,
agent_success_config: incoming.agent_success_config ?? null,
};
}

function errorMessage(err: unknown): string {
if (err instanceof Error) return err.message;
if (typeof err === "string") return err;
Expand Down Expand Up @@ -69,7 +57,7 @@ export function ConfigEditor() {
const body = await res.text();
throw new Error(`GET /api/get_config failed (${res.status}): ${body}`);
}
return hydrate(await res.json());
return hydrateConfig(await res.json());
}, [baseUrl, apiKey]);

const load = useCallback(async (signal?: AbortSignal) => {
Expand Down
55 changes: 40 additions & 15 deletions docs/components/configure/sections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@ import {
TextAreaField,
} from "./primitives";
import {
APIKeyConfig,
AgentSuccessConfig,
type APIKeyConfig,
type AgentSuccessConfig,
EXTRACTION_PRESET_LABELS,
ExtractionPreset,
LLMConfig,
type ExtractionPreset,
type LLMConfig,
PRESET_VALUES,
ProfileExtractorConfig,
ReflexioConfig,
StorageConfigSQLite,
ToolUseConfig,
UserPlaybookExtractorConfig,
type ProfileExtractorConfig,
type ReflexioConfig,
type StorageConfigSQLite,
type ToolUseConfig,
type UserPlaybookExtractorConfig,
defaultAgentSuccess,
defaultPlaybookExtractor,
defaultProfileExtractor,
Expand All @@ -38,6 +38,18 @@ import {

type SetConfig = (updater: (prev: ReflexioConfig) => ReflexioConfig) => void;

// Names are editable, so row identity belongs to the UI rather than the payload.
const toolItemIds = new WeakMap<ToolUseConfig, number>();
let nextToolItemId = 0;
function toolItemId(item: ToolUseConfig): number {
let id = toolItemIds.get(item);
if (id === undefined) {
id = nextToolItemId++;
toolItemIds.set(item, id);
}
return id;
}

export function StorageSection({
value,
setConfig,
Expand All @@ -46,10 +58,20 @@ export function StorageSection({
setConfig: SetConfig;
}) {
const current = value ?? { db_path: null };
// Preserve remote/managed snapshots without offering an incompatible SQLite edit.
const editable = Object.entries(current).every(
([key, field]) =>
key === "db_path" ||
((key === "type" || key === "storage_type") && field === "sqlite"),
);
return (
<Section
title="Storage (SQLite)"
description="This build only supports SQLite storage. Leave blank to use the default file from LOCAL_STORAGE_PATH."
description={
editable
? "Edit SQLite storage. Leave blank to use the default file from LOCAL_STORAGE_PATH."
: "Storage is managed outside this editor. Its configuration is preserved when saving."
}
>
<FieldRow
label="Database file path"
Expand All @@ -59,10 +81,11 @@ export function StorageSection({
<TextField
id="storage-db-path"
value={current.db_path}
disabled={!editable}
onChange={(v) =>
setConfig((prev) => ({
editable && setConfig((prev) => ({
...prev,
storage_config: { db_path: v },
storage_config: { ...prev.storage_config, db_path: v },
}))
Comment thread
guangyu-reflexio marked this conversation as resolved.
}
placeholder="(default)"
Expand Down Expand Up @@ -263,7 +286,7 @@ export function APIKeysSection({
id="key-openai"
type="password"
value={keys.openai?.api_key ?? null}
onChange={(v) => update({ openai: v ? { api_key: v } : null })}
onChange={(v) => update({ openai: { ...keys.openai, api_key: v } })}
placeholder="sk-…"
/>
</FieldRow>
Expand Down Expand Up @@ -730,7 +753,9 @@ export function ToolsSection({
const updateAt = (idx: number, patch: Partial<ToolUseConfig>) => {
setConfig((prev) => {
const next = [...(prev.tool_can_use ?? [])];
next[idx] = { ...next[idx], ...patch };
const previous = next[idx];
next[idx] = { ...previous, ...patch };
toolItemIds.set(next[idx], toolItemId(previous));
return { ...prev, tool_can_use: next };
});
};
Expand Down Expand Up @@ -761,7 +786,7 @@ export function ToolsSection({
)}
{items.map((item, idx) => (
<ListItemCard
key={idx}
key={toolItemId(item)}
title={item.tool_name || "(unnamed)"}
onRemove={() => removeAt(idx)}
>
Expand Down
32 changes: 28 additions & 4 deletions docs/lib/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ export interface StorageConfigSQLite {

export interface OpenAIConfig {
api_key: string | null;
azure_config?: {
api_key: string;
endpoint: string;
api_version: string;
deployment_name?: string | null;
} | null;
}

export interface SimpleKeyConfig {
Expand Down Expand Up @@ -194,6 +200,19 @@ export function defaultTool(): ToolUseConfig {
return { tool_name: "", tool_description: "" };
}

// Hydration keeps the complete server snapshot while defaults stabilize the UI.
// The editor renders a subset; its full-config save must retain other policies.
export function hydrateConfig(raw: unknown): ReflexioConfig {
const base = defaultConfig();
if (!raw || typeof raw !== "object") return base;
const incoming = raw as Partial<ReflexioConfig>;
return {
...base,
...incoming,
agent_success_config: incoming.agent_success_config ?? null,
};
}

// Strip empty strings → null and empty arrays → null so the server receives
// a clean payload. The Pydantic model rejects empty strings for `NonEmptyStr`
// fields, so leaving them in would produce confusing 422s.
Expand All @@ -220,11 +239,14 @@ export function serializeConfig(config: ReflexioConfig): unknown {
const entry = out[provider];
if (!entry || clean(entry.api_key) === null) out[provider] = null;
}
if (out.openai && clean(out.openai.api_key) === null) out.openai = null;
if (out.openai && clean(out.openai.api_key) === null) {
out.openai = out.openai.azure_config ? { ...out.openai, api_key: null } : null;
}
if (out.dashscope && clean(out.dashscope.api_key) === null) {
out.dashscope = null;
} else if (out.dashscope) {
out.dashscope = {
...out.dashscope,
api_key: out.dashscope.api_key,
api_base: clean(out.dashscope.api_base),
};
Expand All @@ -246,6 +268,7 @@ export function serializeConfig(config: ReflexioConfig): unknown {
const cleanLlm = (llm: LLMConfig | null): LLMConfig | null => {
if (!llm) return null;
const out: LLMConfig = {
...llm,
should_run_model_name: clean(llm.should_run_model_name),
generation_model_name: clean(llm.generation_model_name),
embedding_model_name: clean(llm.embedding_model_name),
Expand Down Expand Up @@ -298,9 +321,10 @@ export function serializeConfig(config: ReflexioConfig): unknown {
};

return {
storage_config: config.storage_config
? { db_path: clean(config.storage_config.db_path) }
: null,
...config,
storage_config: config.storage_config && "db_path" in config.storage_config
? { ...config.storage_config, db_path: clean(config.storage_config.db_path) }
: config.storage_config,
agent_context_prompt: clean(config.agent_context_prompt),
tool_can_use: config.tool_can_use?.length ? config.tool_can_use : null,
profile_extractor_config: cleanExtractor(config.profile_extractor_config),
Expand Down
Loading
Loading